refactor(openai): confine legacy codex repair to doctor

Confine retired OpenAI Codex identifiers to doctor repair and migration paths while keeping runtime OpenAI surfaces canonical.\n\nProof: focused Vitest; autoreview clean; AWS Crabbox check:changed run_3789cbe12413 (cbx_2c88b700810b) passed.
This commit is contained in:
Peter Steinberger
2026-05-31 14:03:17 +01:00
committed by GitHub
parent 2f7e6ec196
commit 7423e9cb66
17 changed files with 288 additions and 74 deletions

View File

@@ -1,6 +1,5 @@
{
"id": "openai",
"legacyPluginIds": ["openai-codex"],
"activation": {
"onStartup": false
},

View File

@@ -106,7 +106,7 @@ describe("OpenAI plugin manifest", () => {
});
it("routes setup through the OpenAI setup runtime", () => {
expect(manifest.legacyPluginIds).toEqual(["openai-codex"]);
expect(manifest.legacyPluginIds).toBeUndefined();
expect(manifest.setup?.providers?.map((provider) => provider.id)).toEqual(["openai"]);
expect(manifest.providerAuthAliases).toBeUndefined();
});

View File

@@ -143,6 +143,7 @@ const shellInlineCommandInterpreters = new Set(["bash", "dash", "ksh", "sh", "zs
const remoteChangedGateEnv = [
"OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1",
"OPENCLAW_CHANGED_LANES_RAW_SYNC=1",
"CI=1",
];
const shellInlineCommandOptionsWithNextValue = new Set([
"+O",

View File

@@ -167,9 +167,6 @@ function resolveDescription({ manifest, packageJson }) {
const providers = Array.isArray(manifest.providers) ? manifest.providers : [];
if (providers.length > 0) {
if (manifest.providerAuthAliases?.["openai-codex"] === "openai") {
return `Adds ${displayList(providers)} model provider support to OpenClaw, including ChatGPT/Codex OAuth.`;
}
return `Adds ${displayList(providers)} model provider support to OpenClaw.`;
}

View File

@@ -15,8 +15,5 @@ describe("oauth refresh failure hints", () => {
expect(buildOAuthRefreshFailureLoginCommand("openai")).toBe(
"openclaw models auth login --provider openai",
);
expect(buildOAuthRefreshFailureLoginCommand("OpenAI-Codex")).toBe(
"openclaw models auth login --provider openai",
);
});
});

View File

@@ -11,9 +11,6 @@ export type OAuthRefreshFailureReason =
const OAUTH_REFRESH_FAILURE_PROVIDER_RE = /OAuth token refresh failed for ([^:]+):/i;
const SAFE_PROVIDER_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
const RETIRED_REAUTH_PROVIDER_IDS: Readonly<Record<string, string>> = {
"openai-codex": "openai",
};
function isOAuthRefreshFailureMessage(message: string): boolean {
const lower = message.toLowerCase();
@@ -72,10 +69,7 @@ export function classifyOAuthRefreshFailure(message: string): {
export function buildOAuthRefreshFailureLoginCommand(provider: string | null | undefined): string {
const sanitizedProvider = sanitizeOAuthRefreshFailureProvider(provider);
const reauthProvider = sanitizedProvider
? (RETIRED_REAUTH_PROVIDER_IDS[sanitizedProvider] ?? sanitizedProvider)
: null;
return reauthProvider
? formatCliCommand(`openclaw models auth login --provider ${reauthProvider}`)
return sanitizedProvider
? formatCliCommand(`openclaw models auth login --provider ${sanitizedProvider}`)
: formatCliCommand("openclaw models auth login");
}

View File

@@ -583,36 +583,6 @@ describe("repairSessionFileIfNeeded", () => {
expect(JSON.parse(lines[4])).toEqual(deliveryMirror);
});
it("repairs missing tool results in legacy OpenAI Codex transcripts", async () => {
const { file } = await createTempSessionPath();
const { header, message } = buildSessionHeaderAndMessage();
const toolCallAssistant = {
type: "message",
id: "msg-asst-legacy-process",
parentId: "msg-1",
timestamp: new Date().toISOString(),
message: {
role: "assistant",
provider: "openai-codex",
model: "gpt-5.5",
api: "openai-codex-responses",
content: [{ type: "toolCall", id: "call_process|fc_1", name: "process", arguments: {} }],
stopReason: "toolUse",
},
};
const original = `${JSON.stringify(header)}\n${JSON.stringify(message)}\n${JSON.stringify(toolCallAssistant)}\n`;
await fs.writeFile(file, original, "utf-8");
const result = await repairSessionFileIfNeeded({ sessionFile: file });
expect(result.repaired).toBe(true);
expect(result.insertedToolResults).toBe(1);
const lines = (await fs.readFile(file, "utf-8")).trimEnd().split("\n");
const inserted = JSON.parse(lines[3]);
expect(inserted.message.role).toBe("toolResult");
expect(inserted.message.toolCallId).toBe("call_process|fc_1");
});
it("does not duplicate code-mode tool results that are already persisted", async () => {
const { file } = await createTempSessionPath();
const { header, message } = buildSessionHeaderAndMessage();

View File

@@ -206,17 +206,10 @@ function isCodeModeToolCallRepairCandidate(entry: unknown): entry is SessionMess
provider?: unknown;
stopReason?: unknown;
};
// Persisted transcripts from the retired OpenAI Codex route still need this
// repair so replay sees a complete tool-call/tool-result pair.
const legacyOpenAIProvider = "openai-codex";
const legacyOpenAIResponsesApi = "openai-codex-responses";
const openAIProvider = message.provider === "openai" || message.provider === legacyOpenAIProvider;
const openAIResponsesApi =
message.api === "openai-chatgpt-responses" || message.api === legacyOpenAIResponsesApi;
return (
message.role === "assistant" &&
openAIResponsesApi &&
openAIProvider &&
message.api === "openai-chatgpt-responses" &&
message.provider === "openai" &&
message.stopReason !== "error" &&
message.stopReason !== "aborted"
);

View File

@@ -41,6 +41,9 @@ const LEGACY_CODEX_PROVIDER_ID = "openai-codex";
const CODEX_OAUTH_WARNING_TITLE = "Codex OAuth";
const OPENAI_BASE_URL = "https://api.openai.com/v1";
const LEGACY_CODEX_APIS = new Set(["openai-responses", "openai-completions"]);
const DOCTOR_REAUTH_PROVIDER_ALIASES: Readonly<Record<string, string>> = {
[LEGACY_CODEX_PROVIDER_ID]: OPENAI_PROVIDER_ID,
};
function hasConfiguredCodexOAuthProfile(cfg: OpenClawConfig): boolean {
return Object.values(cfg.auth?.profiles ?? {}).some(
@@ -221,7 +224,10 @@ export function formatOAuthRefreshFailureDoctorLine(params: {
if (!classified) {
return null;
}
const provider = classified.provider ?? params.provider;
const rawProvider = classified.provider ?? params.provider;
const provider = rawProvider
? (DOCTOR_REAUTH_PROVIDER_ALIASES[rawProvider] ?? rawProvider)
: null;
const command = buildOAuthRefreshFailureLoginCommand(provider);
if (classified.reason) {
return `- ${params.profileId}: re-auth required [${formatOAuthRefreshFailureReason(classified.reason)}] — Run \`${command}\`.`;

View File

@@ -143,11 +143,45 @@ describe("doctor session transcript repair", () => {
expect(note).toHaveBeenCalledTimes(1);
const [message, title] = requireFirstMockCall(note, "doctor note") as [string, string];
expect(title).toBe("Session transcripts");
expect(message).toContain("duplicated prompt-rewrite branches");
expect(message).toContain("legacy state");
expect(message).toContain('Run "openclaw doctor --fix"');
expect(countNonEmptyLines(await fs.readFile(filePath, "utf-8"))).toBe(3);
});
it("rewrites legacy OpenAI Codex transcript metadata only during doctor repair", async () => {
const filePath = await writeTranscript([
{ type: "session", version: 3, id: "session-1", timestamp: "2026-04-25T00:00:00Z" },
{
type: "message",
id: "legacy-assistant",
parentId: null,
message: {
role: "assistant",
provider: "openai-codex",
api: "openai-codex-responses",
content: [{ type: "text", text: "hello" }],
},
},
]);
const preview = await repairBrokenSessionTranscriptFile({ filePath, shouldRepair: false });
expect(preview.broken).toBe(true);
expect(preview.repaired).toBe(false);
expect(preview.legacyOpenAICodexEntries).toBe(1);
expect(await fs.readFile(filePath, "utf-8")).toContain("openai-codex");
const result = await repairBrokenSessionTranscriptFile({ filePath, shouldRepair: true });
expect(result.broken).toBe(true);
expect(result.repaired).toBe(true);
expect(result.legacyOpenAICodexEntries).toBe(1);
const lines = (await fs.readFile(filePath, "utf-8")).trim().split(/\r?\n/);
const assistant = JSON.parse(lines[1]);
expect(assistant.message.provider).toBe("openai");
expect(assistant.message.api).toBe("openai-chatgpt-responses");
});
it("ignores ordinary branch history without internal runtime context", async () => {
const filePath = await writeTranscript([
{ type: "session", version: 3, id: "session-1", timestamp: "2026-04-25T00:00:00Z" },

View File

@@ -23,10 +23,16 @@ type TranscriptRepairResult = {
repaired: boolean;
originalEntries: number;
activeEntries: number;
legacyOpenAICodexEntries: number;
backupPath?: string;
reason?: string;
};
const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex";
const OPENAI_PROVIDER_ID = "openai";
const LEGACY_OPENAI_CODEX_RESPONSES_API = "openai-codex-responses";
const OPENAI_CHATGPT_RESPONSES_API = "openai-chatgpt-responses";
function parseTranscriptEntries(raw: string): TranscriptEntry[] {
const entries: TranscriptEntry[] = [];
for (const line of raw.split(/\r?\n/)) {
@@ -59,6 +65,29 @@ function getMessage(entry: TranscriptEntry): Record<string, unknown> | null {
: null;
}
function normalizeLegacyOpenAICodexTranscriptMetadata(entries: TranscriptEntry[]): number {
let changed = 0;
for (const entry of entries) {
const message = getMessage(entry);
if (!message) {
continue;
}
let touched = false;
if (message.provider === LEGACY_OPENAI_CODEX_PROVIDER_ID) {
message.provider = OPENAI_PROVIDER_ID;
touched = true;
}
if (message.api === LEGACY_OPENAI_CODEX_RESPONSES_API) {
message.api = OPENAI_CHATGPT_RESPONSES_API;
touched = true;
}
if (touched) {
changed += 1;
}
}
return changed;
}
function textFromContent(content: unknown): string | null {
if (typeof content === "string") {
return content;
@@ -166,6 +195,19 @@ async function writeActiveTranscript(params: {
return backupPath;
}
async function writeTranscriptEntries(params: {
filePath: string;
entries: TranscriptEntry[];
}): Promise<string> {
const backupPath = `${params.filePath}.pre-doctor-openai-codex-repair-${new Date()
.toISOString()
.replace(/[:.]/g, "-")}.bak`;
await fs.copyFile(params.filePath, backupPath);
const next = params.entries.map((entry) => JSON.stringify(entry)).join("\n");
await fs.writeFile(params.filePath, `${next}\n`, "utf-8");
return backupPath;
}
export async function repairBrokenSessionTranscriptFile(params: {
filePath: string;
shouldRepair: boolean;
@@ -173,25 +215,41 @@ export async function repairBrokenSessionTranscriptFile(params: {
try {
const raw = await fs.readFile(params.filePath, "utf-8");
const entries = parseTranscriptEntries(raw);
const legacyOpenAICodexEntries = normalizeLegacyOpenAICodexTranscriptMetadata(entries);
const activePath = selectActivePath(entries);
if (!activePath) {
if (legacyOpenAICodexEntries > 0 && params.shouldRepair) {
const backupPath = await writeTranscriptEntries({ filePath: params.filePath, entries });
return {
filePath: params.filePath,
broken: true,
repaired: true,
originalEntries: entries.length,
activeEntries: 0,
legacyOpenAICodexEntries,
backupPath,
reason: "no active branch",
};
}
return {
filePath: params.filePath,
broken: false,
broken: legacyOpenAICodexEntries > 0,
repaired: false,
originalEntries: entries.length,
activeEntries: 0,
legacyOpenAICodexEntries,
reason: "no active branch",
};
}
const broken = hasBrokenPromptRewriteBranch(entries, activePath);
if (!broken) {
if (!broken && legacyOpenAICodexEntries === 0) {
return {
filePath: params.filePath,
broken: false,
repaired: false,
originalEntries: entries.length,
activeEntries: activePath.length,
legacyOpenAICodexEntries,
};
}
if (!params.shouldRepair) {
@@ -201,19 +259,23 @@ export async function repairBrokenSessionTranscriptFile(params: {
repaired: false,
originalEntries: entries.length,
activeEntries: activePath.length,
legacyOpenAICodexEntries,
};
}
const backupPath = await writeActiveTranscript({
filePath: params.filePath,
entries,
activePath,
});
const backupPath = broken
? await writeActiveTranscript({
filePath: params.filePath,
entries,
activePath,
})
: await writeTranscriptEntries({ filePath: params.filePath, entries });
return {
filePath: params.filePath,
broken: true,
repaired: true,
originalEntries: entries.length,
activeEntries: activePath.length,
legacyOpenAICodexEntries,
backupPath,
};
} catch (err) {
@@ -223,6 +285,7 @@ export async function repairBrokenSessionTranscriptFile(params: {
repaired: false,
originalEntries: 0,
activeEntries: 0,
legacyOpenAICodexEntries: 0,
reason: String(err),
};
}
@@ -275,11 +338,15 @@ export async function noteSessionTranscriptHealth(params?: {
const repairedCount = broken.filter((result) => result.repaired).length;
const lines = [
`- Found ${broken.length} transcript file${broken.length === 1 ? "" : "s"} with duplicated prompt-rewrite branches.`,
`- Found ${broken.length} transcript file${broken.length === 1 ? "" : "s"} with legacy state.`,
...broken.slice(0, 20).map((result) => {
const backup = result.backupPath ? ` backup=${shortenHomePath(result.backupPath)}` : "";
const status = result.repaired ? "repaired" : "needs repair";
return `- ${shortenHomePath(result.filePath)} ${status} entries=${result.originalEntries}->${result.activeEntries + 1}${backup}`;
const metadata =
result.legacyOpenAICodexEntries > 0
? ` openai-codex=${result.legacyOpenAICodexEntries}`
: "";
return `- ${shortenHomePath(result.filePath)} ${status} entries=${result.originalEntries}->${result.activeEntries + 1}${metadata}${backup}`;
}),
];
if (broken.length > 20) {

View File

@@ -1412,6 +1412,35 @@ describe("legacy migrate x_search auth", () => {
});
describe("legacy bundled provider discovery migrate", () => {
it("rewrites legacy OpenAI Codex plugin policy ids", () => {
const res = migrateLegacyConfigForTest({
plugins: {
allow: ["telegram", "openai-codex", "openai"],
deny: ["openai-codex"],
entries: {
"openai-codex": {
enabled: false,
},
},
slots: {
memory: "openai-codex",
},
},
});
expect(res.config?.plugins?.allow).toEqual(["telegram", "openai"]);
expect(res.config?.plugins?.deny).toEqual(["openai"]);
expect(res.config?.plugins?.entries?.openai).toEqual({ enabled: false });
expect(res.config?.plugins?.entries?.["openai-codex"]).toBeUndefined();
expect(res.config?.plugins?.slots?.memory).toBe("openai");
expect(res.changes).toContain("Rewrote plugins.allow openai-codex references to openai.");
expect(res.changes).toContain("Rewrote plugins.deny openai-codex references to openai.");
expect(res.changes).toContain(
"Rewrote plugins.entries.openai-codex to plugins.entries.openai.",
);
expect(res.changes).toContain("Rewrote plugins.slots openai-codex references to openai.");
});
it("sets compat mode for existing restrictive plugin allowlists", () => {
const res = migrateLegacyConfigForTest({
plugins: {

View File

@@ -6,6 +6,9 @@ import {
import { isRecord } from "./legacy-config-record-shared.js";
import { migrateLegacyXSearchConfig } from "./legacy-x-search-migrate.js";
const LEGACY_OPENAI_CODEX_PLUGIN_ID = "openai-codex";
const OPENAI_PLUGIN_ID = "openai";
const BUNDLED_DISCOVERY_COMPAT_RULE: LegacyConfigRule = {
path: ["plugins", "allow"],
message:
@@ -26,7 +29,95 @@ const X_SEARCH_RULE: LegacyConfigRule = {
'tools.web.x_search.apiKey moved to the xAI plugin; use plugins.entries.xai.config.webSearch.apiKey instead. Run "openclaw doctor --fix".',
};
function rewritePluginIdList(value: unknown): { next: unknown; changed: boolean } {
if (!Array.isArray(value)) {
return { next: value, changed: false };
}
let changed = false;
const seen = new Set<string>();
const next: unknown[] = [];
for (const entry of value) {
const replacement = entry === LEGACY_OPENAI_CODEX_PLUGIN_ID ? OPENAI_PLUGIN_ID : entry;
if (replacement !== entry) {
changed = true;
}
if (typeof replacement === "string") {
if (seen.has(replacement)) {
changed = true;
continue;
}
seen.add(replacement);
}
next.push(replacement);
}
return { next, changed };
}
function rewritePluginSlots(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
let changed = false;
for (const [slot, pluginId] of Object.entries(value)) {
if (pluginId === LEGACY_OPENAI_CODEX_PLUGIN_ID) {
value[slot] = OPENAI_PLUGIN_ID;
changed = true;
}
}
return changed;
}
function rewritePluginEntries(value: unknown): boolean {
if (!isRecord(value) || !(LEGACY_OPENAI_CODEX_PLUGIN_ID in value)) {
return false;
}
if (!(OPENAI_PLUGIN_ID in value)) {
value[OPENAI_PLUGIN_ID] = value[LEGACY_OPENAI_CODEX_PLUGIN_ID];
}
delete value[LEGACY_OPENAI_CODEX_PLUGIN_ID];
return true;
}
function rewriteLegacyOpenAICodexPluginPolicy(raw: Record<string, unknown>): string[] {
const plugins = isRecord(raw.plugins) ? raw.plugins : undefined;
if (!plugins) {
return [];
}
const changes: string[] = [];
for (const key of ["allow", "deny"] as const) {
const rewritten = rewritePluginIdList(plugins[key]);
if (rewritten.changed) {
plugins[key] = rewritten.next;
changes.push(`Rewrote plugins.${key} openai-codex references to openai.`);
}
}
if (rewritePluginEntries(plugins.entries)) {
changes.push("Rewrote plugins.entries.openai-codex to plugins.entries.openai.");
}
if (rewritePluginSlots(plugins.slots)) {
changes.push("Rewrote plugins.slots openai-codex references to openai.");
}
return changes;
}
export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_PROVIDERS: LegacyConfigMigrationSpec[] = [
defineLegacyConfigMigration({
id: "plugins.openai-codex->plugins.openai",
describe: "Rewrite retired OpenAI Codex plugin policy ids",
legacyRules: [
{
path: ["plugins"],
message:
'plugins.openai-codex references are retired; use the openai plugin id. Run "openclaw doctor --fix".',
requireSourceLiteral: true,
match: (_value, root) =>
rewriteLegacyOpenAICodexPluginPolicy(structuredClone(root)).length > 0,
},
],
apply: (raw, changes) => {
changes.push(...rewriteLegacyOpenAICodexPluginPolicy(raw));
},
}),
defineLegacyConfigMigration({
id: "plugins.allow->plugins.bundledDiscovery.compat",
describe: "Preserve bundled provider discovery for existing restrictive allowlists",

View File

@@ -247,6 +247,38 @@ describe("plugin registry install migration", () => {
expect(persisted?.plugins.map((plugin) => plugin.pluginId)).toEqual(["openai"]);
});
it("keeps legacy OpenAI Codex plugin references doctor-only", async () => {
const stateDir = makeTempDir();
const openaiDir = path.join(stateDir, "plugins", "openai");
const unusedBundledDir = path.join(stateDir, "plugins", "unused-bundled");
fs.mkdirSync(openaiDir, { recursive: true });
fs.mkdirSync(unusedBundledDir, { recursive: true });
const result = await migratePluginRegistryForInstall({
stateDir,
candidates: [
createCandidate(openaiDir, "openai", "bundled"),
createCandidate(unusedBundledDir, "unused-bundled", "bundled"),
],
readConfig: async () => ({
plugins: {
entries: {
"openai-codex": {
enabled: true,
},
},
},
}),
env: hermeticEnv(),
});
const current = requireMigratedIndex(result);
expect(current.plugins.map((plugin) => plugin.pluginId)).toEqual(["openai"]);
const persisted = await readPersistedInstalledPluginIndex({ stateDir });
expect(persisted?.plugins.map((plugin) => plugin.pluginId)).toEqual(["openai"]);
});
it("keeps bundled memory command plugins discoverable for first-run CLI registration", async () => {
const stateDir = makeTempDir();
const memoryDir = path.join(stateDir, "plugins", "memory-core");

View File

@@ -25,6 +25,9 @@ import type { PluginManifestRecord } from "../../../plugins/manifest-registry.js
export const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION";
export const FORCE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_FORCE_PLUGIN_REGISTRY_MIGRATION";
const DOCTOR_PLUGIN_ID_ALIASES: Readonly<Record<string, readonly string[]>> = {
openai: ["openai-codex"],
};
export type PluginRegistryInstallMigrationPreflightAction =
| "disabled"
@@ -150,6 +153,7 @@ function createMigrationPluginIdNormalizer(
...(plugin.setup?.cliBackends ?? []),
...Object.keys(plugin.modelCatalog?.providers ?? {}),
...(plugin.legacyPluginIds ?? []),
...(DOCTOR_PLUGIN_ID_ALIASES[plugin.id] ?? []),
]) {
const normalizedAlias = normalizeRegistryReference(alias);
if (normalizedAlias && !aliases.has(normalizedAlias)) {

View File

@@ -7,7 +7,6 @@ import { createVpsAwareOAuthHandlers } from "./provider-oauth-flow.js";
import type { ProviderAuthContext } from "./types.js";
const OPENAI_CODEX_PROVIDER_ID = "openai";
const OPENAI_CODEX_LEGACY_PROVIDER_ID = "openai-codex";
const OPENAI_CODEX_OAUTH_METHOD_ID = "oauth";
type OpenAICodexOAuthBridgeContext = ProviderAuthContext & {
@@ -45,8 +44,7 @@ function isOAuthCredential(value: unknown): value is OAuthCredentials {
const record = value as Record<string, unknown>;
return (
record.type === "oauth" &&
(record.provider === OPENAI_CODEX_PROVIDER_ID ||
record.provider === OPENAI_CODEX_LEGACY_PROVIDER_ID) &&
record.provider === OPENAI_CODEX_PROVIDER_ID &&
typeof record.access === "string" &&
typeof record.refresh === "string" &&
typeof record.expires === "number"

View File

@@ -312,7 +312,7 @@ function expectGroupedShellCommand(remoteCommand: string, command: string): void
}
const remoteChangedGateEnvPrefix =
"OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1";
"OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1";
const remoteChangedGateExport = `export ${remoteChangedGateEnvPrefix};`;
afterAll(() => {
@@ -1758,7 +1758,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expect(remoteCommand).toContain("git diff --cached --quiet");
expect(remoteCommand).toContain("commit -q --no-gpg-sign -m remote-changed-gate-tree");
expect(remoteCommand).toMatch(
/&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed$/u,
/&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 corepack pnpm check:changed$/u,
);
});
@@ -1935,7 +1935,9 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expect(remoteCommand).toContain(
"git fetch -q --depth=1 origin abc123:refs/remotes/origin/main",
);
expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`);
expect(remoteCommand).toContain(
`&& export OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1; ${shellScript}`,
);
});
it("preserves sparse changed-gate Git bootstrap for shell option values before -c", () => {
@@ -2079,7 +2081,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expect(output.args).toContain("--shell");
expect(remoteCommand).toContain("git init -q");
expect(remoteCommand).toMatch(
/&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 timeout 1200s node scripts\/check-changed\.mjs --base origin\/main --head HEAD$/u,
/&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 timeout 1200s node scripts\/check-changed\.mjs --base origin\/main --head HEAD$/u,
);
});
@@ -2102,7 +2104,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
expect(output.args).toContain("--shell");
expect(remoteCommand).toContain("git init -q");
expect(remoteCommand).toMatch(
/&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 timeout 1200s bash -lc 'pnpm check:changed'$/u,
/&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 timeout 1200s bash -lc 'pnpm check:changed'$/u,
);
});