mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
201 lines
7.7 KiB
TypeScript
201 lines
7.7 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { withTempHome } from "openclaw/plugin-sdk/test-env";
|
|
import { describe, expect, it } from "vitest";
|
|
import { runBuiltCli } from "./cli-json-stdout.test-support.js";
|
|
|
|
async function seedTrajectorySession(tempHome: string, sessionKey: string) {
|
|
const stateDir = path.join(tempHome, "isolated-state");
|
|
const env: NodeJS.ProcessEnv = {
|
|
...process.env,
|
|
HOME: tempHome,
|
|
USERPROFILE: tempHome,
|
|
OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"),
|
|
OPENCLAW_STATE_DIR: stateDir,
|
|
};
|
|
delete env.OPENCLAW_HOME;
|
|
const [{ upsertSessionEntryCore }, { closeOpenClawAgentDatabaseByPath }] = await Promise.all([
|
|
import("../src/config/sessions/session-accessor.js"),
|
|
import("../src/state/openclaw-agent-db.js"),
|
|
]);
|
|
await upsertSessionEntryCore(
|
|
{ agentId: "main", env, sessionKey },
|
|
{ sessionId: "trajectory-process-session", updatedAt: 1 },
|
|
);
|
|
closeOpenClawAgentDatabaseByPath(
|
|
path.join(stateDir, "agents", "main", "agent", "openclaw-agent.sqlite"),
|
|
);
|
|
}
|
|
|
|
describe("cli json stdout contract", () => {
|
|
// Command-level suites own individual option validation. This matrix keeps
|
|
// the distinct route-first, Commander, TTY, and operational finalizers.
|
|
it.each([
|
|
{
|
|
name: "human route-first validation",
|
|
args: ["sessions", "--active", "0"],
|
|
message: "--active must be a positive number of minutes, for example --active 30.",
|
|
human: true,
|
|
},
|
|
{
|
|
name: "human Commander validation",
|
|
args: ["sessions", "--limit", "0"],
|
|
message: '--limit must be a positive integer or "all", for example --limit 25.',
|
|
human: true,
|
|
commander: true,
|
|
},
|
|
{
|
|
name: "JSON route-first validation",
|
|
args: ["sessions", "--json", "--active", "0"],
|
|
message: "--active must be a positive number of minutes, for example --active 30.",
|
|
},
|
|
{
|
|
name: "JSON Commander validation",
|
|
args: ["sessions", "--json", "--limit", "0"],
|
|
message: '--limit must be a positive integer or "all", for example --limit 25.',
|
|
commander: true,
|
|
},
|
|
{
|
|
name: "dual-TTY route-first finalization",
|
|
args: ["sessions", "--json", "--active", "0"],
|
|
message: "--active must be a positive number of minutes, for example --active 30.",
|
|
tty: true,
|
|
},
|
|
{
|
|
name: "trajectory exporter operational failure",
|
|
args: [
|
|
"sessions",
|
|
"export-trajectory",
|
|
"--session-key",
|
|
"agent:main:trajectory-process",
|
|
"--workspace",
|
|
"$TRAJECTORY_WORKSPACE",
|
|
"--json",
|
|
],
|
|
message: "Failed to export trajectory: injected trajectory exporter failure",
|
|
exporterFailure: true,
|
|
},
|
|
{
|
|
name: "compact dual-TTY finalization",
|
|
args: ["sessions", "compact", "agent:main:test", "--max-lines", "0", "--json"],
|
|
message: "--max-lines must be a positive integer.",
|
|
tty: true,
|
|
},
|
|
])("renders sessions $name through the canonical failure owner", async (testCase) => {
|
|
await withTempHome(
|
|
async (tempHome) => {
|
|
if ("exporterFailure" in testCase) {
|
|
await seedTrajectorySession(tempHome, "agent:main:trajectory-process");
|
|
}
|
|
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("AUTOQA_NETWORK_FORBIDDEN"); };',
|
|
...("exporterFailure" in testCase
|
|
? [
|
|
'import fs from "node:fs/promises";',
|
|
"const originalRealpath = fs.realpath;",
|
|
`fs.realpath = async (target, ...args) => { if (target === ${JSON.stringify(tempHome)}) { throw new Error("injected trajectory exporter failure"); } return originalRealpath(target, ...args); };`,
|
|
]
|
|
: []),
|
|
...("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 args = testCase.args.map((arg) => (arg === "$TRAJECTORY_WORKSPACE" ? tempHome : arg));
|
|
const result = runBuiltCli(
|
|
tempHome,
|
|
args,
|
|
{
|
|
OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"),
|
|
OPENCLAW_GATEWAY_PORT: "29791",
|
|
OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"),
|
|
...("commander" in testCase ? { OPENCLAW_DISABLE_ROUTE_FIRST: "1" } : {}),
|
|
...("tty" in testCase ? { FORCE_COLOR: "1" } : {}),
|
|
},
|
|
{ execArgv: [`--import=data:text/javascript;base64,${preload}`] },
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stdout, result.stderr).not.toContain("\u001B");
|
|
expect(result.stdout, result.stderr).not.toContain("\u0007");
|
|
if ("human" in testCase) {
|
|
expect(result.stdout).toBe("");
|
|
} else {
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
ok: false,
|
|
error: { type: "cli_error", message: testCase.message },
|
|
});
|
|
}
|
|
expect(result.stderr).toContain(testCase.message);
|
|
expect(result.stderr.split(testCase.message)).toHaveLength(2);
|
|
expect(result.stderr).not.toContain("AUTOQA_NETWORK_FORBIDDEN");
|
|
if ("tty" in testCase) {
|
|
expect(result.stderr).toContain("\u001B[?25h");
|
|
}
|
|
},
|
|
{ prefix: "openclaw-sessions-registration-json-failure-e2e-" },
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{ name: "direct JSON export", encoded: false, json: true },
|
|
{ name: "encoded request precedence with plain output", encoded: true, json: false },
|
|
])("preserves successful trajectory $name", async (testCase) => {
|
|
await withTempHome(
|
|
async (tempHome) => {
|
|
const sessionKey = "agent:main:trajectory-process";
|
|
await seedTrajectorySession(tempHome, sessionKey);
|
|
const output = testCase.encoded ? "encoded-export" : "direct-export";
|
|
const args = [
|
|
"sessions",
|
|
"export-trajectory",
|
|
"--session-key",
|
|
testCase.encoded ? "agent:main:missing" : sessionKey,
|
|
"--output",
|
|
"direct-export",
|
|
"--workspace",
|
|
tempHome,
|
|
];
|
|
if (testCase.encoded) {
|
|
args.push(
|
|
"--request-json-base64",
|
|
Buffer.from(JSON.stringify({ sessionKey, output }), "utf8").toString("base64url"),
|
|
);
|
|
}
|
|
if (testCase.json) {
|
|
args.push("--json");
|
|
}
|
|
|
|
const result = runBuiltCli(tempHome, args, {
|
|
OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"),
|
|
OPENCLAW_GATEWAY_PORT: "29791",
|
|
OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"),
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
if (testCase.json) {
|
|
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
displayPath: `.openclaw/trajectory-exports/${output}`,
|
|
sessionId: "trajectory-process-session",
|
|
});
|
|
} else {
|
|
expect(result.stdout).toContain("✅ Trajectory exported!");
|
|
expect(result.stdout).toContain(`.openclaw/trajectory-exports/${output}`);
|
|
expect(result.stdout).toContain("trajectory-process-session");
|
|
}
|
|
await expect(
|
|
fs.access(
|
|
path.join(tempHome, ".openclaw", "trajectory-exports", output, "manifest.json"),
|
|
),
|
|
).resolves.toBeUndefined();
|
|
},
|
|
{ prefix: "openclaw-trajectory-success-e2e-" },
|
|
);
|
|
});
|
|
});
|