Download test/cli-json-stdout.models.e2e.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/test/cli-json-stdout.models.e2e.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/test/cli-json-stdout.models.e2e.test.ts
-
curl -L -o cli-json-stdout.models.e2e.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/test/cli-json-stdout.models.e2e.test.ts
12.2 kB
| import fs from "node:fs/promises"; | |
| import path from "node:path"; | |
| import { DatabaseSync } from "node:sqlite"; | |
| import { withTempHome } from "openclaw/plugin-sdk/test-env"; | |
| import { describe, expect, it } from "vitest"; | |
| import { readConfigMachineState } from "../src/state/config-machine-state.js"; | |
| import { OPENCLAW_STATE_SCHEMA_SQL } from "../src/state/openclaw-state-schema.js"; | |
| import { runBuiltCli } from "./cli-json-stdout.test-support.js"; | |
| async function seedPendingStateMigration(stateDir: string) { | |
| const databasePath = path.join(stateDir, "state", "openclaw.sqlite"); | |
| await fs.mkdir(path.dirname(databasePath), { recursive: true }); | |
| const database = new DatabaseSync(databasePath); | |
| try { | |
| database.exec(OPENCLAW_STATE_SCHEMA_SQL); | |
| database.exec("PRAGMA user_version = 0;"); | |
| } finally { | |
| database.close(); | |
| } | |
| } | |
| describe("cli json stdout contract", () => { | |
| it.each([ | |
| { name: "leaf JSON", args: ["models", "refresh", "--json"] }, | |
| { name: "parent JSON", args: ["models", "--json", "refresh"] }, | |
| { name: "human output", args: ["models", "refresh"], human: true }, | |
| { | |
| name: "forced Commander JSON", | |
| args: ["models", "refresh", "--json"], | |
| commander: true, | |
| }, | |
| { | |
| name: "dual-TTY JSON", | |
| args: ["models", "refresh", "--json"], | |
| tty: true, | |
| }, | |
| ])("renders model catalog refresh failures canonically for $name", async (testCase) => { | |
| await withTempHome( | |
| async (tempHome) => { | |
| const preload = Buffer.from( | |
| [ | |
| 'import net from "node:net";', | |
| 'net.Socket.prototype.connect = function () { throw new Error("AUTOQA_NETWORK_FORBIDDEN"); };', | |
| 'globalThis.fetch = async () => { throw new Error("offline fixture"); };', | |
| "globalThis.fetch.mock = {};", | |
| ...("tty" in testCase | |
| ? [ | |
| 'Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });', | |
| 'Object.defineProperty(process.stderr, "isTTY", { value: true, configurable: true });', | |
| ] | |
| : []), | |
| ].join("\n"), | |
| ).toString("base64"); | |
| const result = runBuiltCli(tempHome, testCase.args, { | |
| NODE_OPTIONS: `--import=data:text/javascript;base64,${preload}`, | |
| OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"), | |
| OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"), | |
| ...("commander" in testCase ? { OPENCLAW_DISABLE_ROUTE_FIRST: "1" } : {}), | |
| ...("tty" in testCase ? { FORCE_COLOR: "1" } : {}), | |
| }); | |
| const message = "Remote catalog refresh failed: Error: offline fixture"; | |
| expect(result.status, result.stderr).toBe(1); | |
| expect(result.stdout, result.stderr).not.toMatch(/[\u001B\u0007]/u); | |
| if ("human" in testCase) { | |
| expect(result.stdout).toBe(""); | |
| } else { | |
| expect(JSON.parse(result.stdout)).toEqual({ | |
| ok: false, | |
| error: { type: "cli_error", message }, | |
| }); | |
| } | |
| expect(result.stderr).toContain(message); | |
| expect(result.stderr.split(message)).toHaveLength(2); | |
| expect(result.stderr).not.toContain("AUTOQA_NETWORK_FORBIDDEN"); | |
| if ("tty" in testCase) { | |
| expect(result.stderr).toContain("\u001B[?25h"); | |
| } | |
| }, | |
| { prefix: "openclaw-models-refresh-json-failure-e2e-" }, | |
| ); | |
| }); | |
| // Every case opens the state database: config-health observation | |
| // (observeConfigSnapshot -> readConfigHealthStateFromStore) runs on any | |
| // config read whose file exists, so the migration diagnostic always lands | |
| // on stderr; the protected contract is that stdout stays exact. | |
| it.each([ | |
| { | |
| name: "aliases list", | |
| args: ["models", "aliases", "list", "--plain"], | |
| opensStateDatabase: true, | |
| expectedStdout: "chat anthropic/claude-sonnet-4-6\n", | |
| }, | |
| { | |
| name: "fallbacks list", | |
| args: ["models", "fallbacks", "list", "--plain"], | |
| opensStateDatabase: true, | |
| expectedStdout: "anthropic/claude-sonnet-4-6\n", | |
| }, | |
| { | |
| name: "image fallbacks list", | |
| args: ["models", "image-fallbacks", "list", "--plain"], | |
| opensStateDatabase: true, | |
| expectedStdout: "anthropic/claude-sonnet-4-6\n", | |
| }, | |
| { | |
| name: "list control", | |
| args: ["models", "list", "--plain"], | |
| opensStateDatabase: true, | |
| expectedStdout: "anthropic/claude-sonnet-4-6\n", | |
| }, | |
| { | |
| name: "status control", | |
| args: ["models", "status", "--plain"], | |
| opensStateDatabase: true, | |
| expectedStdout: "anthropic/claude-sonnet-4-6\n", | |
| }, | |
| { | |
| name: "parent status control", | |
| args: ["models", "--status-plain"], | |
| opensStateDatabase: true, | |
| expectedStdout: "anthropic/claude-sonnet-4-6\n", | |
| }, | |
| ])("keeps $name stdout exact during a pending state migration", async (testCase) => { | |
| await withTempHome( | |
| async (tempHome) => { | |
| const stateDir = path.join(tempHome, "isolated-state"); | |
| const configPath = path.join(tempHome, "openclaw.json"); | |
| const migrationDiagnostic = "state database schema migration pending"; | |
| await seedPendingStateMigration(stateDir); | |
| await fs.writeFile( | |
| configPath, | |
| JSON.stringify({ | |
| agents: { | |
| defaults: { | |
| model: { | |
| primary: "anthropic/claude-sonnet-4-6", | |
| fallbacks: ["anthropic/claude-sonnet-4-6"], | |
| }, | |
| imageModel: { fallbacks: ["anthropic/claude-sonnet-4-6"] }, | |
| models: { "anthropic/claude-sonnet-4-6": { alias: "chat" } }, | |
| }, | |
| }, | |
| }), | |
| ); | |
| const result = runBuiltCli( | |
| tempHome, | |
| testCase.args, | |
| { | |
| CI: "1", | |
| NO_COLOR: "1", | |
| OPENCLAW_CONFIG_PATH: configPath, | |
| OPENCLAW_STATE_DIR: stateDir, | |
| }, | |
| { inheritEnvironment: false }, | |
| ); | |
| expect(result.status, result.stderr).toBe(0); | |
| expect(result.stdout).toBe(testCase.expectedStdout); | |
| expect(result.stdout).not.toContain(migrationDiagnostic); | |
| expect(result.stderr.includes(migrationDiagnostic), result.stderr).toBe( | |
| testCase.opensStateDatabase, | |
| ); | |
| }, | |
| { prefix: "openclaw-models-plain-stdout-e2e-" }, | |
| ); | |
| }); | |
| it.each(["--plain", "--json"])( | |
| "keeps human auth-list output on stdout when provider value is %s", | |
| async (provider) => { | |
| await withTempHome( | |
| async (tempHome) => { | |
| const result = runBuiltCli(tempHome, ["models", "auth", "list", "--provider", provider], { | |
| CI: "1", | |
| NO_COLOR: "1", | |
| OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"), | |
| OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"), | |
| }); | |
| expect(result.status, result.stderr).toBe(0); | |
| expect(result.stdout).toContain("Agent: main\n"); | |
| expect(result.stdout).toContain(`Provider: ${provider}\n`); | |
| expect(result.stdout).toContain("Profiles: (none)\n"); | |
| expect(result.stderr).not.toContain("Agent: main"); | |
| expect(result.stderr).not.toContain(`Provider: ${provider}`); | |
| }, | |
| { prefix: "openclaw-models-output-option-value-e2e-" }, | |
| ); | |
| }, | |
| ); | |
| it("preserves model catalog refresh success payloads and persisted rows", async () => { | |
| await withTempHome( | |
| async (tempHome) => { | |
| const stateDir = path.join(tempHome, "isolated-state"); | |
| const configPath = path.join(tempHome, "openclaw.json"); | |
| const databasePath = path.join(stateDir, "state", "openclaw.sqlite"); | |
| const generatedAt = Date.now() + 60_000; | |
| const bundle = { | |
| schemaVersion: 1, | |
| generatedAt, | |
| sourceCommit: "autoqa-model-catalog-fixture", | |
| providers: { openai: { models: [{ id: "gpt-5.6-luna" }] } }, | |
| }; | |
| const updatedBundle = { ...bundle, generatedAt: generatedAt + 1_000 }; | |
| const preloadFor = (response: "initial" | "updated" | "unchanged" | "failure") => { | |
| const fixture = response === "initial" ? bundle : updatedBundle; | |
| const fetchResponse = | |
| response === "failure" | |
| ? 'throw new Error("offline fixture");' | |
| : response === "unchanged" | |
| ? "return new Response(null, { status: 304 });" | |
| : `return new Response(JSON.stringify(${JSON.stringify(fixture)}), { headers: { etag: '\"fixture\"' } });`; | |
| return Buffer.from( | |
| [ | |
| 'import net from "node:net";', | |
| 'import module from "node:module";', | |
| 'net.Socket.prototype.connect = function () { throw new Error("AUTOQA_NETWORK_FORBIDDEN"); };', | |
| "const createRequire = module.createRequire;", | |
| 'module.createRequire = (...args) => new Proxy(createRequire(...args), { apply(target, receiver, request) { return String(request[0]).endsWith("/build-info.json") ? { builtAt: "2020-01-01T00:00:00.000Z" } : Reflect.apply(target, receiver, request); } });', | |
| "module.syncBuiltinESMExports();", | |
| `globalThis.fetch = async () => { ${fetchResponse} };`, | |
| "globalThis.fetch.mock = {};", | |
| ].join("\n"), | |
| ).toString("base64"); | |
| }; | |
| const runRefresh = ( | |
| args: string[], | |
| response: "initial" | "updated" | "unchanged" | "failure", | |
| ) => | |
| runBuiltCli(tempHome, ["models", ...args], { | |
| NODE_OPTIONS: `--import=data:text/javascript;base64,${preloadFor(response)}`, | |
| OPENCLAW_CONFIG_PATH: configPath, | |
| OPENCLAW_STATE_DIR: stateDir, | |
| }); | |
| const readCatalogRow = () => | |
| readConfigMachineState<{ generated_at: number; bundle_json: string }>( | |
| "modelCatalog.remote", | |
| { path: databasePath }, | |
| ); | |
| const human = runRefresh(["refresh"], "initial"); | |
| expect(human.status, human.stderr).toBe(0); | |
| expect(human.stdout).toContain("Remote catalog refresh: updated (1 providers, 1 models;"); | |
| expect(human.stdout).toContain( | |
| "A running Gateway applies the updated catalog after its next restart.", | |
| ); | |
| const updated = runRefresh(["refresh", "--json"], "updated"); | |
| expect(updated.status, updated.stderr).toBe(0); | |
| expect(JSON.parse(updated.stdout)).toEqual({ | |
| status: "updated", | |
| generatedAt: updatedBundle.generatedAt, | |
| providers: 1, | |
| models: 1, | |
| }); | |
| const unchanged = runRefresh(["--json", "refresh"], "unchanged"); | |
| expect(unchanged.status, unchanged.stderr).toBe(0); | |
| expect(JSON.parse(unchanged.stdout)).toEqual({ | |
| status: "unchanged", | |
| generatedAt: updatedBundle.generatedAt, | |
| providers: 1, | |
| models: 1, | |
| }); | |
| const persistedRow = readCatalogRow(); | |
| expect(persistedRow).toMatchObject({ | |
| generated_at: updatedBundle.generatedAt, | |
| bundle_json: JSON.stringify(updatedBundle), | |
| }); | |
| const failure = runRefresh(["refresh", "--json"], "failure"); | |
| expect(failure.status, failure.stderr).toBe(1); | |
| expect(JSON.parse(failure.stdout)).toEqual({ | |
| ok: false, | |
| error: { | |
| type: "cli_error", | |
| message: "Remote catalog refresh failed: Error: offline fixture", | |
| }, | |
| }); | |
| expect(readCatalogRow()).toEqual(persistedRow); | |
| await fs.writeFile( | |
| configPath, | |
| `${JSON.stringify({ models: { catalogRefresh: { enabled: false } } })}\n`, | |
| "utf8", | |
| ); | |
| const disabled = runRefresh(["refresh", "--json"], "failure"); | |
| expect(disabled.status, disabled.stderr).toBe(0); | |
| expect(JSON.parse(disabled.stdout)).toEqual({ | |
| status: "disabled", | |
| providers: 0, | |
| models: 0, | |
| }); | |
| expect(readCatalogRow()).toEqual(persistedRow); | |
| }, | |
| { prefix: "openclaw-models-refresh-persistence-e2e-" }, | |
| ); | |
| }); | |
| }); | |