mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
refactor: make OpenAI Codex legacy doctor-only (#88605)
This commit is contained in:
committed by
GitHub
parent
5976f14832
commit
00d17e9df7
@@ -19,7 +19,7 @@ or validating a change without wasting hours.
|
||||
Prove the touched surface first. Do not reflexively run the whole suite.
|
||||
|
||||
1. Inspect the diff and classify the touched surface:
|
||||
- normal source checkout, source change: `pnpm changed:lanes --json`, then `pnpm check:changed`
|
||||
- normal source checkout, source change: `pnpm changed:lanes --json`, then `pnpm check:changed` (delegates to Crabbox/Testbox)
|
||||
- normal source checkout, tests only: `pnpm test:changed`
|
||||
- normal source checkout, one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
|
||||
- Codex worktree or linked/sparse checkout, one/few explicit files: `node scripts/run-vitest.mjs <path-or-filter>`
|
||||
@@ -27,7 +27,7 @@ Prove the touched surface first. Do not reflexively run the whole suite.
|
||||
use the Crabbox wrapper with the provider that matches the proof surface.
|
||||
For maintainer heavy `pnpm` gates, that is usually delegated Blacksmith
|
||||
Testbox through Crabbox, e.g. `node scripts/crabbox-wrapper.mjs run
|
||||
--provider blacksmith-testbox ... -- pnpm check:changed`. For direct AWS
|
||||
--provider blacksmith-testbox ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed`. For direct AWS
|
||||
Crabbox proof, omit `--provider` and let `.crabbox.yaml` choose AWS.
|
||||
- workflow-only: `git diff --check`, workflow syntax/lint (`actionlint` when available)
|
||||
- docs-only: `pnpm docs:list`, docs formatter/lint only if docs tooling changed or requested
|
||||
@@ -66,7 +66,7 @@ scripts/crabbox-wrapper.mjs` for Testbox, and `git commit --no-verify` only
|
||||
|
||||
```bash
|
||||
pnpm changed:lanes --json
|
||||
pnpm check:changed # changed typecheck/lint/guards; no Vitest
|
||||
pnpm check:changed # Crabbox/Testbox changed typecheck/lint/guards; no Vitest
|
||||
pnpm test:changed # cheap smart changed Vitest targets
|
||||
pnpm verify # full check, then full Vitest
|
||||
OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed
|
||||
|
||||
@@ -95,8 +95,8 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
|
||||
- If raw Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch mode and will not exit on its own.
|
||||
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
|
||||
- Checks in a normal source checkout: `pnpm check:changed`; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
|
||||
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... --shell -- "pnpm check:changed"` so pnpm runs inside Testbox, not locally.
|
||||
- Checks in a normal source checkout: `pnpm check:changed` delegates to Crabbox/Testbox; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
|
||||
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed` so pnpm runs inside Testbox, not locally.
|
||||
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
|
||||
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); never add `tsc --noEmit`, `typecheck`, `check:types`.
|
||||
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `pnpm lint:*`, `scripts/run-oxlint.mjs`).
|
||||
|
||||
@@ -18,8 +18,8 @@ title: "Tests"
|
||||
- `pnpm test:changed`: cheap smart changed test run. It runs precise targets from direct test edits, sibling `*.test.ts` files, explicit source mappings, and the local import graph. Broad/config/package changes are skipped unless they map to precise tests.
|
||||
- `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed`: explicit broad changed test run. Use it when a test harness/config/package edit should fall back to Vitest's broader changed-test behavior.
|
||||
- `pnpm changed:lanes`: shows the architectural lanes triggered by the diff against `origin/main`.
|
||||
- `pnpm check:changed`: runs the smart changed check gate for the diff against `origin/main`. It runs typecheck, lint, and guard commands for the affected architectural lanes, but does not run Vitest tests. Use `pnpm test:changed` or explicit `pnpm test <target>` for test proof.
|
||||
- Codex worktrees and linked/sparse checkouts: avoid direct local `pnpm test*`, `pnpm check*`, and `pnpm crabbox:run` unless you have verified pnpm will not reconcile dependencies. For tiny explicit-file proof use `node scripts/run-vitest.mjs <path-or-filter>`; for changed gates or broad proof use `node scripts/crabbox-wrapper.mjs run --provider blacksmith-testbox ... --shell -- "pnpm check:changed"` so pnpm runs inside Testbox.
|
||||
- `pnpm check:changed`: delegates to Crabbox/Testbox by default outside CI, then runs the smart changed check gate for the diff against `origin/main` inside the remote child. It runs typecheck, lint, and guard commands for the affected architectural lanes, but does not run Vitest tests. Use `pnpm test:changed` or explicit `pnpm test <target>` for test proof.
|
||||
- Codex worktrees and linked/sparse checkouts: avoid direct local `pnpm test*`, `pnpm check*`, and `pnpm crabbox:run` unless you have verified pnpm will not reconcile dependencies. For tiny explicit-file proof use `node scripts/run-vitest.mjs <path-or-filter>`; for changed gates or broad proof use `node scripts/crabbox-wrapper.mjs run --provider blacksmith-testbox ... -- env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed` so pnpm runs inside Testbox.
|
||||
- `OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree <local-heavy-check command>`: keeps heavy-check serialization inside the current worktree instead of the Git common dir for commands such as `pnpm check:changed` and targeted `pnpm test ...`. Use it only on high-capacity local hosts when you intentionally run independent checks across linked worktrees.
|
||||
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs are full-suite proof: they use fixed shard groups, expand to leaf configs for local parallel execution, and print the expected local shard fanout before starting. The extension group always expands to the per-extension shard configs instead of one giant root-project process.
|
||||
- Test wrapper runs end with a short `[test] passed|failed|skipped ... in ...` summary. Vitest's own duration line stays the per-shard detail.
|
||||
|
||||
@@ -39,7 +39,6 @@ import type { HermesSource } from "./source.js";
|
||||
import type { PlannedTargets } from "./targets.js";
|
||||
|
||||
const OPENAI_PROVIDER_ID = "openai";
|
||||
const LEGACY_OPENAI_PROVIDER_ID = ["openai", "codex"].join("-");
|
||||
const OPENAI_DEFAULT_MODEL = "openai/gpt-5.5";
|
||||
const HERMES_AUTH_DISPLAY_NAME = "Hermes import";
|
||||
|
||||
@@ -143,10 +142,7 @@ function hasLegacyOpenAIOAuthTokenFields(value: unknown, keyHint = ""): boolean
|
||||
}
|
||||
const provider = readString(value.provider)?.toLowerCase();
|
||||
const normalizedKeyHint = keyHint.toLowerCase();
|
||||
const isOpenAIRecord =
|
||||
normalizedKeyHint.includes("openai") ||
|
||||
provider === OPENAI_PROVIDER_ID ||
|
||||
provider === LEGACY_OPENAI_PROVIDER_ID;
|
||||
const isOpenAIRecord = normalizedKeyHint.includes("openai") || provider === OPENAI_PROVIDER_ID;
|
||||
const hasTokenPair =
|
||||
(readString(value.access) && readString(value.refresh)) ||
|
||||
(readString(value.access_token) && readString(value.refresh_token));
|
||||
|
||||
@@ -183,17 +183,16 @@ describe("Hermes migration file and skill items", () => {
|
||||
await expectPathMissing(path.join(workspaceDir, "logs", "session.log"));
|
||||
});
|
||||
|
||||
it("reports legacy Hermes auth.json OAuth state as manual reauth work", async () => {
|
||||
it("reports legacy Hermes OpenAI auth.json OAuth state as manual reauth work", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const source = path.join(root, "hermes");
|
||||
const workspaceDir = path.join(root, "workspace");
|
||||
const stateDir = path.join(root, "state");
|
||||
const legacyOpenAIProvider = ["openai", "codex"].join("-");
|
||||
await writeFile(
|
||||
path.join(source, "auth.json"),
|
||||
JSON.stringify({
|
||||
providers: {
|
||||
[legacyOpenAIProvider]: {
|
||||
openai: {
|
||||
tokens: {
|
||||
access_token: "old-access",
|
||||
refresh_token: "old-refresh",
|
||||
@@ -201,7 +200,7 @@ describe("Hermes migration file and skill items", () => {
|
||||
},
|
||||
},
|
||||
credential_pool: {
|
||||
[legacyOpenAIProvider]: [
|
||||
openai: [
|
||||
{
|
||||
access_token: "pool-access",
|
||||
refresh_token: "pool-refresh",
|
||||
|
||||
@@ -18,12 +18,10 @@ const {
|
||||
(params: { provider: string; agentDir?: string }) => boolean
|
||||
>(() => false),
|
||||
listProfilesForProviderMock: vi.fn(
|
||||
(store: { profiles?: Record<string, { provider?: string }> }, provider: string) => {
|
||||
const normalize = (raw?: string) => (raw === ["openai", "codex"].join("-") ? "openai" : raw);
|
||||
return Object.entries(store.profiles ?? {})
|
||||
.filter(([, profile]) => normalize(profile.provider) === provider)
|
||||
.map(([profileId]) => profileId);
|
||||
},
|
||||
(store: { profiles?: Record<string, { provider?: string }> }, provider: string) =>
|
||||
Object.entries(store.profiles ?? {})
|
||||
.filter(([, profile]) => profile.provider === provider)
|
||||
.map(([profileId]) => profileId),
|
||||
),
|
||||
resolveApiKeyForProviderMock: vi.fn(
|
||||
async (_params?: {
|
||||
@@ -386,25 +384,6 @@ describe("openai image generation provider", () => {
|
||||
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(false);
|
||||
});
|
||||
|
||||
it("reports configured before doctor rewrites retired OpenAI auth profiles", () => {
|
||||
const provider = buildOpenAIImageGenerationProvider();
|
||||
isProviderApiKeyConfiguredMock.mockReturnValue(false);
|
||||
ensureAuthProfileStoreMock.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "oauth",
|
||||
provider: ["openai", "codex"].join("-"),
|
||||
access: "legacy-chatgpt-access",
|
||||
refresh: "legacy-chatgpt-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(true);
|
||||
});
|
||||
|
||||
it("does not report Codex OAuth image auth as configured for custom OpenAI endpoints", () => {
|
||||
const provider = buildOpenAIImageGenerationProvider();
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
|
||||
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
|
||||
const IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS = 200;
|
||||
const RAW_SYNC_CHANGED_LANES_ENV = "OPENCLAW_CHANGED_LANES_RAW_SYNC";
|
||||
|
||||
const DOCS_PATH_RE = /^(?:docs\/|README\.md$|AGENTS\.md$|.*\.mdx?$)/u;
|
||||
const APP_PATH_RE = /^(?:apps\/|Swabble\/|appcast\.xml$)/u;
|
||||
@@ -236,18 +238,38 @@ export function listChangedPathsFromGit(params) {
|
||||
if (!base) {
|
||||
return [];
|
||||
}
|
||||
const rangePaths = runGitNameOnlyDiff([`${base}...${head}`], cwd);
|
||||
let rangePaths;
|
||||
let noMergeBase = false;
|
||||
try {
|
||||
rangePaths = runGitNameOnlyDiff([`${base}...${head}`], cwd);
|
||||
} catch (error) {
|
||||
if (!isGitNoMergeBaseError(error)) {
|
||||
throw error;
|
||||
}
|
||||
noMergeBase = true;
|
||||
rangePaths = runGitNameOnlyDiff([`${base}..${head}`], cwd);
|
||||
}
|
||||
if (params.includeWorktree === false) {
|
||||
return rangePaths;
|
||||
}
|
||||
return [
|
||||
...new Set([
|
||||
...rangePaths,
|
||||
...runGitNameOnlyDiff(["--cached", "--diff-filter=ACMRD"], cwd),
|
||||
...runGitNameOnlyDiff(["--diff-filter=ACMRD"], cwd),
|
||||
...runGitLsFiles(["--others", "--exclude-standard"], cwd),
|
||||
]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
const worktreePaths = [
|
||||
...runGitNameOnlyDiff(["--cached", "--diff-filter=ACMRD"], cwd),
|
||||
...runGitNameOnlyDiff(["--diff-filter=ACMRD"], cwd),
|
||||
...runGitLsFiles(["--others", "--exclude-standard"], cwd),
|
||||
];
|
||||
// Raw Crabbox syncs can have unrelated synthetic refs; prefer the synced
|
||||
// worktree delta instead of turning that into an accidental whole-repo gate.
|
||||
if (
|
||||
noMergeBase &&
|
||||
process.env[RAW_SYNC_CHANGED_LANES_ENV] === "1" &&
|
||||
worktreePaths.length > 0 &&
|
||||
rangePaths.length > IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS
|
||||
) {
|
||||
rangePaths = [];
|
||||
}
|
||||
return [...new Set([...rangePaths, ...worktreePaths])].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
|
||||
function runGitNameOnlyDiff(extraArgs, cwd = process.cwd()) {
|
||||
@@ -260,6 +282,17 @@ function runGitNameOnlyDiff(extraArgs, cwd = process.cwd()) {
|
||||
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
|
||||
}
|
||||
|
||||
function isGitNoMergeBaseError(error) {
|
||||
const text = [
|
||||
error?.message,
|
||||
error?.stderr?.toString?.("utf8"),
|
||||
Array.isArray(error?.output)
|
||||
? error.output.map((value) => value?.toString?.("utf8")).join("\n")
|
||||
: "",
|
||||
].join("\n");
|
||||
return text.includes("no merge base");
|
||||
}
|
||||
|
||||
function runGitLsFiles(extraArgs, cwd = process.cwd()) {
|
||||
const output = execFileSync("git", ["ls-files", ...extraArgs], {
|
||||
cwd,
|
||||
@@ -270,8 +303,9 @@ function runGitLsFiles(extraArgs, cwd = process.cwd()) {
|
||||
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
|
||||
}
|
||||
|
||||
export function listStagedChangedPaths() {
|
||||
export function listStagedChangedPaths(cwd = process.cwd()) {
|
||||
const output = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMRD"], {
|
||||
cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf8",
|
||||
maxBuffer: GIT_OUTPUT_MAX_BUFFER,
|
||||
|
||||
@@ -82,7 +82,7 @@ export function shouldSkipAppLintForMissingSwiftlint(options = {}) {
|
||||
}
|
||||
|
||||
export function shouldDelegateChangedCheckToCrabbox(argv = [], env = process.env) {
|
||||
if (!isTruthyEnvFlag(env.OPENCLAW_TESTBOX)) {
|
||||
if (isTruthyEnvFlag(env.OPENCLAW_CHECK_CHANGED_REMOTE_CHILD)) {
|
||||
return false;
|
||||
}
|
||||
if (isTruthyEnvFlag(env.CI) || isTruthyEnvFlag(env.GITHUB_ACTIONS)) {
|
||||
@@ -94,7 +94,8 @@ export function shouldDelegateChangedCheckToCrabbox(argv = [], env = process.env
|
||||
return true;
|
||||
}
|
||||
|
||||
export function buildChangedCheckCrabboxArgs(argv = []) {
|
||||
export function buildChangedCheckCrabboxArgs(argv = [], options = {}) {
|
||||
const delegatedArgv = buildDelegatedChangedCheckArgv(argv, options);
|
||||
return [
|
||||
"crabbox:run",
|
||||
"--",
|
||||
@@ -114,13 +115,36 @@ export function buildChangedCheckCrabboxArgs(argv = []) {
|
||||
"240m",
|
||||
"--timing-json",
|
||||
"--",
|
||||
"env",
|
||||
"OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1",
|
||||
"OPENCLAW_CHANGED_LANES_RAW_SYNC=1",
|
||||
"CI=1",
|
||||
"corepack",
|
||||
"pnpm",
|
||||
"check:changed",
|
||||
...argv,
|
||||
...delegatedArgv,
|
||||
];
|
||||
}
|
||||
|
||||
function buildDelegatedChangedCheckArgv(argv, options = {}) {
|
||||
const args = parseArgs(argv);
|
||||
if (!args.staged || args.paths.length > 0) {
|
||||
return argv;
|
||||
}
|
||||
const stagedPaths = listStagedChangedPaths(options.cwd);
|
||||
const next = [];
|
||||
if (args.timed) {
|
||||
next.push("--timed");
|
||||
}
|
||||
if (stagedPaths.length === 0) {
|
||||
next.push("--no-changes");
|
||||
return next;
|
||||
}
|
||||
next.push("--base", "HEAD", "--head", "HEAD");
|
||||
next.push("--", ...stagedPaths);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function shouldRunShrinkwrapGuard(paths) {
|
||||
return paths.some((changedPath) => SHRINKWRAP_POLICY_PATH_RE.test(changedPath));
|
||||
}
|
||||
@@ -148,9 +172,7 @@ export function createShrinkwrapGuardCommand(paths) {
|
||||
}
|
||||
|
||||
export async function runChangedCheckViaCrabbox(argv = [], env = process.env) {
|
||||
console.error(
|
||||
"[check:changed] OPENCLAW_TESTBOX=1 set; delegating to Blacksmith Testbox via `pnpm crabbox:run`.",
|
||||
);
|
||||
console.error("[check:changed] delegating to Blacksmith Testbox via `pnpm crabbox:run`.");
|
||||
return await runManagedCommand({
|
||||
bin: "pnpm",
|
||||
args: buildChangedCheckCrabboxArgs(argv),
|
||||
@@ -477,6 +499,7 @@ function parseArgs(argv) {
|
||||
staged: false,
|
||||
dryRun: false,
|
||||
timed: false,
|
||||
noChanges: false,
|
||||
help: false,
|
||||
paths: [],
|
||||
};
|
||||
@@ -489,6 +512,7 @@ function parseArgs(argv) {
|
||||
booleanFlag("--staged", "staged"),
|
||||
booleanFlag("--dry-run", "dryRun"),
|
||||
booleanFlag("--timed", "timed"),
|
||||
booleanFlag("--no-changes", "noChanges"),
|
||||
booleanFlag("--help", "help"),
|
||||
booleanFlag("-h", "help"),
|
||||
],
|
||||
@@ -515,6 +539,7 @@ function printUsage() {
|
||||
" --staged Check staged paths instead of git diff paths",
|
||||
" --dry-run Print the planned checks without running them",
|
||||
" --timed Print timing summary",
|
||||
" --no-changes Treat the changed path set as empty",
|
||||
" -h, --help Show this help",
|
||||
"",
|
||||
].join("\n"),
|
||||
@@ -534,8 +559,9 @@ if (isDirectRun()) {
|
||||
} else if (shouldDelegateChangedCheckToCrabbox(argv, process.env)) {
|
||||
process.exitCode = await runChangedCheckViaCrabbox(argv, process.env);
|
||||
} else {
|
||||
const paths =
|
||||
args.paths.length > 0
|
||||
const paths = args.noChanges
|
||||
? []
|
||||
: args.paths.length > 0
|
||||
? args.paths
|
||||
: args.staged
|
||||
? listStagedChangedPaths()
|
||||
|
||||
@@ -140,6 +140,10 @@ const shellControlCommandPrefixes = new Set([
|
||||
]);
|
||||
const shellCommandExecutionPrefixes = new Set(["exec"]);
|
||||
const shellInlineCommandInterpreters = new Set(["bash", "dash", "ksh", "sh", "zsh"]);
|
||||
const remoteChangedGateEnv = [
|
||||
"OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1",
|
||||
"OPENCLAW_CHANGED_LANES_RAW_SYNC=1",
|
||||
];
|
||||
const shellInlineCommandOptionsWithNextValue = new Set([
|
||||
"+O",
|
||||
"+o",
|
||||
@@ -1437,6 +1441,82 @@ function remoteGitBootstrapForChangedGate(changedGateBase) {
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function injectRemoteChangedGateEnvironment(commandArgs) {
|
||||
if (commandArgs[0] !== "run" || isWindowsRemoteTarget(commandArgs)) {
|
||||
return commandArgs;
|
||||
}
|
||||
|
||||
const { start } = runCommandBounds(commandArgs);
|
||||
if (start < 0) {
|
||||
return commandArgs;
|
||||
}
|
||||
|
||||
const remoteCommand = commandArgs.slice(start);
|
||||
if (!isChangedGateCommand(remoteCommand)) {
|
||||
return commandArgs;
|
||||
}
|
||||
|
||||
const normalizedArgs = [...commandArgs];
|
||||
const markedRemoteCommand =
|
||||
hasOption(normalizedArgs, "--shell") && remoteCommand.length === 1
|
||||
? [markShellChangedGateAsRemoteChild(remoteCommand[0])]
|
||||
: markDirectChangedGateAsRemoteChild(remoteCommand);
|
||||
normalizedArgs.splice(start, normalizedArgs.length - start, ...markedRemoteCommand);
|
||||
return normalizedArgs;
|
||||
}
|
||||
|
||||
function markShellChangedGateAsRemoteChild(command) {
|
||||
const missingEnv = remoteChangedGateEnv.filter((assignment) => !command.includes(assignment));
|
||||
if (missingEnv.length === 0) {
|
||||
return command;
|
||||
}
|
||||
return `export ${missingEnv.join(" ")}; ${command}`;
|
||||
}
|
||||
|
||||
function markDirectChangedGateAsRemoteChild(commandArgs) {
|
||||
const missingEnv = remoteChangedGateEnv.filter((assignment) => !commandArgs.includes(assignment));
|
||||
if (missingEnv.length === 0) {
|
||||
return commandArgs;
|
||||
}
|
||||
|
||||
const markedCommandArgs = [...commandArgs];
|
||||
if (shellWordBasename(markedCommandArgs[0]) !== "env") {
|
||||
return ["env", ...missingEnv, ...markedCommandArgs];
|
||||
}
|
||||
|
||||
markedCommandArgs.splice(envAssignmentInsertIndex(markedCommandArgs), 0, ...missingEnv);
|
||||
return markedCommandArgs;
|
||||
}
|
||||
|
||||
function envAssignmentInsertIndex(words) {
|
||||
let index = 1;
|
||||
for (;;) {
|
||||
const word = words[index] ?? "";
|
||||
if (!word) {
|
||||
return 1;
|
||||
}
|
||||
if (word === "--") {
|
||||
return index + 1;
|
||||
}
|
||||
if (word === "-S" || word === "--split-string" || (word.startsWith("-S") && word !== "-S")) {
|
||||
return index;
|
||||
}
|
||||
if (word === "-u" || word === "--unset" || word === "-C" || word === "--chdir") {
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if (word.startsWith("--unset=") || word.startsWith("--chdir=")) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (word.startsWith("-") && word !== "-") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
function isWindowsRemoteTarget(commandArgs) {
|
||||
return (
|
||||
optionValue(commandArgs, "--target") === "windows" || hasOption(commandArgs, "--windows-mode")
|
||||
@@ -2024,11 +2104,12 @@ if (
|
||||
);
|
||||
}
|
||||
|
||||
const remoteMarkedArgs = injectRemoteChangedGateEnvironment(normalizedArgs);
|
||||
const childArgs =
|
||||
childCwd === repoRoot
|
||||
? injectRemoteAwsMacosJsBootstrap(normalizedArgs, provider)
|
||||
? injectRemoteAwsMacosJsBootstrap(remoteMarkedArgs, provider)
|
||||
: injectRemoteChangedGateGitBootstrap(
|
||||
injectRemoteAwsMacosJsBootstrap(absolutizeLocalRunPaths(normalizedArgs), provider),
|
||||
injectRemoteAwsMacosJsBootstrap(absolutizeLocalRunPaths(remoteMarkedArgs), provider),
|
||||
remoteChangedGateBase,
|
||||
);
|
||||
const childInvocation = spawnInvocation(binary, childArgs, childEnv, process.platform);
|
||||
|
||||
@@ -5,18 +5,17 @@ import {
|
||||
} from "./oauth-refresh-failure.js";
|
||||
|
||||
describe("oauth refresh failure hints", () => {
|
||||
it("canonicalizes retired OpenAI provider ids in refresh-failure login hints", () => {
|
||||
const legacyProvider = ["openai", "codex"].join("-");
|
||||
|
||||
it("builds OpenAI refresh-failure login hints", () => {
|
||||
expect(
|
||||
classifyOAuthRefreshFailure(
|
||||
`OAuth token refresh failed for ${legacyProvider}: invalid_grant`,
|
||||
),
|
||||
classifyOAuthRefreshFailure("OAuth token refresh failed for openai: invalid_grant"),
|
||||
).toEqual({
|
||||
provider: "openai",
|
||||
reason: "invalid_grant",
|
||||
});
|
||||
expect(buildOAuthRefreshFailureLoginCommand(legacyProvider)).toBe(
|
||||
expect(buildOAuthRefreshFailureLoginCommand("openai")).toBe(
|
||||
"openclaw models auth login --provider openai",
|
||||
);
|
||||
expect(buildOAuthRefreshFailureLoginCommand("OpenAI-Codex")).toBe(
|
||||
"openclaw models auth login --provider openai",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,8 +11,9 @@ 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 LEGACY_OPENAI_CODEX_PROVIDER_ID = ["openai", "codex"].join("-");
|
||||
const OPENAI_PROVIDER_ID = "openai";
|
||||
const RETIRED_REAUTH_PROVIDER_IDS: Readonly<Record<string, string>> = {
|
||||
"openai-codex": "openai",
|
||||
};
|
||||
|
||||
function isOAuthRefreshFailureMessage(message: string): boolean {
|
||||
const lower = message.toLowerCase();
|
||||
@@ -34,10 +35,6 @@ function sanitizeOAuthRefreshFailureProvider(provider: string | null | undefined
|
||||
return normalized && SAFE_PROVIDER_ID_RE.test(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
function canonicalizeOAuthRefreshFailureProvider(provider: string | null): string | null {
|
||||
return provider === LEGACY_OPENAI_CODEX_PROVIDER_ID ? OPENAI_PROVIDER_ID : provider;
|
||||
}
|
||||
|
||||
export function classifyOAuthRefreshFailureReason(
|
||||
message: string,
|
||||
): OAuthRefreshFailureReason | null {
|
||||
@@ -68,18 +65,17 @@ export function classifyOAuthRefreshFailure(message: string): {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: canonicalizeOAuthRefreshFailureProvider(
|
||||
sanitizeOAuthRefreshFailureProvider(extractOAuthRefreshFailureProvider(message)),
|
||||
),
|
||||
provider: sanitizeOAuthRefreshFailureProvider(extractOAuthRefreshFailureProvider(message)),
|
||||
reason: classifyOAuthRefreshFailureReason(message),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOAuthRefreshFailureLoginCommand(provider: string | null | undefined): string {
|
||||
const canonicalProvider = canonicalizeOAuthRefreshFailureProvider(
|
||||
sanitizeOAuthRefreshFailureProvider(provider),
|
||||
);
|
||||
return canonicalProvider
|
||||
? formatCliCommand(`openclaw models auth login --provider ${canonicalProvider}`)
|
||||
const sanitizedProvider = sanitizeOAuthRefreshFailureProvider(provider);
|
||||
const reauthProvider = sanitizedProvider
|
||||
? (RETIRED_REAUTH_PROVIDER_IDS[sanitizedProvider] ?? sanitizedProvider)
|
||||
: null;
|
||||
return reauthProvider
|
||||
? formatCliCommand(`openclaw models auth login --provider ${reauthProvider}`)
|
||||
: formatCliCommand("openclaw models auth login");
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ describe("promoteAuthProfileInOrder", () => {
|
||||
try {
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const legacyProvider = ["openai", "codex"].join("-");
|
||||
const legacyProvider = "retired-oauth-provider";
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
@@ -302,7 +302,7 @@ describe("promoteAuthProfileInOrder", () => {
|
||||
refresh: "old-refresh-token",
|
||||
oauthRef: {
|
||||
source: "openclaw-credentials",
|
||||
provider: ["openai", "codex"].join("-"),
|
||||
provider: "retired-oauth-provider",
|
||||
id: "legacy-profile",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
import { resolveModelRuntimePolicy } from "./model-runtime-policy.js";
|
||||
import { resolveProviderIdForAuth } from "./provider-auth-aliases.js";
|
||||
|
||||
const RUNTIME_COMPARISON_PROVIDER_ALIASES = new Map<string, string>([["openai", "openai"]]);
|
||||
|
||||
/** True for CLI runtime provider ids such as `claude-cli` and `google-gemini-cli`. */
|
||||
export function isCliRuntimeProvider(
|
||||
provider: string,
|
||||
@@ -57,14 +55,12 @@ function canonicalizeRuntimeAliasProvider(
|
||||
): string {
|
||||
const normalized = normalizeProviderId(provider);
|
||||
return (
|
||||
RUNTIME_COMPARISON_PROVIDER_ALIASES.get(normalized) ??
|
||||
listCliRuntimeModelBackendBindings({
|
||||
config: options.config,
|
||||
env: options.env,
|
||||
includeSetupRegistry:
|
||||
options.includeSetupRegistry ?? (options.config !== undefined || options.env !== undefined),
|
||||
}).find((binding) => binding.runtime === normalized)?.provider ??
|
||||
provider
|
||||
}).find((binding) => binding.runtime === normalized)?.provider ?? provider
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,10 +77,6 @@ describe("provider auth aliases", () => {
|
||||
expect(resolveProviderIdForAuth("openai")).toBe("openai");
|
||||
});
|
||||
|
||||
it("maps retired persisted OpenAI auth provider ids to canonical OpenAI", () => {
|
||||
expect(resolveProviderIdForAuth(["openai", "codex"].join("-"))).toBe("openai");
|
||||
});
|
||||
|
||||
it("does not reuse aliases across env-resolved plugin roots", () => {
|
||||
const env = {
|
||||
HOME: "/home/one",
|
||||
|
||||
@@ -25,10 +25,6 @@ type ProviderAuthAliasCandidate = {
|
||||
target: string;
|
||||
};
|
||||
|
||||
const RETIRED_PROVIDER_AUTH_ALIASES: Readonly<Record<string, string>> = {
|
||||
[["openai", "codex"].join("-")]: "openai",
|
||||
};
|
||||
|
||||
const PROVIDER_AUTH_ALIAS_ORIGIN_PRIORITY: Readonly<Record<PluginOrigin, number>> = {
|
||||
config: 0,
|
||||
bundled: 1,
|
||||
@@ -207,9 +203,5 @@ export function resolveProviderIdForAuth(
|
||||
if (!normalized) {
|
||||
return normalized;
|
||||
}
|
||||
return (
|
||||
resolveProviderAuthAliasMap(params)[normalized] ??
|
||||
RETIRED_PROVIDER_AUTH_ALIASES[normalized] ??
|
||||
normalized
|
||||
);
|
||||
return resolveProviderAuthAliasMap(params)[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
@@ -583,10 +583,9 @@ describe("repairSessionFileIfNeeded", () => {
|
||||
expect(JSON.parse(lines[4])).toEqual(deliveryMirror);
|
||||
});
|
||||
|
||||
it("repairs missing tool results in legacy OpenAI ChatGPT transcripts", async () => {
|
||||
it("repairs missing tool results in legacy OpenAI Codex transcripts", async () => {
|
||||
const { file } = await createTempSessionPath();
|
||||
const { header, message } = buildSessionHeaderAndMessage();
|
||||
const legacyProvider = ["openai", "codex"].join("-");
|
||||
const toolCallAssistant = {
|
||||
type: "message",
|
||||
id: "msg-asst-legacy-process",
|
||||
@@ -594,9 +593,9 @@ describe("repairSessionFileIfNeeded", () => {
|
||||
timestamp: new Date().toISOString(),
|
||||
message: {
|
||||
role: "assistant",
|
||||
provider: legacyProvider,
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.5",
|
||||
api: `${legacyProvider}-responses`,
|
||||
api: "openai-codex-responses",
|
||||
content: [{ type: "toolCall", id: "call_process|fc_1", name: "process", arguments: {} }],
|
||||
stopReason: "toolUse",
|
||||
},
|
||||
|
||||
@@ -206,8 +206,10 @@ function isCodeModeToolCallRepairCandidate(entry: unknown): entry is SessionMess
|
||||
provider?: unknown;
|
||||
stopReason?: unknown;
|
||||
};
|
||||
const legacyOpenAIProvider = ["openai", "codex"].join("-");
|
||||
const legacyOpenAIResponsesApi = `${legacyOpenAIProvider}-responses`;
|
||||
// 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;
|
||||
|
||||
@@ -59,22 +59,12 @@ describe("auth choice legacy aliases", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not keep old OpenAI Codex setup choices alive outside doctor", () => {
|
||||
const legacyChoice = ["openai", "codex"].join("-");
|
||||
expect(normalizeLegacyOnboardAuthChoice(legacyChoice, { env: authChoiceManifestEnv() })).toBe(
|
||||
legacyChoice,
|
||||
);
|
||||
it("does not keep retired Codex setup choices alive outside doctor", () => {
|
||||
expect(normalizeLegacyOnboardAuthChoice("codex-cli", { env: authChoiceManifestEnv() })).toBe(
|
||||
"codex-cli",
|
||||
);
|
||||
expect(
|
||||
resolveDeprecatedAuthChoiceReplacement(legacyChoice, { env: authChoiceManifestEnv() }),
|
||||
resolveDeprecatedAuthChoiceReplacement("codex-cli", { env: authChoiceManifestEnv() }),
|
||||
).toBeUndefined();
|
||||
expect(normalizeLegacyOnboardAuthChoice(`${legacyChoice}-device-code`)).toBe(
|
||||
`${legacyChoice}-device-code`,
|
||||
);
|
||||
expect(normalizeLegacyOnboardAuthChoice(`${legacyChoice}-api-key`)).toBe(
|
||||
`${legacyChoice}-api-key`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +63,70 @@ describe("compatibility binding repair migrate", () => {
|
||||
});
|
||||
|
||||
describe("legacy memory search config migrate", () => {
|
||||
it("moves legacy OpenAI Codex provider config to canonical OpenAI provider config", () => {
|
||||
const res = migrateLegacyConfigForTest({
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
api: "openai-codex-responses",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
api: "openai-codex-responses",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.config?.models?.providers?.openai).toEqual({
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
api: "openai-chatgpt-responses",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
api: "openai-chatgpt-responses",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(res.config?.models?.providers).not.toHaveProperty("openai-codex");
|
||||
expect(res.changes).toEqual([
|
||||
'Moved models.providers.openai-codex.api "openai-codex-responses" → "openai-chatgpt-responses".',
|
||||
'Moved models.providers.openai-codex.models[0].api "openai-codex-responses" → "openai-chatgpt-responses".',
|
||||
"Moved models.providers.openai-codex → models.providers.openai.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("records removal when canonical OpenAI provider already exists", () => {
|
||||
const res = migrateLegacyConfigForTest({
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
api: "openai-chatgpt-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
},
|
||||
"openai-codex": {
|
||||
api: "openai-chatgpt-responses",
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.config?.models?.providers?.openai).toEqual({
|
||||
api: "openai-chatgpt-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
});
|
||||
expect(res.config?.models?.providers).not.toHaveProperty("openai-codex");
|
||||
expect(res.changes).toEqual([
|
||||
"Removed models.providers.openai-codex because models.providers.openai already exists.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rewrites top-level legacy auto provider after moving memorySearch into agent defaults", () => {
|
||||
const raw = {
|
||||
memorySearch: {
|
||||
|
||||
@@ -920,6 +920,99 @@ function rewriteKnownModelRefs(
|
||||
|
||||
const RETIRED_MODEL_REF_MESSAGE =
|
||||
'Configured retired model refs are no longer in the bundled catalogs; run "openclaw doctor --fix" to upgrade them.';
|
||||
const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
||||
const LEGACY_OPENAI_CODEX_RESPONSES_API = "openai-codex-responses";
|
||||
const OPENAI_PROVIDER_ID = "openai";
|
||||
const OPENAI_CHATGPT_RESPONSES_API = "openai-chatgpt-responses";
|
||||
|
||||
function hasCanonicalOpenAIProvider(providers: Record<string, unknown>): boolean {
|
||||
return Object.keys(providers).some(
|
||||
(providerId) => normalizeProviderId(providerId) === OPENAI_PROVIDER_ID,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeLegacyOpenAIResponsesApi(
|
||||
providerId: string,
|
||||
provider: Record<string, unknown>,
|
||||
changes: string[],
|
||||
): { value: Record<string, unknown>; changed: boolean } {
|
||||
let changed = false;
|
||||
const next: Record<string, unknown> = { ...provider };
|
||||
if (next.api === LEGACY_OPENAI_CODEX_RESPONSES_API) {
|
||||
next.api = OPENAI_CHATGPT_RESPONSES_API;
|
||||
changes.push(
|
||||
`Moved models.providers.${providerId}.api "${LEGACY_OPENAI_CODEX_RESPONSES_API}" → "${OPENAI_CHATGPT_RESPONSES_API}".`,
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (Array.isArray(provider.models)) {
|
||||
let modelsChanged = false;
|
||||
const nextModels = provider.models.map((model, index) => {
|
||||
const modelRecord = getRecord(model);
|
||||
if (!modelRecord || modelRecord.api !== LEGACY_OPENAI_CODEX_RESPONSES_API) {
|
||||
return model;
|
||||
}
|
||||
modelsChanged = true;
|
||||
changes.push(
|
||||
`Moved models.providers.${providerId}.models[${index}].api "${LEGACY_OPENAI_CODEX_RESPONSES_API}" → "${OPENAI_CHATGPT_RESPONSES_API}".`,
|
||||
);
|
||||
return {
|
||||
...modelRecord,
|
||||
api: OPENAI_CHATGPT_RESPONSES_API,
|
||||
};
|
||||
});
|
||||
if (modelsChanged) {
|
||||
next.models = nextModels;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { value: next, changed };
|
||||
}
|
||||
|
||||
function migrateLegacyOpenAICodexProvider(raw: Record<string, unknown>, changes: string[]): void {
|
||||
const models = getRecord(raw.models);
|
||||
const providers = getRecord(models?.providers);
|
||||
if (!models || !providers) {
|
||||
return;
|
||||
}
|
||||
|
||||
let providersChanged = false;
|
||||
for (const [providerId, providerValue] of Object.entries({ ...providers })) {
|
||||
const provider = getRecord(providerValue);
|
||||
if (!provider) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalized = normalizeLegacyOpenAIResponsesApi(providerId, provider, changes);
|
||||
if (normalizeProviderId(providerId) !== LEGACY_OPENAI_CODEX_PROVIDER_ID) {
|
||||
if (normalized.changed) {
|
||||
providers[providerId] = normalized.value;
|
||||
providersChanged = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasCanonicalOpenAIProvider(providers)) {
|
||||
providers[OPENAI_PROVIDER_ID] = normalized.value;
|
||||
changes.push(
|
||||
`Moved models.providers.${LEGACY_OPENAI_CODEX_PROVIDER_ID} → models.providers.${OPENAI_PROVIDER_ID}.`,
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
`Removed models.providers.${LEGACY_OPENAI_CODEX_PROVIDER_ID} because models.providers.${OPENAI_PROVIDER_ID} already exists.`,
|
||||
);
|
||||
}
|
||||
delete providers[providerId];
|
||||
providersChanged = true;
|
||||
}
|
||||
|
||||
if (providersChanged) {
|
||||
models.providers = providers;
|
||||
}
|
||||
}
|
||||
|
||||
const RETIRED_MODEL_REF_RULES: LegacyConfigRule[] = [
|
||||
"agents",
|
||||
"plugins",
|
||||
@@ -935,6 +1028,46 @@ const RETIRED_MODEL_REF_RULES: LegacyConfigRule[] = [
|
||||
}));
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS: LegacyConfigMigrationSpec[] = [
|
||||
defineLegacyConfigMigration({
|
||||
id: "models.providers.openai-codex->models.providers.openai",
|
||||
describe: "Move legacy OpenAI Codex provider config to canonical OpenAI provider config",
|
||||
legacyRules: [
|
||||
{
|
||||
path: ["models", "providers"],
|
||||
message:
|
||||
'models.providers.openai-codex is legacy; run "openclaw doctor --fix" to move it to models.providers.openai.',
|
||||
match: (value) => {
|
||||
const providers = getRecord(value);
|
||||
return providers
|
||||
? Object.keys(providers).some(
|
||||
(providerId) => normalizeProviderId(providerId) === LEGACY_OPENAI_CODEX_PROVIDER_ID,
|
||||
)
|
||||
: false;
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ["models", "providers"],
|
||||
message:
|
||||
'openai-codex-responses is legacy; run "openclaw doctor --fix" to use openai-chatgpt-responses.',
|
||||
match: (value) => {
|
||||
const providers = getRecord(value);
|
||||
return providers
|
||||
? Object.values(providers).some((providerValue) => {
|
||||
const provider = getRecord(providerValue);
|
||||
return (
|
||||
provider?.api === LEGACY_OPENAI_CODEX_RESPONSES_API ||
|
||||
(Array.isArray(provider?.models) &&
|
||||
provider.models.some(
|
||||
(model) => getRecord(model)?.api === LEGACY_OPENAI_CODEX_RESPONSES_API,
|
||||
))
|
||||
);
|
||||
})
|
||||
: false;
|
||||
},
|
||||
},
|
||||
],
|
||||
apply: migrateLegacyOpenAICodexProvider,
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
id: "models.retired-model-refs",
|
||||
describe: "Upgrade retired model refs to current catalog entries",
|
||||
|
||||
@@ -88,7 +88,7 @@ describe("modelsAuthListCommand", () => {
|
||||
mocks.ensureAuthProfileStore.mockReturnValue(store);
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsAuthListCommand({ provider: "OpenAI-Codex", agent: "coder", json: true }, runtime);
|
||||
await modelsAuthListCommand({ provider: "OpenAI", agent: "coder", json: true }, runtime);
|
||||
|
||||
expect(mocks.externalCliDiscoveryForProviderAuth).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
@@ -116,20 +116,10 @@ describe("modelsAuthListCommand", () => {
|
||||
expect(JSON.stringify(runtime.jsonPayloads[0])).not.toContain("secret");
|
||||
});
|
||||
|
||||
it("treats the OpenAI filter as the friendly view over API-key and Codex subscription profiles", async () => {
|
||||
const legacyOpenAIProvider = ["openai", "codex"].join("-");
|
||||
const legacyProfileId = `${legacyOpenAIProvider}:legacy@example.com`;
|
||||
it("treats the OpenAI filter as the friendly view over API-key and OAuth profiles", async () => {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[legacyProfileId]: {
|
||||
type: "oauth",
|
||||
provider: legacyOpenAIProvider,
|
||||
access: "legacy-access-secret",
|
||||
refresh: "legacy-refresh-secret",
|
||||
expires: 1_800_000_000_000,
|
||||
email: "legacy@example.com",
|
||||
},
|
||||
"openai:user@example.com": {
|
||||
type: "oauth",
|
||||
provider: "openai",
|
||||
@@ -165,14 +155,6 @@ describe("modelsAuthListCommand", () => {
|
||||
agentId: "main",
|
||||
authStatePath: "/tmp/openclaw/agents/main/auth-state.json",
|
||||
profiles: [
|
||||
{
|
||||
email: "legacy@example.com",
|
||||
expiresAt: "2027-01-15T08:00:00.000Z",
|
||||
id: legacyProfileId,
|
||||
label: legacyProfileId,
|
||||
provider: "openai",
|
||||
type: "oauth",
|
||||
},
|
||||
{
|
||||
id: "openai:api-key-backup",
|
||||
label: "openai:api-key-backup",
|
||||
|
||||
@@ -771,21 +771,6 @@ describe("modelsAuthLoginCommand", () => {
|
||||
).toBe("/tmp/openclaw/agents/coder");
|
||||
});
|
||||
|
||||
it("normalizes legacy OpenAI auth provider requests to the OpenAI plugin", async () => {
|
||||
const runtime = createRuntime();
|
||||
const legacyProvider = ["openai", "codex"].join("-");
|
||||
|
||||
await modelsAuthLoginCommand({ provider: legacyProvider }, runtime);
|
||||
|
||||
const providerResolutionCall = readMockCallArg(
|
||||
mocks.resolvePluginProviders,
|
||||
) as ResolvePluginProvidersCall;
|
||||
expect(providerResolutionCall.providerRefs).toEqual(["openai"]);
|
||||
expect(runProviderAuth).toHaveBeenCalledOnce();
|
||||
const upsertCall = readMockCallArg(mocks.upsertAuthProfileWithLock) as UpsertAuthProfileCall;
|
||||
expect(upsertCall.credential?.provider).toBe("openai");
|
||||
});
|
||||
|
||||
it("loads the owning plugin for an explicit provider even in a clean config", async () => {
|
||||
const runtime = createRuntime();
|
||||
const runClaudeCliMigration = vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -65,7 +65,6 @@ import { isRemoteEnvironment } from "../oauth-env.js";
|
||||
import { loadValidConfigOrThrow, resolveKnownAgentId, updateConfig } from "./shared.js";
|
||||
|
||||
type UpsertAuthProfileParams = Parameters<typeof upsertAuthProfileWithLock>[0];
|
||||
const LEGACY_OPENAI_AUTH_PROVIDER_ID = ["openai", "codex"].join("-");
|
||||
|
||||
function resolveManualTokenExpiryMs(expiresIn: string | undefined): number | undefined {
|
||||
const normalizedExpiresIn = normalizeStringifiedOptionalString(expiresIn);
|
||||
@@ -152,9 +151,7 @@ function resolveDefaultTokenProfileId(provider: string): string {
|
||||
|
||||
function normalizeManualAuthProvider(provider: string): string {
|
||||
const normalized = normalizeProviderId(provider);
|
||||
return normalized === "openai" || normalized === LEGACY_OPENAI_AUTH_PROVIDER_ID
|
||||
? "openai"
|
||||
: normalized;
|
||||
return normalized === "openai" ? "openai" : normalized;
|
||||
}
|
||||
|
||||
function isOpenAIProvider(provider: string): boolean {
|
||||
|
||||
@@ -97,16 +97,14 @@ describe("formatOpenAIOAuthTlsPreflightFix", () => {
|
||||
});
|
||||
|
||||
describe("shouldRunOpenAIOAuthTlsPrerequisites", () => {
|
||||
it("runs for pre-doctor legacy OpenAI OAuth profiles", () => {
|
||||
const legacyOpenAIProvider = ["openai", "codex"].join("-");
|
||||
|
||||
it("runs for OpenAI OAuth profiles", () => {
|
||||
expect(
|
||||
shouldRunOpenAIOAuthTlsPrerequisites({
|
||||
cfg: {
|
||||
auth: {
|
||||
profiles: {
|
||||
[`${legacyOpenAIProvider}:default`]: {
|
||||
provider: legacyOpenAIProvider,
|
||||
"openai:default": {
|
||||
provider: "openai",
|
||||
mode: "oauth",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -50,10 +50,6 @@ import { OpenClawSchema } from "./zod-schema.js";
|
||||
|
||||
const LEGACY_REMOVED_PLUGIN_IDS = new Set(["google-antigravity-auth", "google-gemini-cli-auth"]);
|
||||
const BLOCKED_PLUGIN_CANDIDATE_PREFIX = "blocked plugin candidate:";
|
||||
const LEGACY_CHATGPT_PROVIDER_ID = ["openai", "codex"].join("-");
|
||||
const LEGACY_CHATGPT_RESPONSES_API = `${LEGACY_CHATGPT_PROVIDER_ID}-responses`;
|
||||
const OPENAI_PROVIDER_ID = "openai";
|
||||
const OPENAI_CHATGPT_RESPONSES_API = "openai-chatgpt-responses";
|
||||
|
||||
type UnknownIssueRecord = Record<string, unknown>;
|
||||
type ConfigPathSegment = string | number;
|
||||
@@ -70,79 +66,14 @@ type AllowedValuesCollection = {
|
||||
};
|
||||
type JsonSchemaLike = Record<string, unknown>;
|
||||
|
||||
function normalizeLegacyOpenAIProviderForValidation(raw: unknown): unknown {
|
||||
if (!isRecord(raw) || !isRecord(raw.models) || !isRecord(raw.models.providers)) {
|
||||
return raw;
|
||||
}
|
||||
let providersChanged = false;
|
||||
const providers = { ...raw.models.providers };
|
||||
const normalizeProviderConfig = (providerConfig: unknown): unknown => {
|
||||
if (!isRecord(providerConfig)) {
|
||||
return providerConfig;
|
||||
}
|
||||
let providerChanged = false;
|
||||
const nextProvider = { ...providerConfig };
|
||||
if (nextProvider.api === LEGACY_CHATGPT_RESPONSES_API) {
|
||||
nextProvider.api = OPENAI_CHATGPT_RESPONSES_API;
|
||||
providerChanged = true;
|
||||
}
|
||||
if (Array.isArray(nextProvider.models)) {
|
||||
const nextModels = nextProvider.models.map((model) => {
|
||||
if (!isRecord(model) || model.api !== LEGACY_CHATGPT_RESPONSES_API) {
|
||||
return model;
|
||||
}
|
||||
providerChanged = true;
|
||||
return { ...model, api: OPENAI_CHATGPT_RESPONSES_API };
|
||||
});
|
||||
if (providerChanged) {
|
||||
nextProvider.models = nextModels;
|
||||
}
|
||||
}
|
||||
return providerChanged ? nextProvider : providerConfig;
|
||||
};
|
||||
|
||||
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
||||
const normalizedProvider = normalizeLowercaseStringOrEmpty(providerId);
|
||||
const normalizedConfig = normalizeProviderConfig(providerConfig);
|
||||
if (normalizedProvider !== LEGACY_CHATGPT_PROVIDER_ID) {
|
||||
if (normalizedConfig !== providerConfig) {
|
||||
providers[providerId] = normalizedConfig;
|
||||
providersChanged = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!Object.hasOwn(providers, OPENAI_PROVIDER_ID)) {
|
||||
providers[OPENAI_PROVIDER_ID] = normalizedConfig;
|
||||
}
|
||||
delete providers[providerId];
|
||||
providersChanged = true;
|
||||
}
|
||||
|
||||
if (!providersChanged) {
|
||||
return raw;
|
||||
}
|
||||
return {
|
||||
...raw,
|
||||
models: {
|
||||
...raw.models,
|
||||
providers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function stripDeprecatedValidationKeys(raw: unknown): unknown {
|
||||
const normalizedRaw = normalizeLegacyOpenAIProviderForValidation(raw);
|
||||
if (
|
||||
!isRecord(normalizedRaw) ||
|
||||
!isRecord(normalizedRaw.commands) ||
|
||||
!Object.hasOwn(normalizedRaw.commands, "modelsWrite")
|
||||
) {
|
||||
return normalizedRaw;
|
||||
if (!isRecord(raw) || !isRecord(raw.commands) || !Object.hasOwn(raw.commands, "modelsWrite")) {
|
||||
return raw;
|
||||
}
|
||||
const commands = { ...normalizedRaw.commands };
|
||||
const commands = { ...raw.commands };
|
||||
delete commands.modelsWrite;
|
||||
return {
|
||||
...normalizedRaw,
|
||||
...raw,
|
||||
commands,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { validateConfigObjectRaw } from "./validation.js";
|
||||
import { ModelsConfigSchema } from "./zod-schema.core.js";
|
||||
|
||||
describe("ModelsConfigSchema", () => {
|
||||
@@ -23,36 +22,4 @@ describe("ModelsConfigSchema", () => {
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("canonicalizes legacy OpenAI ChatGPT response config before validation", () => {
|
||||
const legacyProvider = ["openai", "codex"].join("-");
|
||||
const legacyApi = `${legacyProvider}-responses`;
|
||||
const result = validateConfigObjectRaw({
|
||||
models: {
|
||||
providers: {
|
||||
[legacyProvider]: {
|
||||
baseUrl: "https://chatgpt.com/backend-api/codex",
|
||||
api: legacyApi,
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
api: legacyApi,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.config.models?.providers?.openai?.api).toBe("openai-chatgpt-responses");
|
||||
expect(result.config.models?.providers?.openai?.models?.[0]?.api).toBe(
|
||||
"openai-chatgpt-responses",
|
||||
);
|
||||
expect(result.config.models?.providers).not.toHaveProperty(legacyProvider);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ const TLS_CERT_ERROR_PATTERNS = [
|
||||
const OPENAI_AUTH_PROBE_URL =
|
||||
"https://auth.openai.com/oauth/authorize?response_type=code&client_id=openclaw-preflight&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email";
|
||||
const OPENAI_PROVIDER_ID = "openai";
|
||||
const LEGACY_OPENAI_PROVIDER_ID = ["openai", "codex"].join("-");
|
||||
|
||||
type PreflightFailureKind = "tls-cert" | "network";
|
||||
|
||||
@@ -85,9 +84,7 @@ function hasOpenAICodexOAuthProfile(cfg: OpenClawConfig): boolean {
|
||||
return false;
|
||||
}
|
||||
return Object.values(profiles).some(
|
||||
(profile) =>
|
||||
(profile.provider === OPENAI_PROVIDER_ID || profile.provider === LEGACY_OPENAI_PROVIDER_ID) &&
|
||||
profile.mode === "oauth",
|
||||
(profile) => profile.provider === OPENAI_PROVIDER_ID && profile.mode === "oauth",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
detectChangedLanes,
|
||||
isLiveDockerPackageScriptOnlyChange,
|
||||
isPackageScriptOnlyChange,
|
||||
listChangedPathsFromGit,
|
||||
} from "../../scripts/changed-lanes.mjs";
|
||||
import {
|
||||
buildChangedCheckCrabboxArgs,
|
||||
@@ -152,6 +153,101 @@ describe("scripts/changed-lanes", () => {
|
||||
expectLanes(result.lanes, { tooling: true });
|
||||
});
|
||||
|
||||
it("falls back to a two-dot diff when a delegated checkout has no merge base", () => {
|
||||
const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-no-merge-base-");
|
||||
git(dir, ["init", "-q", "--initial-branch=main"]);
|
||||
writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
|
||||
git(dir, ["add", "README.md"]);
|
||||
git(dir, [
|
||||
"-c",
|
||||
"user.email=test@example.com",
|
||||
"-c",
|
||||
"user.name=Test User",
|
||||
"commit",
|
||||
"-q",
|
||||
"-m",
|
||||
"initial",
|
||||
]);
|
||||
git(dir, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
|
||||
git(dir, ["switch", "-q", "--orphan", "feature"]);
|
||||
writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
|
||||
mkdirSync(path.join(dir, "src"), { recursive: true });
|
||||
writeFileSync(path.join(dir, "src", "committed.ts"), "export const committed = 1;\n", "utf8");
|
||||
git(dir, ["add", "README.md", "src/committed.ts"]);
|
||||
git(dir, [
|
||||
"-c",
|
||||
"user.email=test@example.com",
|
||||
"-c",
|
||||
"user.name=Test User",
|
||||
"commit",
|
||||
"-q",
|
||||
"-m",
|
||||
"feature base",
|
||||
]);
|
||||
writeFileSync(path.join(dir, "src", "feature.ts"), "export const value = 1;\n", "utf8");
|
||||
|
||||
expect(
|
||||
listChangedPathsFromGit({ base: "origin/main", cwd: dir, includeWorktree: false }),
|
||||
).toEqual(["src/committed.ts"]);
|
||||
expect(listChangedPathsFromGit({ base: "origin/main", cwd: dir })).toEqual([
|
||||
"src/committed.ts",
|
||||
"src/feature.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("prefers raw sync worktree paths over an implausibly broad no-merge-base diff", () => {
|
||||
const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-raw-sync-");
|
||||
git(dir, ["init", "-q", "--initial-branch=main"]);
|
||||
for (let index = 0; index < 250; index += 1) {
|
||||
writeFileSync(path.join(dir, `baseline-${index}.txt`), "baseline\n", "utf8");
|
||||
}
|
||||
git(dir, ["add", "."]);
|
||||
git(dir, [
|
||||
"-c",
|
||||
"user.email=test@example.com",
|
||||
"-c",
|
||||
"user.name=Test User",
|
||||
"commit",
|
||||
"-q",
|
||||
"-m",
|
||||
"initial",
|
||||
]);
|
||||
git(dir, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
|
||||
git(dir, ["switch", "-q", "--orphan", "feature"]);
|
||||
git(dir, [
|
||||
"-c",
|
||||
"user.email=test@example.com",
|
||||
"-c",
|
||||
"user.name=Test User",
|
||||
"commit",
|
||||
"-q",
|
||||
"--allow-empty",
|
||||
"-m",
|
||||
"raw sync base",
|
||||
]);
|
||||
mkdirSync(path.join(dir, "src"), { recursive: true });
|
||||
writeFileSync(path.join(dir, "src", "feature.ts"), "export const value = 1;\n", "utf8");
|
||||
|
||||
const normalPaths = listChangedPathsFromGit({ base: "origin/main", cwd: dir });
|
||||
expect(normalPaths.length).toBeGreaterThan(200);
|
||||
expect(normalPaths).toContain("baseline-0.txt");
|
||||
expect(normalPaths).toContain("src/feature.ts");
|
||||
|
||||
const previousRawSync = process.env.OPENCLAW_CHANGED_LANES_RAW_SYNC;
|
||||
process.env.OPENCLAW_CHANGED_LANES_RAW_SYNC = "1";
|
||||
try {
|
||||
expect(listChangedPathsFromGit({ base: "origin/main", cwd: dir })).toEqual([
|
||||
"src/feature.ts",
|
||||
]);
|
||||
} finally {
|
||||
if (previousRawSync === undefined) {
|
||||
delete process.env.OPENCLAW_CHANGED_LANES_RAW_SYNC;
|
||||
} else {
|
||||
process.env.OPENCLAW_CHANGED_LANES_RAW_SYNC = previousRawSync;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores local Crabbox metadata in the default local diff", () => {
|
||||
const dir = makeTempRepoRoot(tempDirs, "openclaw-changed-lanes-crabbox-");
|
||||
git(dir, ["init", "-q", "--initial-branch=main"]);
|
||||
@@ -415,10 +511,9 @@ describe("scripts/changed-lanes", () => {
|
||||
expect(command.args).toEqual(["check:no-conflict-markers"]);
|
||||
});
|
||||
|
||||
it("delegates local Testbox-mode changed gates before running locally", () => {
|
||||
it("delegates local changed gates to Crabbox before running locally", () => {
|
||||
expect(
|
||||
shouldDelegateChangedCheckToCrabbox(["--base", "origin/main"], {
|
||||
OPENCLAW_TESTBOX: "1",
|
||||
PATH: "/usr/bin",
|
||||
}),
|
||||
).toBe(true);
|
||||
@@ -442,6 +537,10 @@ describe("scripts/changed-lanes", () => {
|
||||
"240m",
|
||||
"--timing-json",
|
||||
"--",
|
||||
"env",
|
||||
"OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1",
|
||||
"OPENCLAW_CHANGED_LANES_RAW_SYNC=1",
|
||||
"CI=1",
|
||||
"corepack",
|
||||
"pnpm",
|
||||
"check:changed",
|
||||
@@ -452,14 +551,67 @@ describe("scripts/changed-lanes", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not delegate dry-run or CI changed gates", () => {
|
||||
expect(shouldDelegateChangedCheckToCrabbox(["--dry-run"], { OPENCLAW_TESTBOX: "1" })).toBe(
|
||||
false,
|
||||
);
|
||||
it("delegates staged changed gates as explicit remote paths", () => {
|
||||
const dir = makeTempRepoRoot(tempDirs, "openclaw-check-changed-staged-delegate-");
|
||||
git(dir, ["init", "-q", "--initial-branch=main"]);
|
||||
writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
|
||||
git(dir, ["add", "README.md"]);
|
||||
git(dir, [
|
||||
"-c",
|
||||
"user.email=test@example.com",
|
||||
"-c",
|
||||
"user.name=Test User",
|
||||
"commit",
|
||||
"-q",
|
||||
"-m",
|
||||
"initial",
|
||||
]);
|
||||
mkdirSync(path.join(dir, "src"), { recursive: true });
|
||||
writeFileSync(path.join(dir, "src", "staged.ts"), "export const staged = 1;\n", "utf8");
|
||||
git(dir, ["add", "src/staged.ts"]);
|
||||
|
||||
const args = buildChangedCheckCrabboxArgs(["--staged", "--timed"], { cwd: dir });
|
||||
expect(args.slice(args.indexOf("check:changed") + 1)).toEqual([
|
||||
"--timed",
|
||||
"--base",
|
||||
"HEAD",
|
||||
"--head",
|
||||
"HEAD",
|
||||
"--",
|
||||
"src/staged.ts",
|
||||
]);
|
||||
});
|
||||
|
||||
it("delegates empty staged changed gates without rediscovering unstaged paths", () => {
|
||||
const dir = makeTempRepoRoot(tempDirs, "openclaw-check-changed-empty-staged-delegate-");
|
||||
git(dir, ["init", "-q", "--initial-branch=main"]);
|
||||
writeFileSync(path.join(dir, "README.md"), "initial\n", "utf8");
|
||||
git(dir, ["add", "README.md"]);
|
||||
git(dir, [
|
||||
"-c",
|
||||
"user.email=test@example.com",
|
||||
"-c",
|
||||
"user.name=Test User",
|
||||
"commit",
|
||||
"-q",
|
||||
"-m",
|
||||
"initial",
|
||||
]);
|
||||
mkdirSync(path.join(dir, "src"), { recursive: true });
|
||||
writeFileSync(path.join(dir, "src", "unstaged.ts"), "export const unstaged = 1;\n", "utf8");
|
||||
|
||||
const args = buildChangedCheckCrabboxArgs(["--staged", "--timed"], { cwd: dir });
|
||||
|
||||
expect(args.slice(args.indexOf("check:changed") + 1)).toEqual(["--timed", "--no-changes"]);
|
||||
});
|
||||
|
||||
it("does not delegate dry-run, CI, or remote-child changed gates", () => {
|
||||
expect(shouldDelegateChangedCheckToCrabbox(["--dry-run"], {})).toBe(false);
|
||||
expect(shouldDelegateChangedCheckToCrabbox([], { GITHUB_ACTIONS: "true" })).toBe(false);
|
||||
expect(shouldDelegateChangedCheckToCrabbox([], { CI: "1" })).toBe(false);
|
||||
expect(
|
||||
shouldDelegateChangedCheckToCrabbox([], { OPENCLAW_TESTBOX: "1", GITHUB_ACTIONS: "true" }),
|
||||
shouldDelegateChangedCheckToCrabbox([], { OPENCLAW_CHECK_CHANGED_REMOTE_CHILD: "1" }),
|
||||
).toBe(false);
|
||||
expect(shouldDelegateChangedCheckToCrabbox([], { OPENCLAW_TESTBOX: "1", CI: "1" })).toBe(false);
|
||||
});
|
||||
|
||||
it("runs changed-check lint lanes under the parent heavy-check lock", () => {
|
||||
|
||||
@@ -311,6 +311,10 @@ function expectGroupedShellCommand(remoteCommand: string, command: string): void
|
||||
}
|
||||
}
|
||||
|
||||
const remoteChangedGateEnvPrefix =
|
||||
"OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1";
|
||||
const remoteChangedGateExport = `export ${remoteChangedGateEnvPrefix};`;
|
||||
|
||||
afterAll(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
@@ -925,7 +929,10 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(remoteCommand).toContain("node --version >&2");
|
||||
expect(remoteCommand).toContain('corepack enable --install-directory "$PNPM_HOME"');
|
||||
expect(remoteCommand).toContain("pnpm --version >&2");
|
||||
expectGroupedShellCommand(remoteCommand, "node scripts/check-changed.mjs");
|
||||
expectGroupedShellCommand(
|
||||
remoteCommand,
|
||||
`openclaw_crabbox_env ${remoteChangedGateEnvPrefix} node scripts/check-changed.mjs`,
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves shell commands when bootstrapping raw AWS macOS JavaScript commands", () => {
|
||||
@@ -939,7 +946,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(result.status).toBe(0);
|
||||
expect(output.args.filter((arg) => arg === "--shell")).toHaveLength(1);
|
||||
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
|
||||
expectGroupedShellCommand(remoteCommand, "pnpm check:changed");
|
||||
expectGroupedShellCommand(remoteCommand, `${remoteChangedGateExport} pnpm check:changed`);
|
||||
});
|
||||
|
||||
it("bootstraps raw AWS macOS shell scripts that set up before JavaScript commands", () => {
|
||||
@@ -1141,7 +1148,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
|
||||
expectGroupedShellCommand(remoteCommand, shellScript);
|
||||
expectGroupedShellCommand(remoteCommand, `${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("bootstraps raw AWS macOS shell scripts with command-prefixed JavaScript commands", () => {
|
||||
@@ -1462,7 +1469,10 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
|
||||
expectGroupedShellCommand(remoteCommand, "pnpm check:changed || true");
|
||||
expectGroupedShellCommand(
|
||||
remoteCommand,
|
||||
`${remoteChangedGateExport} pnpm check:changed || true`,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not bootstrap non-macOS AWS JavaScript commands", () => {
|
||||
@@ -1747,7 +1757,46 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(remoteCommand).toContain("git add -A");
|
||||
expect(remoteCommand).toContain("git diff --cached --quiet");
|
||||
expect(remoteCommand).toContain("commit -q --no-gpg-sign -m remote-changed-gate-tree");
|
||||
expect(remoteCommand).toMatch(/&& corepack pnpm check:changed$/u);
|
||||
expect(remoteCommand).toMatch(
|
||||
/&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 corepack pnpm check:changed$/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("bootstraps Git metadata for env-prefixed sparse changed gates", () => {
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
[
|
||||
"run",
|
||||
"--provider",
|
||||
"aws",
|
||||
"--",
|
||||
"env",
|
||||
"OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1",
|
||||
"OPENCLAW_CHANGED_LANES_RAW_SYNC=1",
|
||||
"CI=1",
|
||||
"corepack",
|
||||
"pnpm",
|
||||
"check:changed",
|
||||
],
|
||||
{
|
||||
gitResponses: {
|
||||
[GIT_CONFIG_SPARSE_KEY]: { stdout: "true\n" },
|
||||
[GIT_STATUS_PORCELAIN_KEY]: { stdout: "" },
|
||||
[GIT_MERGE_BASE_MAIN_HEAD_KEY]: { stdout: "abc123\n" },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const output = parseFakeCrabboxOutput(result);
|
||||
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(output.args).toContain("--shell");
|
||||
expect(remoteCommand).toContain(
|
||||
"git fetch -q --depth=1 origin abc123:refs/remotes/origin/main",
|
||||
);
|
||||
expect(remoteCommand).toMatch(
|
||||
/&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 CI=1 corepack pnpm check:changed$/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves macOS JS bootstrapping for sparse changed gates on remote raw syncs", () => {
|
||||
@@ -1771,7 +1820,10 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
"git fetch -q --depth=1 origin abc123:refs/remotes/origin/main",
|
||||
);
|
||||
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
|
||||
expectGroupedShellCommand(remoteCommand, "pnpm check:changed");
|
||||
expectGroupedShellCommand(
|
||||
remoteCommand,
|
||||
`openclaw_crabbox_env ${remoteChangedGateEnvPrefix} pnpm check:changed`,
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves macOS JS and Git bootstraps for sparse shell changed gates with setup", () => {
|
||||
@@ -1794,7 +1846,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(output.args.filter((arg) => arg === "--shell")).toHaveLength(1);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
|
||||
expectGroupedShellCommand(remoteCommand, shellScript);
|
||||
expectGroupedShellCommand(remoteCommand, `${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves macOS JS and Git bootstraps for shell-wrapped sparse changed gates", () => {
|
||||
@@ -1816,7 +1868,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
|
||||
expectGroupedShellCommand(remoteCommand, shellScript);
|
||||
expectGroupedShellCommand(remoteCommand, `${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for assignment-prefix command substitutions", () => {
|
||||
@@ -1837,7 +1889,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for command-prefixed shell commands", () => {
|
||||
@@ -1858,7 +1910,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for bash -lc shell commands", () => {
|
||||
@@ -1883,7 +1935,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(remoteCommand).toContain(
|
||||
"git fetch -q --depth=1 origin abc123:refs/remotes/origin/main",
|
||||
);
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for shell option values before -c", () => {
|
||||
@@ -1904,7 +1956,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for attached shell option values before -c", () => {
|
||||
@@ -1925,7 +1977,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for grouped shell options before -c", () => {
|
||||
@@ -1946,7 +1998,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for absolute time-prefixed shell commands", () => {
|
||||
@@ -1967,7 +2019,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
const remoteCommand = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for timeout-wrapped shell commands", () => {
|
||||
@@ -1992,7 +2044,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(remoteCommand).toContain(
|
||||
"git fetch -q --depth=1 origin abc123:refs/remotes/origin/main",
|
||||
);
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
expect(remoteCommand).toContain(`&& ${remoteChangedGateExport} ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for direct timeout-wrapped node commands", () => {
|
||||
@@ -2027,7 +2079,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(output.args).toContain("--shell");
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toMatch(
|
||||
/&& 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 timeout 1200s node scripts\/check-changed\.mjs --base origin\/main --head HEAD$/u,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2049,7 +2101,9 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(result.status).toBe(0);
|
||||
expect(output.args).toContain("--shell");
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toMatch(/&& timeout 1200s bash -lc 'pnpm check:changed'$/u);
|
||||
expect(remoteCommand).toMatch(
|
||||
/&& env OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1 timeout 1200s bash -lc 'pnpm check:changed'$/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not treat quoted sparse shell text as a changed gate", () => {
|
||||
@@ -2191,7 +2245,9 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
expect(remoteCommand).toContain(
|
||||
"git fetch -q --depth=1 origin abc123:refs/remotes/origin/main",
|
||||
);
|
||||
expect(remoteCommand).toMatch(/&& env CI=1 pnpm check:changed$/u);
|
||||
expect(remoteCommand).toMatch(
|
||||
/&& export OPENCLAW_CHECK_CHANGED_REMOTE_CHILD=1 OPENCLAW_CHANGED_LANES_RAW_SYNC=1; env CI=1 pnpm check:changed$/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not inject the POSIX changed-gate bootstrap for Windows targets", () => {
|
||||
|
||||
Reference in New Issue
Block a user