mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
refactor(cli): consolidate security-sensitive regression fixtures (#114411)
* refactor(cli): consolidate security-sensitive regression fixtures * refactor(cli): refresh fixture cleanup against main
This commit is contained in:
committed by
GitHub
parent
76ec39556e
commit
6b625357fc
+132
-289
File diff suppressed because it is too large
Load Diff
+212
-407
@@ -407,11 +407,11 @@ function makeProxyHandle() {
|
||||
};
|
||||
}
|
||||
|
||||
async function withInteractiveTty(fn: () => Promise<void>): Promise<void> {
|
||||
async function withCliTty(value: boolean, fn: () => Promise<void>): Promise<void> {
|
||||
const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true });
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
|
||||
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value });
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value });
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
@@ -428,6 +428,36 @@ async function withInteractiveTty(fn: () => Promise<void>): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function withInteractiveTty(fn: () => Promise<void>): Promise<void> {
|
||||
return withCliTty(true, fn);
|
||||
}
|
||||
|
||||
function runBareCli(): Promise<void> {
|
||||
return withInteractiveTty(() => runCli(["node", "openclaw"]));
|
||||
}
|
||||
|
||||
function primeBareRootConfig(sourceConfig: ConfigSnapshotStub["sourceConfig"]): void {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({ exists: true, valid: true, sourceConfig });
|
||||
}
|
||||
|
||||
async function expectNonInteractiveBareCliError(
|
||||
message: string,
|
||||
assert?: () => void,
|
||||
): Promise<void> {
|
||||
const previousExitCode = process.exitCode;
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
process.exitCode = undefined;
|
||||
try {
|
||||
await withCliTty(false, () => runCli(["node", "openclaw"]));
|
||||
expect(process.exitCode).toBe(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith(message);
|
||||
assert?.();
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
process.exitCode = previousExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
describe("runCli exit behavior", () => {
|
||||
afterAll(() => {
|
||||
serviceEnvSnapshot.restore();
|
||||
@@ -2970,7 +3000,7 @@ describe("runCli exit behavior", () => {
|
||||
},
|
||||
])("$name", async ({ snapshot }) => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce(snapshot);
|
||||
await withInteractiveTty(async () => runCli(["node", "openclaw"]));
|
||||
await runBareCli();
|
||||
|
||||
expect(readConfigFileSnapshotMock).toHaveBeenCalledOnce();
|
||||
expect(setupWizardCommandMock).toHaveBeenCalledWith({});
|
||||
@@ -2979,43 +3009,20 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("points noninteractive fresh bare root invocations to onboarding automation", async () => {
|
||||
const previousExitCode = process.exitCode;
|
||||
const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
process.exitCode = undefined;
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: false,
|
||||
valid: true,
|
||||
sourceConfig: {},
|
||||
});
|
||||
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: false });
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: false });
|
||||
|
||||
try {
|
||||
await runCli(["node", "openclaw"]);
|
||||
|
||||
expect(process.exitCode).toBe(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Onboarding needs an interactive TTY. Use `openclaw onboard --non-interactive --accept-risk ...` for automation.",
|
||||
);
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
process.exitCode = previousExitCode;
|
||||
if (stdinDescriptor) {
|
||||
Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdin, "isTTY");
|
||||
}
|
||||
if (stdoutDescriptor) {
|
||||
Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdout, "isTTY");
|
||||
}
|
||||
}
|
||||
await expectNonInteractiveBareCliError(
|
||||
"Onboarding needs an interactive TTY. Use `openclaw onboard --non-interactive --accept-risk ...` for automation.",
|
||||
() => {
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("starts the gateway-backed TUI for bare root invocations when config already exists", async () => {
|
||||
@@ -3038,9 +3045,7 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
await withEnvAsync({ OPENCLAW_GATEWAY_PASSWORD: "gateway-ref-password" }, async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
});
|
||||
|
||||
expect(readConfigFileSnapshotMock).toHaveBeenCalledTimes(1);
|
||||
@@ -3078,9 +3083,7 @@ describe("runCli exit behavior", () => {
|
||||
detail: "Gateway default agent has no configured model",
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(runRemoteGatewayInferenceOnboardingMock).toHaveBeenCalledWith({
|
||||
@@ -3093,21 +3096,15 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("keeps missing inference setup local for a local Gateway", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: { mode: "local" },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: { mode: "local" },
|
||||
});
|
||||
probeGatewayConfiguredModelMock.mockResolvedValueOnce({
|
||||
kind: "missing-configured-model",
|
||||
detail: "Gateway default agent has no configured model",
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(setupWizardCommandMock).toHaveBeenCalledWith({});
|
||||
expect(runRemoteGatewayInferenceOnboardingMock).not.toHaveBeenCalled();
|
||||
@@ -3115,69 +3112,36 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("does not direct non-interactive remote setup into local onboarding", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: { url: "wss://gateway.example/ws", token: "remote-token" },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: { url: "wss://gateway.example/ws", token: "remote-token" },
|
||||
},
|
||||
});
|
||||
probeGatewayConfiguredModelMock.mockResolvedValueOnce({
|
||||
kind: "missing-configured-model",
|
||||
detail: "Gateway default agent has no configured model",
|
||||
});
|
||||
const previousExitCode = process.exitCode;
|
||||
const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: false });
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: false });
|
||||
|
||||
try {
|
||||
await runCli(["node", "openclaw"]);
|
||||
|
||||
expect(process.exitCode).toBe(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Remote Gateway inference setup needs an interactive TTY. Re-run `openclaw` in a terminal connected to this Gateway.",
|
||||
);
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(runRemoteGatewayInferenceOnboardingMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
process.exitCode = previousExitCode;
|
||||
if (stdinDescriptor) {
|
||||
Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdin, "isTTY");
|
||||
}
|
||||
if (stdoutDescriptor) {
|
||||
Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdout, "isTTY");
|
||||
}
|
||||
}
|
||||
await expectNonInteractiveBareCliError(
|
||||
"Remote Gateway inference setup needs an interactive TTY. Re-run `openclaw` in a terminal connected to this Gateway.",
|
||||
() => {
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(runRemoteGatewayInferenceOnboardingMock).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the active local gateway lock port for bare root preflight and TUI handoff", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
port: 18789,
|
||||
auth: { mode: "token", token: "configured-token" },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
port: 18789,
|
||||
auth: { mode: "token", token: "configured-token" },
|
||||
},
|
||||
});
|
||||
readActiveGatewayLockPortMock.mockResolvedValueOnce(48789);
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenCalledWith({
|
||||
url: "ws://127.0.0.1:48789",
|
||||
@@ -3190,23 +3154,17 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("keeps an explicit gateway port ahead of active local lock metadata", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
port: 18789,
|
||||
auth: { mode: "token", token: "configured-token" },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
port: 18789,
|
||||
auth: { mode: "token", token: "configured-token" },
|
||||
},
|
||||
});
|
||||
readActiveGatewayLockPortMock.mockResolvedValueOnce(48789);
|
||||
|
||||
await withEnvAsync({ OPENCLAW_GATEWAY_PORT: "19001" }, async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
});
|
||||
|
||||
expect(readActiveGatewayLockPortMock).not.toHaveBeenCalled();
|
||||
@@ -3217,15 +3175,11 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("carries the canonical local TLS fingerprint through bare root", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
tls: { enabled: true },
|
||||
auth: { mode: "token", token: "configured-token" },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
tls: { enabled: true },
|
||||
auth: { mode: "token", token: "configured-token" },
|
||||
},
|
||||
});
|
||||
loadGatewayTlsRuntimeMock.mockResolvedValueOnce({
|
||||
@@ -3234,9 +3188,7 @@ describe("runCli exit behavior", () => {
|
||||
fingerprintSha256: "sha256:local-self-signed-fingerprint",
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenCalledWith({
|
||||
url: "wss://127.0.0.1:18789",
|
||||
@@ -3250,21 +3202,15 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("uses gateway env credentials for bare root gateway preflight", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: { mode: "token" },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: { mode: "token" },
|
||||
},
|
||||
});
|
||||
|
||||
await withEnvAsync({ OPENCLAW_GATEWAY_TOKEN: "env-token" }, async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
});
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenCalledWith({
|
||||
@@ -3292,45 +3238,39 @@ describe("runCli exit behavior", () => {
|
||||
"process.stdout.write(JSON.stringify({ protocolVersion: 1, values: { PASSWORD_SECRET: 'password-from-exec' } }));", // pragma: allowlist secret
|
||||
].join("");
|
||||
await withSecureTestNodeExecPath(async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
secrets: {
|
||||
providers: {
|
||||
tokenprovider: {
|
||||
source: "exec",
|
||||
command: process.execPath,
|
||||
args: ["-e", tokenProgram],
|
||||
allowInsecurePath: true,
|
||||
},
|
||||
passwordprovider: {
|
||||
source: "exec",
|
||||
command: process.execPath,
|
||||
args: ["-e", passwordProgram],
|
||||
allowInsecurePath: true,
|
||||
},
|
||||
primeBareRootConfig({
|
||||
secrets: {
|
||||
providers: {
|
||||
tokenprovider: {
|
||||
source: "exec",
|
||||
command: process.execPath,
|
||||
args: ["-e", tokenProgram],
|
||||
allowInsecurePath: true,
|
||||
},
|
||||
passwordprovider: {
|
||||
source: "exec",
|
||||
command: process.execPath,
|
||||
args: ["-e", passwordProgram],
|
||||
allowInsecurePath: true,
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "password",
|
||||
token: { source: "exec", provider: "tokenprovider", id: "TOKEN_SECRET" },
|
||||
password: {
|
||||
source: "exec",
|
||||
provider: "passwordprovider",
|
||||
id: "PASSWORD_SECRET",
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
mode: "local",
|
||||
auth: {
|
||||
mode: "password",
|
||||
token: { source: "exec", provider: "tokenprovider", id: "TOKEN_SECRET" },
|
||||
password: {
|
||||
source: "exec",
|
||||
provider: "passwordprovider",
|
||||
id: "PASSWORD_SECRET",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenCalledWith({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
@@ -3349,24 +3289,18 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("probes local gateways over loopback even when the gateway advertises a LAN bind", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "lan",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "local-token",
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "lan",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "local-token",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(readActiveGatewayLockPortMock).toHaveBeenCalledTimes(1);
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenCalledWith({
|
||||
@@ -3380,17 +3314,13 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("falls back to the configured local tailnet gateway URL when loopback is unavailable", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "tailnet",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "local-token",
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "tailnet",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "local-token",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -3409,9 +3339,7 @@ describe("runCli exit behavior", () => {
|
||||
.mockResolvedValueOnce({ kind: "unreachable", detail: "loopback offline" })
|
||||
.mockResolvedValueOnce({ kind: "configured" });
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenNthCalledWith(1, {
|
||||
url: "ws://127.0.0.1:18789",
|
||||
@@ -3428,15 +3356,11 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("prefers a configured secondary Gateway over a missing-model primary probe", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "tailnet",
|
||||
auth: { mode: "token", token: "local-token" },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "tailnet",
|
||||
auth: { mode: "token", token: "local-token" },
|
||||
},
|
||||
});
|
||||
resolveControlUiLinksMock.mockImplementation(({ bind }: { bind?: string } = {}) =>
|
||||
@@ -3451,9 +3375,7 @@ describe("runCli exit behavior", () => {
|
||||
})
|
||||
.mockResolvedValueOnce({ kind: "configured" });
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(launchTuiCliMock).toHaveBeenCalledWith(
|
||||
@@ -3463,16 +3385,12 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("keeps confirmed missing inference ahead of an unverified secondary Gateway", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
agents: { defaults: { model: { primary: "openai/local-only-model" } } },
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "tailnet",
|
||||
auth: { mode: "token", token: "local-token" },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
agents: { defaults: { model: { primary: "openai/local-only-model" } } },
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "tailnet",
|
||||
auth: { mode: "token", token: "local-token" },
|
||||
},
|
||||
});
|
||||
resolveControlUiLinksMock.mockImplementation(({ bind }: { bind?: string } = {}) =>
|
||||
@@ -3490,9 +3408,7 @@ describe("runCli exit behavior", () => {
|
||||
detail: "Gateway default agent has no configured model",
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(setupWizardCommandMock).toHaveBeenCalledWith({});
|
||||
expect(launchTuiCliMock).not.toHaveBeenCalled();
|
||||
@@ -3500,22 +3416,16 @@ describe("runCli exit behavior", () => {
|
||||
|
||||
it("keeps a reachable unverified Gateway ahead of local inference fallback", async () => {
|
||||
const url = "ws://127.0.0.1:18789";
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
agents: { defaults: { model: { primary: "openai/local-only-model" } } },
|
||||
gateway: { mode: "remote", remote: { url, token: "remote-token" } },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
agents: { defaults: { model: { primary: "openai/local-only-model" } } },
|
||||
gateway: { mode: "remote", remote: { url, token: "remote-token" } },
|
||||
});
|
||||
probeGatewayConfiguredModelMock.mockResolvedValueOnce({
|
||||
kind: "reachable-unverified",
|
||||
detail: "config.get: unauthorized",
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(launchTuiCliMock).toHaveBeenCalledWith(
|
||||
@@ -3526,21 +3436,15 @@ describe("runCli exit behavior", () => {
|
||||
|
||||
it("keeps a configured remote Gateway authoritative across a transient cold-restart probe", async () => {
|
||||
const url = "wss://gateway.example/ws";
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: { mode: "remote", remote: { url, token: "remote-token" } },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: { mode: "remote", remote: { url, token: "remote-token" } },
|
||||
});
|
||||
probeGatewayConfiguredModelMock.mockResolvedValueOnce({
|
||||
kind: "unreachable",
|
||||
detail: "gateway restarting",
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(runRemoteGatewayInferenceOnboardingMock).not.toHaveBeenCalled();
|
||||
@@ -3551,21 +3455,15 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("keeps a configured local Gateway authoritative across a transient cold-restart probe", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: { mode: "local", auth: { mode: "token", token: "local-token" } },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: { mode: "local", auth: { mode: "token", token: "local-token" } },
|
||||
});
|
||||
probeGatewayConfiguredModelMock.mockResolvedValueOnce({
|
||||
kind: "unreachable",
|
||||
detail: "offline",
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
expect(launchTuiCliMock).toHaveBeenCalledWith(
|
||||
@@ -3575,21 +3473,15 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("starts the local TUI when no Gateway is configured and the default probe is unavailable", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
|
||||
},
|
||||
primeBareRootConfig({
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
|
||||
});
|
||||
probeGatewayConfiguredModelMock.mockResolvedValueOnce({
|
||||
kind: "unreachable",
|
||||
detail: "offline",
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(launchTuiCliMock).toHaveBeenCalledWith({ deliver: false, local: true }, {});
|
||||
});
|
||||
@@ -3599,24 +3491,18 @@ describe("runCli exit behavior", () => {
|
||||
{ label: "mDNS", url: "ws://gateway.local:18789" },
|
||||
{ label: "Tailnet DNS", url: "ws://machine.tail123.ts.net:18789" },
|
||||
])("does not probe a plaintext remote gateway over $label without opt-in", async ({ url }) => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
token: "remote-token",
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
token: "remote-token",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await withEnvAsync({ OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: undefined }, async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
});
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).not.toHaveBeenCalled();
|
||||
@@ -3626,24 +3512,18 @@ describe("runCli exit behavior", () => {
|
||||
|
||||
it("probes a plaintext remote loopback gateway", async () => {
|
||||
const url = "ws://127.0.0.1:18789";
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
token: "remote-token",
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
token: "remote-token",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await withEnvAsync({ OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: undefined }, async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
});
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenCalledWith({
|
||||
@@ -3659,24 +3539,18 @@ describe("runCli exit behavior", () => {
|
||||
|
||||
it("keeps configured remote password authoritative from preflight through TUI launch", async () => {
|
||||
const url = "ws://127.0.0.1:18789";
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
password: "configured-remote-password", // pragma: allowlist secret
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
password: "configured-remote-password", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await withEnvAsync({ OPENCLAW_GATEWAY_PASSWORD: "stale-env-password" }, async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
});
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenCalledWith({
|
||||
@@ -3691,24 +3565,20 @@ describe("runCli exit behavior", () => {
|
||||
|
||||
it("falls back to gateway env auth when configured remote SecretRefs are unresolved", async () => {
|
||||
const url = "ws://127.0.0.1:18789";
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_REMOTE_GATEWAY_TOKEN",
|
||||
},
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_REMOTE_GATEWAY_PASSWORD",
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
token: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_REMOTE_GATEWAY_TOKEN",
|
||||
},
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_REMOTE_GATEWAY_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -3722,9 +3592,7 @@ describe("runCli exit behavior", () => {
|
||||
OPENCLAW_GATEWAY_PASSWORD: "env-remote-password",
|
||||
},
|
||||
async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3739,24 +3607,18 @@ describe("runCli exit behavior", () => {
|
||||
|
||||
it("probes an explicitly allowed plaintext private remote gateway", async () => {
|
||||
const url = "ws://192.168.1.10:18789";
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
token: "remote-token",
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url,
|
||||
token: "remote-token",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await withEnvAsync({ OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: "1" }, async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
});
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenCalledWith({
|
||||
@@ -3771,24 +3633,18 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("forwards the configured TLS pin when probing a remote gateway", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url: "wss://gateway.example.com:18789",
|
||||
token: "remote-token",
|
||||
tlsFingerprint: "sha256:11:22:33:44",
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url: "wss://gateway.example.com:18789",
|
||||
token: "remote-token",
|
||||
tlsFingerprint: "sha256:11:22:33:44",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).toHaveBeenCalledWith({
|
||||
url: "wss://gateway.example.com:18789",
|
||||
@@ -3802,24 +3658,18 @@ describe("runCli exit behavior", () => {
|
||||
});
|
||||
|
||||
it("routes to inference onboarding without probing a public plaintext remote gateway", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: true,
|
||||
sourceConfig: {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url: "ws://gateway.example.com:18789",
|
||||
token: "remote-token",
|
||||
},
|
||||
primeBareRootConfig({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url: "ws://gateway.example.com:18789",
|
||||
token: "remote-token",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await withEnvAsync({ OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: undefined }, async () => {
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
});
|
||||
|
||||
expect(probeGatewayConfiguredModelMock).not.toHaveBeenCalled();
|
||||
@@ -3867,73 +3717,28 @@ describe("runCli exit behavior", () => {
|
||||
sourceConfig: { gateway: { mode: "local" } },
|
||||
});
|
||||
|
||||
await withInteractiveTty(async () => {
|
||||
await runCli(["node", "openclaw"]);
|
||||
});
|
||||
await runBareCli();
|
||||
|
||||
expect(setupWizardCommandMock).toHaveBeenCalledWith({ classic: true });
|
||||
expect(launchTuiCliMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("points noninteractive invalid config to doctor before onboarding", async () => {
|
||||
const previousExitCode = process.exitCode;
|
||||
const stdinDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
process.exitCode = undefined;
|
||||
readConfigFileSnapshotMock.mockResolvedValueOnce({
|
||||
exists: true,
|
||||
valid: false,
|
||||
sourceConfig: { gateway: { mode: "local" } },
|
||||
});
|
||||
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: false });
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: false });
|
||||
|
||||
try {
|
||||
await runCli(["node", "openclaw"]);
|
||||
|
||||
expect(process.exitCode).toBe(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"OpenClaw config is invalid. Run `openclaw doctor --fix` before onboarding.",
|
||||
);
|
||||
expect(setupWizardCommandMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
errorSpy.mockRestore();
|
||||
process.exitCode = previousExitCode;
|
||||
if (stdinDescriptor) {
|
||||
Object.defineProperty(process.stdin, "isTTY", stdinDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdin, "isTTY");
|
||||
}
|
||||
if (stdoutDescriptor) {
|
||||
Object.defineProperty(process.stdout, "isTTY", stdoutDescriptor);
|
||||
} else {
|
||||
Reflect.deleteProperty(process.stdout, "isTTY");
|
||||
}
|
||||
}
|
||||
await expectNonInteractiveBareCliError(
|
||||
"OpenClaw config is invalid. Run `openclaw doctor --fix` before onboarding.",
|
||||
() => expect(setupWizardCommandMock).not.toHaveBeenCalled(),
|
||||
);
|
||||
});
|
||||
|
||||
it("bootstraps env proxy before bare TUI startup", async () => {
|
||||
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(true);
|
||||
const stdinTty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
||||
const stdoutTty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true });
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
|
||||
|
||||
try {
|
||||
await runCli(["node", "openclaw"]);
|
||||
} finally {
|
||||
if (stdinTty) {
|
||||
Object.defineProperty(process.stdin, "isTTY", stdinTty);
|
||||
} else {
|
||||
delete (process.stdin as { isTTY?: boolean }).isTTY;
|
||||
}
|
||||
if (stdoutTty) {
|
||||
Object.defineProperty(process.stdout, "isTTY", stdoutTty);
|
||||
} else {
|
||||
delete (process.stdout as { isTTY?: boolean }).isTTY;
|
||||
}
|
||||
}
|
||||
await runBareCli();
|
||||
|
||||
expect(ensureGlobalUndiciEnvProxyDispatcherMock).toHaveBeenCalledTimes(1);
|
||||
expect(launchTuiCliMock).toHaveBeenCalledOnce();
|
||||
|
||||
+61
-129
@@ -592,6 +592,20 @@ describe("update-cli", () => {
|
||||
});
|
||||
};
|
||||
|
||||
const primeNpmChannelTag = (tag: string, version: string | null): void => {
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({ tag, version });
|
||||
};
|
||||
|
||||
const primeServiceCommand = (
|
||||
programArguments: Array<string | undefined>,
|
||||
environment?: NodeJS.ProcessEnv,
|
||||
): void => {
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments,
|
||||
...(environment === undefined ? {} : { environment }),
|
||||
});
|
||||
};
|
||||
|
||||
const expectUpdateCallChannel = (channel: string) => {
|
||||
const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0];
|
||||
expect(call?.channel).toBe(channel);
|
||||
@@ -845,9 +859,7 @@ describe("update-cli", () => {
|
||||
|
||||
const mockOwnedGitService = () => {
|
||||
const serviceEntrypoint = path.join(process.cwd(), "dist", "index.js");
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["node", serviceEntrypoint, "gateway", "run"],
|
||||
});
|
||||
primeServiceCommand(["node", serviceEntrypoint, "gateway", "run"]);
|
||||
pathExists.mockImplementation(
|
||||
async (candidate: string) => candidate === path.join(process.cwd(), "package.json"),
|
||||
);
|
||||
@@ -886,10 +898,7 @@ describe("update-cli", () => {
|
||||
readPackageVersion.mockResolvedValue("2.0.0");
|
||||
|
||||
mockPackageInstallStatus(tempDir);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "0.0.1",
|
||||
});
|
||||
primeNpmChannelTag("latest", "0.0.1");
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue({
|
||||
status: "ok",
|
||||
mode: "npm",
|
||||
@@ -940,9 +949,7 @@ describe("update-cli", () => {
|
||||
});
|
||||
}
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["node", entrypoints[0], "gateway", "run"],
|
||||
});
|
||||
primeServiceCommand(["node", entrypoints[0], "gateway", "run"]);
|
||||
return { root, entrypoints };
|
||||
};
|
||||
|
||||
@@ -1337,10 +1344,7 @@ describe("update-cli", () => {
|
||||
version: "2026.6.33",
|
||||
packageSpec: "openclaw@2026.6.33",
|
||||
});
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "9999.0.0",
|
||||
});
|
||||
primeNpmChannelTag("latest", "9999.0.0");
|
||||
nodeVersionSatisfiesEngine.mockReturnValue(true);
|
||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||
root: "/test/path",
|
||||
@@ -1879,10 +1883,7 @@ describe("update-cli", () => {
|
||||
}),
|
||||
});
|
||||
readPackageVersion.mockResolvedValue("2026.4.14");
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.4.10",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.4.10");
|
||||
mockCurrentProcessFreshDoctor();
|
||||
mockGatewayProbe("2026.4.10", "downgraded-gateway");
|
||||
|
||||
@@ -1908,7 +1909,7 @@ describe("update-cli", () => {
|
||||
}),
|
||||
});
|
||||
readPackageVersion.mockResolvedValue("2026.4.14");
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({ tag: "latest", version: "2026.4.10" });
|
||||
primeNpmChannelTag("latest", "2026.4.10");
|
||||
mockCurrentProcessFreshDoctor();
|
||||
|
||||
await updateCommand({ yes: true, tag: "2026.4.10", restart: false });
|
||||
@@ -1936,7 +1937,7 @@ describe("update-cli", () => {
|
||||
readPackageVersion.mockImplementation(async (pkgRoot: string) =>
|
||||
pkgRoot === downgradedRoot ? "2026.4.10" : "2026.4.14",
|
||||
);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({ tag: "latest", version: "2026.4.10" });
|
||||
primeNpmChannelTag("latest", "2026.4.10");
|
||||
|
||||
delete process.env.OPENCLAW_COMPATIBILITY_HOST_VERSION;
|
||||
let hostVersionDuringPluginUpdate: string | undefined = "unset";
|
||||
@@ -3105,9 +3106,8 @@ describe("update-cli", () => {
|
||||
it("refuses a package target that changes after the service stops", async () => {
|
||||
mockPackageInstallStatus(process.cwd());
|
||||
mockOwnedGitService();
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["node", path.join(process.cwd(), "dist", "index.js"), "gateway", "run"],
|
||||
environment: { OPENCLAW_STATE_DIR: "/managed/state" },
|
||||
primeServiceCommand(["node", path.join(process.cwd(), "dist", "index.js"), "gateway", "run"], {
|
||||
OPENCLAW_STATE_DIR: "/managed/state",
|
||||
});
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
vi.mocked(fetchNpmPackageTargetStatus).mockResolvedValue({
|
||||
@@ -3345,10 +3345,7 @@ describe("update-cli", () => {
|
||||
...baseSnapshot,
|
||||
config: { update: { channel: "beta" } } as OpenClawConfig,
|
||||
});
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "1.2.3-1",
|
||||
});
|
||||
primeNpmChannelTag("latest", "1.2.3-1");
|
||||
await updateCommand({});
|
||||
|
||||
expectPackageInstallSpec("openclaw@1.2.3-1");
|
||||
@@ -3528,10 +3525,7 @@ describe("update-cli", () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
mockPackageInstallStatus(tempDir);
|
||||
readPackageVersion.mockResolvedValue("2026.4.22");
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.4.22",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.4.22");
|
||||
|
||||
await updateCommand({ yes: true });
|
||||
|
||||
@@ -3551,10 +3545,7 @@ describe("update-cli", () => {
|
||||
setTty(false);
|
||||
mockPackageInstallStatus(tempDir);
|
||||
readPackageVersion.mockResolvedValue("2026.4.22");
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: null,
|
||||
});
|
||||
primeNpmChannelTag("latest", null);
|
||||
mockCurrentProcessFreshDoctor();
|
||||
|
||||
await updateCommand({});
|
||||
@@ -3633,12 +3624,9 @@ describe("update-cli", () => {
|
||||
|
||||
it("refuses package updates from inherited gateway service env when --no-restart leaves the gateway running", async () => {
|
||||
mockPackageInstallStatus(createCaseDir("openclaw-update"));
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
},
|
||||
primeServiceCommand(["openclaw", "gateway", "run"], {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
});
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
|
||||
@@ -3670,12 +3658,9 @@ describe("update-cli", () => {
|
||||
"refuses package updates from inherited gateway service env when $name",
|
||||
async ({ setupRuntime }) => {
|
||||
mockPackageInstallStatus(createCaseDir("openclaw-update"));
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
},
|
||||
primeServiceCommand(["openclaw", "gateway", "run"], {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
});
|
||||
setupRuntime();
|
||||
|
||||
@@ -3758,10 +3743,7 @@ describe("update-cli", () => {
|
||||
|
||||
it("blocks package updates when the target requires a newer Node runtime", async () => {
|
||||
mockPackageInstallStatus(createCaseDir("openclaw-update"));
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.3.23-2",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.3.23-2");
|
||||
vi.mocked(fetchNpmPackageTargetStatus).mockResolvedValue({
|
||||
target: "latest",
|
||||
version: "2026.3.23-2",
|
||||
@@ -3894,10 +3876,7 @@ describe("update-cli", () => {
|
||||
const nodeModules = path.join(prefix, "lib", "node_modules");
|
||||
const { pkgRoot } = await setupInstalledPackageAtNodeModules(nodeModules, "2026.4.20");
|
||||
readPackageVersion.mockResolvedValue("2026.4.20");
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.4.25",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.4.25");
|
||||
vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
|
||||
if (Array.isArray(argv) && argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
|
||||
return commandResult({ stdout: `${nodeModules}\n` });
|
||||
@@ -3942,18 +3921,13 @@ describe("update-cli", () => {
|
||||
it("runs old package doctors without fix mode when service ownership is unknown", async () => {
|
||||
const tempDir = await createTrackedTempDir("openclaw-update-package-");
|
||||
const { nodeModules, pkgRoot, entryPath } = await setupInstalledPackageRoot(tempDir);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw-wrapper", "gateway", "run"],
|
||||
});
|
||||
primeServiceCommand(["openclaw-wrapper", "gateway", "run"]);
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
serviceReadRuntime.mockResolvedValue({ status: "stopped", state: "stopped" });
|
||||
readPackageVersion.mockImplementation(async (packageRoot: string) =>
|
||||
packageRoot === pkgRoot ? "2026.4.21" : "1.0.0",
|
||||
);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.4.21",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.4.21");
|
||||
mockFileBackedPathExists();
|
||||
mockNpmGlobalRoot(nodeModules);
|
||||
|
||||
@@ -3991,10 +3965,7 @@ describe("update-cli", () => {
|
||||
it("continues package post-core work for explicit post-update doctor advisories", async () => {
|
||||
const tempDir = await createTrackedTempDir("openclaw-update-package-doctor-warning-");
|
||||
const { nodeModules, entryPath } = await setupInstalledPackageRoot(tempDir);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.4.21",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.4.21");
|
||||
mockFileBackedPathExists();
|
||||
vi.mocked(runCommandWithTimeout).mockImplementation(async (argv, options) => {
|
||||
if (Array.isArray(argv) && argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
|
||||
@@ -4060,10 +4031,7 @@ describe("update-cli", () => {
|
||||
it("fails package updates when the post-update doctor is killed after verification", async () => {
|
||||
const tempDir = await createTrackedTempDir("openclaw-update-package-doctor-timeout-");
|
||||
const { nodeModules, entryPath } = await setupInstalledPackageRoot(tempDir);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.4.21",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.4.21");
|
||||
mockFileBackedPathExists();
|
||||
vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
|
||||
if (Array.isArray(argv) && argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
|
||||
@@ -4103,10 +4071,7 @@ describe("update-cli", () => {
|
||||
const { nodeModules, pkgRoot, entryPath } = await setupInstalledPackageAtNodeModules(
|
||||
path.join(tempDir, "lib", "node_modules"),
|
||||
);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.5.14",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.5.14");
|
||||
mockFileBackedPathExists();
|
||||
vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
|
||||
if (!Array.isArray(argv)) {
|
||||
@@ -4351,12 +4316,9 @@ describe("update-cli", () => {
|
||||
it("preserves both the update and Scheduled Task recovery failures", async () => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
mockPackageInstallStatus(createCaseDir("openclaw-update-recovery-failure"));
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
},
|
||||
primeServiceCommand(["openclaw", "gateway", "run"], {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
});
|
||||
serviceReadRuntime.mockResolvedValue({ status: "stopped", state: "stopped" });
|
||||
suspendScheduledTaskAutoStartForUpdate.mockResolvedValue(true);
|
||||
@@ -4399,12 +4361,9 @@ describe("update-cli", () => {
|
||||
);
|
||||
resumeScheduledTaskAutoStartAfterUpdate.mockResolvedValue(true);
|
||||
mockPackageInstallStatus(createCaseDir("openclaw-update-suspension-signal"));
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
},
|
||||
primeServiceCommand(["openclaw", "gateway", "run"], {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
});
|
||||
serviceReadRuntime.mockResolvedValue({ status: "stopped", state: "stopped" });
|
||||
|
||||
@@ -4439,12 +4398,9 @@ describe("update-cli", () => {
|
||||
async (runtimeStatus) => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
mockPackageInstallStatus(createCaseDir("openclaw-update-stopped-task"));
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
},
|
||||
primeServiceCommand(["openclaw", "gateway", "run"], {
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
});
|
||||
serviceReadRuntime.mockResolvedValue(
|
||||
runtimeStatus === "running"
|
||||
@@ -5025,9 +4981,7 @@ describe("update-cli", () => {
|
||||
"utf-8",
|
||||
);
|
||||
mockPackageInstallStatus(shellRoot);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: [serviceNode, path.join(serviceRoot, "dist", "index.js"), "gateway"],
|
||||
});
|
||||
primeServiceCommand([serviceNode, path.join(serviceRoot, "dist", "index.js"), "gateway"]);
|
||||
|
||||
await updateCommand({ dryRun: true });
|
||||
|
||||
@@ -5053,10 +5007,8 @@ describe("update-cli", () => {
|
||||
"utf-8",
|
||||
);
|
||||
mockPackageInstallStatus(shellRoot);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: [serviceNode, path.join(serviceRoot, "dist", "index.js"), "gateway"],
|
||||
});
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({ tag: "latest", version: "2026.5.20" });
|
||||
primeServiceCommand([serviceNode, path.join(serviceRoot, "dist", "index.js"), "gateway"]);
|
||||
primeNpmChannelTag("latest", "2026.5.20");
|
||||
vi.mocked(fetchNpmPackageTargetStatus).mockResolvedValue({
|
||||
target: "latest",
|
||||
version: "2026.5.20",
|
||||
@@ -5101,14 +5053,9 @@ describe("update-cli", () => {
|
||||
await fs.writeFile(entrypoint, "", "utf-8");
|
||||
await writePackageDistInventory(serviceRoot);
|
||||
mockPackageInstallStatus(shellRoot);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: [serviceNode, entrypoint, "gateway"],
|
||||
});
|
||||
primeServiceCommand([serviceNode, entrypoint, "gateway"]);
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.5.20",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.5.20");
|
||||
mockFileBackedPathExists();
|
||||
vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
|
||||
if (Array.isArray(argv) && argv[0] === serviceNode && argv[1] === "--version") {
|
||||
@@ -5157,9 +5104,7 @@ describe("update-cli", () => {
|
||||
const serviceNode = "/opt/other-node/bin/node";
|
||||
const entrypoint = path.join(root, "dist", "index.js");
|
||||
mockPackageInstallStatus(root);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: [serviceNode, entrypoint, "gateway"],
|
||||
});
|
||||
primeServiceCommand([serviceNode, entrypoint, "gateway"]);
|
||||
|
||||
await updateCommand({ dryRun: true });
|
||||
|
||||
@@ -5195,11 +5140,9 @@ describe("update-cli", () => {
|
||||
await writePackageDistInventory(root);
|
||||
// Same package root for both shell and service.
|
||||
mockPackageInstallStatus(root);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: [serviceNode, entrypoint, "gateway"],
|
||||
});
|
||||
primeServiceCommand([serviceNode, entrypoint, "gateway"]);
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({ tag: "latest", version: "2026.7.1" });
|
||||
primeNpmChannelTag("latest", "2026.7.1");
|
||||
vi.mocked(fetchNpmPackageTargetStatus).mockResolvedValue({
|
||||
target: "latest",
|
||||
version: "2026.7.1",
|
||||
@@ -5270,14 +5213,9 @@ describe("update-cli", () => {
|
||||
await fs.writeFile(entrypoint, "", "utf-8");
|
||||
await writePackageDistInventory(root);
|
||||
mockPackageInstallStatus(root);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: [serviceNode, entrypoint, "gateway"],
|
||||
});
|
||||
primeServiceCommand([serviceNode, entrypoint, "gateway"]);
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "latest",
|
||||
version: "2026.5.20",
|
||||
});
|
||||
primeNpmChannelTag("latest", "2026.5.20");
|
||||
mockFileBackedPathExists();
|
||||
// The PATH npm returns a DIFFERENT global root (simulates Node-B's npm).
|
||||
const nodeBGlobalRoot = path.join(
|
||||
@@ -6247,9 +6185,7 @@ describe("update-cli", () => {
|
||||
it("tries the updated install restart when package service refresh fails", async () => {
|
||||
const { updatedRoot, updatedEntrypoint } = setupNpmUpdatedRootRefresh();
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["node", updatedEntrypoint, "gateway", "run"],
|
||||
});
|
||||
primeServiceCommand(["node", updatedEntrypoint, "gateway", "run"]);
|
||||
mockGatewayInstallFailure(updatedEntrypoint);
|
||||
mockGatewayProbe("2026.4.24", "updated-gateway");
|
||||
|
||||
@@ -6289,9 +6225,7 @@ describe("update-cli", () => {
|
||||
candidate === updatedEntrypoint || candidate === updatedPackageJson,
|
||||
);
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["node", updatedEntrypoint, "gateway", "run"],
|
||||
});
|
||||
primeServiceCommand(["node", updatedEntrypoint, "gateway", "run"]);
|
||||
mockGatewayInstallFailure(updatedEntrypoint);
|
||||
mockGatewayProbe("2026.4.24", "matching-old-gateway");
|
||||
|
||||
@@ -6340,9 +6274,7 @@ describe("update-cli", () => {
|
||||
[oldEntrypoint, updatedEntrypoint, oldPackageJson, updatedPackageJson].includes(candidate),
|
||||
);
|
||||
serviceLoaded.mockResolvedValue(true);
|
||||
serviceReadCommand.mockResolvedValue({
|
||||
programArguments: ["node", oldEntrypoint, "gateway", "run"],
|
||||
});
|
||||
primeServiceCommand(["node", oldEntrypoint, "gateway", "run"]);
|
||||
mockGatewayInstallFailure(updatedEntrypoint);
|
||||
mockGatewayProbe("2026.4.24", "matching-old-service");
|
||||
|
||||
|
||||
@@ -233,16 +233,8 @@ vi.mock("../commands/model-picker.runtime.js", () => ({
|
||||
}));
|
||||
|
||||
const OPENROUTER_CATALOG = [
|
||||
{
|
||||
provider: "openrouter",
|
||||
id: "auto",
|
||||
name: "OpenRouter Auto",
|
||||
},
|
||||
{
|
||||
provider: "openrouter",
|
||||
id: "meta-llama/llama-3.3-70b:free",
|
||||
name: "Llama 3.3 70B",
|
||||
},
|
||||
catalogModel("openrouter", "auto", "OpenRouter Auto"),
|
||||
catalogModel("openrouter", "meta-llama/llama-3.3-70b:free", "Llama 3.3 70B"),
|
||||
] as const;
|
||||
|
||||
function expectRouterModelFiltering(options: Array<{ value: string }>) {
|
||||
@@ -256,6 +248,19 @@ function createSelectAllMultiselect() {
|
||||
return vi.fn(async (params) => params.options.map((option: { value: string }) => option.value));
|
||||
}
|
||||
|
||||
function promptDefaultPicker(params: Parameters<typeof promptDefaultModel>[0]) {
|
||||
return promptDefaultModel({
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
function catalogModel(provider: string, id: string, name: string): ModelCatalogEntry {
|
||||
return { provider, id, name };
|
||||
}
|
||||
|
||||
function configuredTextModel(id: string, name: string) {
|
||||
return {
|
||||
id,
|
||||
@@ -428,12 +433,9 @@ describe("promptDefaultModel", () => {
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
const options = pickerOptions(select as MockCallSource);
|
||||
@@ -462,12 +464,9 @@ describe("promptDefaultModel", () => {
|
||||
]);
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults } } as OpenClawConfig,
|
||||
prompter: makePrompter({ select }),
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
const option = requireOption(pickerOptions(select as MockCallSource), "openai/gpt-5.5");
|
||||
@@ -493,12 +492,9 @@ describe("promptDefaultModel", () => {
|
||||
} as OpenClawConfig;
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config,
|
||||
prompter: makePrompter({ select }),
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
const option = requireOption(pickerOptions(select as MockCallSource), "openai/gpt-5.5");
|
||||
@@ -537,12 +533,9 @@ describe("promptDefaultModel", () => {
|
||||
modelCatalogRouteVariants.value = [platform, chatGPT];
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
prompter: makePrompter({ select }),
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
const option = requireOption(pickerOptions(select as MockCallSource), "openai/gpt-5.5");
|
||||
@@ -562,12 +555,9 @@ describe("promptDefaultModel", () => {
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } } },
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
const values = optionValues(pickerOptions(select as MockCallSource));
|
||||
@@ -587,14 +577,11 @@ describe("promptDefaultModel", () => {
|
||||
]);
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: {
|
||||
agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" } } },
|
||||
},
|
||||
prompter: makePrompter({ select }),
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
expect(optionValues(pickerOptions(select as MockCallSource))).toEqual([
|
||||
@@ -618,12 +605,9 @@ describe("promptDefaultModel", () => {
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
const values = optionValues(pickerOptions(select as MockCallSource));
|
||||
@@ -659,12 +643,9 @@ describe("promptDefaultModel", () => {
|
||||
]);
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
prompter: makePrompter({ select }),
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
expect(optionValues(pickerOptions(select as MockCallSource))).toEqual([
|
||||
@@ -699,12 +680,9 @@ describe("promptDefaultModel", () => {
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
const values = optionValues(pickerOptions(select as MockCallSource));
|
||||
@@ -723,12 +701,9 @@ describe("promptDefaultModel", () => {
|
||||
const select = vi.fn(async (params) => params.options[0]?.value as never);
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
expect(result.model).toBe("google/gemini-3.1-pro-preview");
|
||||
@@ -764,12 +739,9 @@ describe("promptDefaultModel", () => {
|
||||
agents: { defaults: {} },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
expect(loadModelCatalog).not.toHaveBeenCalled();
|
||||
@@ -783,16 +755,8 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("treats byteplus plan models as preferred-provider matches", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
{
|
||||
provider: "byteplus-plan",
|
||||
id: "ark-code-latest",
|
||||
name: "Ark Coding Plan",
|
||||
},
|
||||
catalogModel("openai", "gpt-5.5", "GPT-5.5"),
|
||||
catalogModel("byteplus-plan", "ark-code-latest", "Ark Coding Plan"),
|
||||
]);
|
||||
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
@@ -805,12 +769,10 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "byteplus",
|
||||
});
|
||||
|
||||
@@ -828,11 +790,7 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("shows literal double-prefix labels for providers that preserve literal prefixes", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "Nemotron",
|
||||
},
|
||||
catalogModel("nvidia", "nvidia/nemotron-3-super-120b-a12b", "Nemotron"),
|
||||
]);
|
||||
resolvePluginProviders.mockReturnValue([
|
||||
{
|
||||
@@ -851,12 +809,10 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
const options = pickerOptions(select as MockCallSource);
|
||||
@@ -870,21 +826,9 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("does not double-prefix non-literal NVIDIA vendor model labels", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "Nemotron",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "minimaxai/minimax-m2.7",
|
||||
name: "MiniMax M2.7",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "z-ai/glm-5.1",
|
||||
name: "GLM 5.1",
|
||||
},
|
||||
catalogModel("nvidia", "nvidia/nemotron-3-super-120b-a12b", "Nemotron"),
|
||||
catalogModel("nvidia", "minimaxai/minimax-m2.7", "MiniMax M2.7"),
|
||||
catalogModel("nvidia", "z-ai/glm-5.1", "GLM 5.1"),
|
||||
]);
|
||||
resolvePluginProviders.mockReturnValue([
|
||||
{
|
||||
@@ -896,12 +840,9 @@ describe("promptDefaultModel", () => {
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "nvidia",
|
||||
});
|
||||
|
||||
@@ -933,12 +874,11 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: true,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "nvidia",
|
||||
browseCatalogOnDemand: true,
|
||||
});
|
||||
@@ -966,12 +906,11 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: true,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "openai",
|
||||
browseCatalogOnDemand: true,
|
||||
});
|
||||
@@ -999,12 +938,11 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: true,
|
||||
ignoreAllowlist: true,
|
||||
browseCatalogOnDemand: true,
|
||||
loadCatalog: true,
|
||||
});
|
||||
@@ -1023,16 +961,8 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("loads the full model catalog when browsing without a preferred provider", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5-pro",
|
||||
name: "GPT-5.5 Pro",
|
||||
},
|
||||
catalogModel("openai", "gpt-5.5", "GPT-5.5"),
|
||||
catalogModel("openai", "gpt-5.5-pro", "GPT-5.5 Pro"),
|
||||
]);
|
||||
const select = vi
|
||||
.fn()
|
||||
@@ -1052,12 +982,11 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: true,
|
||||
ignoreAllowlist: true,
|
||||
browseCatalogOnDemand: true,
|
||||
});
|
||||
|
||||
@@ -1070,16 +999,8 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("loads the preferred provider catalog when the user chooses to browse", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5-pro",
|
||||
name: "GPT-5.5 Pro",
|
||||
},
|
||||
catalogModel("openai", "gpt-5.5", "GPT-5.5"),
|
||||
catalogModel("openai", "gpt-5.5-pro", "GPT-5.5 Pro"),
|
||||
]);
|
||||
const select = vi
|
||||
.fn()
|
||||
@@ -1099,12 +1020,11 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: true,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "openai",
|
||||
browseCatalogOnDemand: true,
|
||||
});
|
||||
@@ -1122,16 +1042,8 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("scopes on-demand preferred-provider loads before the first model prompt", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "NVIDIA Nemotron 3 Super 120B",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "moonshotai/kimi-k2.5",
|
||||
name: "Kimi K2.5",
|
||||
},
|
||||
catalogModel("nvidia", "nvidia/nemotron-3-super-120b-a12b", "NVIDIA Nemotron 3 Super 120B"),
|
||||
catalogModel("nvidia", "moonshotai/kimi-k2.5", "Kimi K2.5"),
|
||||
]);
|
||||
const select = vi.fn(async (params) => params.options[0]?.value as never);
|
||||
const prompter = makePrompter({ select });
|
||||
@@ -1143,12 +1055,9 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "nvidia",
|
||||
browseCatalogOnDemand: true,
|
||||
});
|
||||
@@ -1168,16 +1077,8 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("preselects the first live provider row when keep-current is disabled", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "z-ai/glm-5.1",
|
||||
name: "GLM 5.1",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "NVIDIA Nemotron 3 Super 120B",
|
||||
},
|
||||
catalogModel("nvidia", "z-ai/glm-5.1", "GLM 5.1"),
|
||||
catalogModel("nvidia", "nvidia/nemotron-3-super-120b-a12b", "NVIDIA Nemotron 3 Super 120B"),
|
||||
]);
|
||||
const select = vi.fn(async (params) => params.initialValue as never);
|
||||
const prompter = makePrompter({ select });
|
||||
@@ -1189,12 +1090,9 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "nvidia",
|
||||
browseCatalogOnDemand: true,
|
||||
});
|
||||
@@ -1214,21 +1112,9 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("keeps on-demand NVIDIA vendor labels single-prefixed after browsing", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "NVIDIA Nemotron 3 Super 120B",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "minimaxai/minimax-m2.7",
|
||||
name: "MiniMax M2.7",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "z-ai/glm-5.1",
|
||||
name: "GLM 5.1",
|
||||
},
|
||||
catalogModel("nvidia", "nvidia/nemotron-3-super-120b-a12b", "NVIDIA Nemotron 3 Super 120B"),
|
||||
catalogModel("nvidia", "minimaxai/minimax-m2.7", "MiniMax M2.7"),
|
||||
catalogModel("nvidia", "z-ai/glm-5.1", "GLM 5.1"),
|
||||
]);
|
||||
resolvePluginProviders.mockReturnValue([
|
||||
{
|
||||
@@ -1242,7 +1128,7 @@ describe("promptDefaultModel", () => {
|
||||
.mockResolvedValueOnce("nvidia/nemotron-3-super-120b-a12b");
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -1253,7 +1139,6 @@ describe("promptDefaultModel", () => {
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: true,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "nvidia",
|
||||
browseCatalogOnDemand: true,
|
||||
});
|
||||
@@ -1270,21 +1155,9 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("omits local NVIDIA static fallback rows when browsing live provider rows", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "NVIDIA Nemotron 3 Super 120B",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "minimaxai/minimax-m2.7",
|
||||
name: "MiniMax M2.7",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "z-ai/glm-5.1",
|
||||
name: "GLM 5.1",
|
||||
},
|
||||
catalogModel("nvidia", "nvidia/nemotron-3-super-120b-a12b", "NVIDIA Nemotron 3 Super 120B"),
|
||||
catalogModel("nvidia", "minimaxai/minimax-m2.7", "MiniMax M2.7"),
|
||||
catalogModel("nvidia", "z-ai/glm-5.1", "GLM 5.1"),
|
||||
]);
|
||||
loadStaticManifestCatalogRowsForList.mockReturnValue([
|
||||
manifestTextRow("nvidia", "minimaxai/minimax-m2.5", "MiniMax M2.5", "deprecated"),
|
||||
@@ -1302,7 +1175,7 @@ describe("promptDefaultModel", () => {
|
||||
.mockResolvedValueOnce("nvidia/nemotron-3-super-120b-a12b");
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -1313,7 +1186,6 @@ describe("promptDefaultModel", () => {
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: true,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "nvidia",
|
||||
browseCatalogOnDemand: true,
|
||||
});
|
||||
@@ -1329,11 +1201,7 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("uses the configured default agent dir for provider-scoped catalog auth", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "z-ai/glm-5.1",
|
||||
name: "GLM 5.1",
|
||||
},
|
||||
catalogModel("nvidia", "z-ai/glm-5.1", "GLM 5.1"),
|
||||
]);
|
||||
const select = vi.fn(async (params) => params.options[0]?.value as never);
|
||||
const prompter = makePrompter({ select });
|
||||
@@ -1350,12 +1218,9 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
preferredProvider: "nvidia",
|
||||
browseCatalogOnDemand: true,
|
||||
env,
|
||||
@@ -1371,11 +1236,7 @@ describe("promptDefaultModel", () => {
|
||||
|
||||
it("supports configuring vLLM during setup", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "anthropic",
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.5",
|
||||
},
|
||||
catalogModel("anthropic", "claude-sonnet-4-6", "Claude Sonnet 4.5"),
|
||||
]);
|
||||
resolveProviderModelPickerEntries.mockReturnValue([
|
||||
{ value: "vllm", label: "vLLM (custom)", hint: "Enter vLLM URL + API key + model" },
|
||||
@@ -1413,13 +1274,10 @@ describe("promptDefaultModel", () => {
|
||||
const prompter = makePrompter({ select });
|
||||
const config = { agents: { defaults: {} } } as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
includeProviderPluginSetups: true,
|
||||
ignoreAllowlist: true,
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
runtime: {} as never,
|
||||
});
|
||||
@@ -1443,13 +1301,7 @@ describe("promptDefaultModel", () => {
|
||||
});
|
||||
|
||||
it("prefers provider model-picker contributions when the runtime exposes them", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
]);
|
||||
loadModelCatalog.mockResolvedValue([catalogModel("openai", "gpt-5.5", "GPT-5.5")]);
|
||||
providerModelPickerContributionRuntime.enabled = true;
|
||||
providerModelPickerContributionRuntime.resolve.mockReturnValue([
|
||||
{
|
||||
@@ -1477,13 +1329,10 @@ describe("promptDefaultModel", () => {
|
||||
});
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
includeProviderPluginSetups: true,
|
||||
ignoreAllowlist: true,
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
runtime: {} as never,
|
||||
});
|
||||
@@ -1505,12 +1354,11 @@ describe("promptDefaultModel", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
const result = await promptDefaultPicker({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: true,
|
||||
includeManual: true,
|
||||
ignoreAllowlist: true,
|
||||
includeProviderPluginSetups: true,
|
||||
loadCatalog: false,
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
@@ -1529,13 +1377,7 @@ describe("promptDefaultModel", () => {
|
||||
});
|
||||
|
||||
it("surfaces NVIDIA provider model-picker contributions", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.4",
|
||||
name: "GPT-5.4",
|
||||
},
|
||||
]);
|
||||
loadModelCatalog.mockResolvedValue([catalogModel("openai", "gpt-5.4", "GPT-5.4")]);
|
||||
providerModelPickerContributionRuntime.enabled = true;
|
||||
providerModelPickerContributionRuntime.resolve.mockReturnValue([
|
||||
{
|
||||
@@ -1558,13 +1400,10 @@ describe("promptDefaultModel", () => {
|
||||
});
|
||||
const prompter = makePrompter({ select });
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
includeProviderPluginSetups: true,
|
||||
ignoreAllowlist: true,
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
runtime: {} as never,
|
||||
});
|
||||
@@ -1579,21 +1418,9 @@ describe("promptDefaultModel", () => {
|
||||
describe("promptModelAllowlist", () => {
|
||||
it("filters to allowed keys when provided", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "anthropic",
|
||||
id: "claude-opus-4-6",
|
||||
name: "Claude Opus 4.5",
|
||||
},
|
||||
{
|
||||
provider: "anthropic",
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.5",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
catalogModel("anthropic", "claude-opus-4-6", "Claude Opus 4.5"),
|
||||
catalogModel("anthropic", "claude-sonnet-4-6", "Claude Sonnet 4.5"),
|
||||
catalogModel("openai", "gpt-5.5", "GPT-5.5"),
|
||||
]);
|
||||
|
||||
const multiselect = createSelectAllMultiselect();
|
||||
@@ -1613,13 +1440,7 @@ describe("promptModelAllowlist", () => {
|
||||
|
||||
it("localizes the model allowlist picker", async () => {
|
||||
process.env.OPENCLAW_LOCALE = "zh-CN";
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
]);
|
||||
loadModelCatalog.mockResolvedValue([catalogModel("openai", "gpt-5.5", "GPT-5.5")]);
|
||||
|
||||
const multiselect = createSelectAllMultiselect();
|
||||
const prompter = makePrompter({ multiselect });
|
||||
@@ -1743,13 +1564,7 @@ describe("promptModelAllowlist", () => {
|
||||
});
|
||||
|
||||
it("uses configured provider models for allowlist picker without loading the full catalog in replace mode", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
]);
|
||||
loadModelCatalog.mockResolvedValue([catalogModel("openai", "gpt-5.5", "GPT-5.5")]);
|
||||
|
||||
const multiselect = createSelectAllMultiselect();
|
||||
const prompter = makePrompter({ multiselect });
|
||||
@@ -1782,21 +1597,9 @@ describe("promptModelAllowlist", () => {
|
||||
|
||||
it("scopes the initial allowlist picker to the preferred provider", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "anthropic",
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.5",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.4-mini",
|
||||
name: "GPT-5.4 Mini",
|
||||
},
|
||||
catalogModel("anthropic", "claude-sonnet-4-6", "Claude Sonnet 4.5"),
|
||||
catalogModel("openai", "gpt-5.5", "GPT-5.5"),
|
||||
catalogModel("openai", "gpt-5.4-mini", "GPT-5.4 Mini"),
|
||||
]);
|
||||
|
||||
const multiselect = createSelectAllMultiselect();
|
||||
@@ -1815,16 +1618,8 @@ describe("promptModelAllowlist", () => {
|
||||
|
||||
it("includes stale configured preferred provider models in the scoped cleanup", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openrouter",
|
||||
id: "meta-llama/llama-3.3-70b:free",
|
||||
name: "Llama 3.3 70B",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
catalogModel("openrouter", "meta-llama/llama-3.3-70b:free", "Llama 3.3 70B"),
|
||||
catalogModel("openai", "gpt-5.5", "GPT-5.5"),
|
||||
]);
|
||||
|
||||
const activeModel = "openrouter/meta-llama/llama-3.3-70b:free";
|
||||
@@ -1918,16 +1713,8 @@ describe("promptModelAllowlist", () => {
|
||||
|
||||
it("keeps live preferred-provider rows before configured fallback supplements", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "minimaxai/minimax-m2.7",
|
||||
name: "MiniMax M2.7",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "Nemotron 3 Super",
|
||||
},
|
||||
catalogModel("nvidia", "minimaxai/minimax-m2.7", "MiniMax M2.7"),
|
||||
catalogModel("nvidia", "nvidia/nemotron-3-super-120b-a12b", "Nemotron 3 Super"),
|
||||
]);
|
||||
|
||||
const multiselect = createSelectAllMultiselect();
|
||||
@@ -1969,36 +1756,12 @@ describe("promptModelAllowlist", () => {
|
||||
|
||||
it("keeps provider-scoped live rows authoritative over configured provider supplements", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "Nemotron 3 Super",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "z-ai/glm-5.1",
|
||||
name: "GLM 5.1",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "minimaxai/minimax-m2.7",
|
||||
name: "MiniMax M2.7",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "moonshotai/kimi-k2.5",
|
||||
name: "Kimi K2.5",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "minimaxai/minimax-m2.5",
|
||||
name: "MiniMax M2.5",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "z-ai/glm5",
|
||||
name: "GLM5",
|
||||
},
|
||||
catalogModel("nvidia", "nvidia/nemotron-3-super-120b-a12b", "Nemotron 3 Super"),
|
||||
catalogModel("nvidia", "z-ai/glm-5.1", "GLM 5.1"),
|
||||
catalogModel("nvidia", "minimaxai/minimax-m2.7", "MiniMax M2.7"),
|
||||
catalogModel("nvidia", "moonshotai/kimi-k2.5", "Kimi K2.5"),
|
||||
catalogModel("nvidia", "minimaxai/minimax-m2.5", "MiniMax M2.5"),
|
||||
catalogModel("nvidia", "z-ai/glm5", "GLM5"),
|
||||
]);
|
||||
loadStaticManifestCatalogRowsForList.mockReturnValue([
|
||||
manifestTextRow("nvidia", "nvidia/nemotron-3-super-120b-a12b", "Bundled Nemotron 3 Super"),
|
||||
@@ -2052,16 +1815,8 @@ describe("promptModelAllowlist", () => {
|
||||
|
||||
it("keeps custom configured rows after provider-scoped live rows", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "nvidia/nemotron-3-super-120b-a12b",
|
||||
name: "Nemotron 3 Super",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "z-ai/glm-5.1",
|
||||
name: "GLM 5.1",
|
||||
},
|
||||
catalogModel("nvidia", "nvidia/nemotron-3-super-120b-a12b", "Nemotron 3 Super"),
|
||||
catalogModel("nvidia", "z-ai/glm-5.1", "GLM 5.1"),
|
||||
]);
|
||||
loadStaticManifestCatalogRowsForList.mockReturnValue([
|
||||
manifestTextRow("nvidia", "nvidia/nemotron-3-super-120b-a12b", "Bundled Nemotron 3 Super"),
|
||||
@@ -2107,16 +1862,8 @@ describe("promptModelAllowlist", () => {
|
||||
|
||||
it("does not re-add configured static rows after filtering deprecated live rows", async () => {
|
||||
loadPreferredProviderPickerCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "minimaxai/minimax-m2.5",
|
||||
name: "MiniMax M2.5",
|
||||
},
|
||||
{
|
||||
provider: "nvidia",
|
||||
id: "z-ai/glm5",
|
||||
name: "GLM5",
|
||||
},
|
||||
catalogModel("nvidia", "minimaxai/minimax-m2.5", "MiniMax M2.5"),
|
||||
catalogModel("nvidia", "z-ai/glm5", "GLM5"),
|
||||
]);
|
||||
loadStaticManifestCatalogRowsForList.mockReturnValue([
|
||||
manifestTextRow("nvidia", "minimaxai/minimax-m2.5", "Bundled MiniMax M2.5", "deprecated"),
|
||||
@@ -2196,16 +1943,8 @@ describe("promptModelAllowlist", () => {
|
||||
it("keeps local no-key provider models visible in allowlist choices", async () => {
|
||||
resolveEnvApiKey.mockReturnValue(null);
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "vllm",
|
||||
id: "meta-llama/Meta-Llama-3-8B-Instruct",
|
||||
name: "Meta Llama",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
catalogModel("vllm", "meta-llama/Meta-Llama-3-8B-Instruct", "Meta Llama"),
|
||||
catalogModel("openai", "gpt-5.5", "GPT-5.5"),
|
||||
]);
|
||||
|
||||
const multiselect = createSelectAllMultiselect();
|
||||
@@ -2232,13 +1971,7 @@ describe("promptModelAllowlist", () => {
|
||||
});
|
||||
|
||||
it("seeds existing model fallbacks into unscoped allowlist selections", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
]);
|
||||
loadModelCatalog.mockResolvedValue([catalogModel("openai", "gpt-5.5", "GPT-5.5")]);
|
||||
|
||||
const multiselect = vi.fn(async (params) => params.initialValues ?? []);
|
||||
const prompter = makePrompter({ multiselect });
|
||||
@@ -2268,21 +2001,9 @@ describe("promptModelAllowlist", () => {
|
||||
|
||||
it("resolves bare fallback seeds against the primary model provider", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "anthropic",
|
||||
id: "claude-opus-4-6",
|
||||
name: "Claude Opus 4.5",
|
||||
},
|
||||
{
|
||||
provider: "anthropic",
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.5",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Wrong provider",
|
||||
},
|
||||
catalogModel("anthropic", "claude-opus-4-6", "Claude Opus 4.5"),
|
||||
catalogModel("anthropic", "claude-sonnet-4-6", "Claude Sonnet 4.5"),
|
||||
catalogModel("openai", "claude-sonnet-4-6", "Wrong provider"),
|
||||
]);
|
||||
|
||||
const multiselect = vi.fn(async (params) => params.initialValues ?? []);
|
||||
@@ -2356,21 +2077,9 @@ describe("promptModelAllowlist", () => {
|
||||
|
||||
it("keeps provider-scoped fallback supplements within scope", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.4",
|
||||
name: "GPT-5.4",
|
||||
},
|
||||
{
|
||||
provider: "anthropic",
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.5",
|
||||
},
|
||||
catalogModel("openai", "gpt-5.5", "GPT-5.5"),
|
||||
catalogModel("openai", "gpt-5.4", "GPT-5.4"),
|
||||
catalogModel("anthropic", "claude-sonnet-4-6", "Claude Sonnet 4.5"),
|
||||
]);
|
||||
|
||||
const multiselect = vi.fn(async (params) => params.initialValues ?? []);
|
||||
@@ -2515,12 +2224,9 @@ describe("router model filtering", () => {
|
||||
const allowlistPrompter = makePrompter({ multiselect });
|
||||
const config = { agents: { defaults: {} } } as OpenClawConfig;
|
||||
|
||||
await promptDefaultModel({
|
||||
await promptDefaultPicker({
|
||||
config,
|
||||
prompter: defaultPrompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
await promptModelAllowlist({ config, prompter: allowlistPrompter });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user