acpx/test/cli.test.ts
2026-04-25 09:47:51 +01:00

2433 lines
75 KiB
TypeScript

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { readFileSync } from "node:fs";
import fs from "node:fs/promises";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { InvalidArgumentError } from "commander";
import { AGENT_REGISTRY } from "../src/agent-registry.js";
import {
formatPromptSessionBannerLine,
parseAllowedTools,
parseMaxTurns,
parseTtlSeconds,
} from "../src/cli.js";
import { serializeSessionRecordForDisk } from "../src/session/persistence.js";
import type { SessionRecord } from "../src/types.js";
import {
cleanupOwnerArtifacts,
closeServer,
listenServer,
queuePaths,
startKeeperProcess,
stopProcess,
writeQueueOwnerLock,
} from "./queue-test-helpers.js";
const CLI_PATH = fileURLToPath(new URL("../src/cli.js", import.meta.url));
const MOCK_AGENT_PATH = fileURLToPath(new URL("./mock-agent.js", import.meta.url));
function readPackageVersionForTest(): string {
const candidates = [
fileURLToPath(new URL("../package.json", import.meta.url)),
fileURLToPath(new URL("../../package.json", import.meta.url)),
path.join(process.cwd(), "package.json"),
];
for (const candidate of candidates) {
try {
const parsed = JSON.parse(readFileSync(candidate, "utf8")) as {
version?: unknown;
};
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
return parsed.version;
}
} catch {
// continue searching
}
}
throw new Error("package.json version is missing");
}
const PACKAGE_VERSION = readPackageVersionForTest();
const MOCK_AGENT_COMMAND = `node ${JSON.stringify(MOCK_AGENT_PATH)}`;
const MOCK_AGENT_IGNORING_SIGTERM = `${MOCK_AGENT_COMMAND} --ignore-sigterm`;
const MOCK_CODEX_AGENT_WITH_RUNTIME_SESSION_ID = `${MOCK_AGENT_COMMAND} --codex-session-id codex-runtime-session`;
const MOCK_CLAUDE_AGENT_WITH_RUNTIME_SESSION_ID = `${MOCK_AGENT_COMMAND} --claude-session-id claude-runtime-session`;
const MOCK_AGENT_WITH_LOAD_RUNTIME_SESSION_ID = `${MOCK_AGENT_COMMAND} --supports-load-session --load-runtime-session-id loaded-runtime-session`;
const MOCK_AGENT_WITH_DISTINCT_CREATE_AND_LOAD_RUNTIME_SESSION_IDS =
`${MOCK_AGENT_COMMAND} --runtime-session-id fresh-runtime-session ` +
"--supports-load-session --load-runtime-session-id resumed-runtime-session";
const MOCK_AGENT_WITH_LOAD_FALLBACK = `${MOCK_AGENT_COMMAND} --supports-load-session --load-session-fails-on-empty`;
const MOCK_AGENT_WITH_LOAD_SESSION_NOT_FOUND = `${MOCK_AGENT_COMMAND} --supports-load-session --load-session-not-found`;
const MOCK_AGENT_WITH_LOAD_FALLBACK_AND_MODE_FAILURE = `${MOCK_AGENT_COMMAND} --supports-load-session --load-session-fails-on-empty --set-session-mode-fails`;
const MOCK_AGENT_WITH_SET_MODE_INVALID_PARAMS = `${MOCK_AGENT_COMMAND} --set-session-mode-invalid-params`;
const MOCK_AGENT_WITH_SET_CONFIG_INVALID_PARAMS = `${MOCK_AGENT_COMMAND} --set-session-config-invalid-params`;
type CliRunResult = {
code: number | null;
stdout: string;
stderr: string;
};
type ParsedAcpError = {
code?: number;
message?: string;
data?: Record<string, unknown> & {
acpxCode?: string;
detailCode?: string;
origin?: string;
sessionId?: string;
};
};
test("CLI --version prints package version", async () => {
await withTempHome(async (homeDir) => {
const result = await runCli(["--version"], homeDir);
assert.equal(result.code, 0, result.stderr);
assert.equal(result.stderr.trim(), "");
assert.equal(result.stdout.trim(), PACKAGE_VERSION);
});
});
function parseSingleAcpErrorLine(stdout: string): ParsedAcpError {
const payload = JSON.parse(stdout.trim()) as {
jsonrpc?: string;
error?: ParsedAcpError;
};
assert.equal(payload.jsonrpc, "2.0");
assert.equal(typeof payload.error, "object");
return payload.error ?? {};
}
function parseJsonRpcLines(stdout: string): Array<Record<string, unknown>> {
const lines = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);
assert(lines.length > 0, "expected at least one stdout line");
return lines.map((line) => {
const parsed = JSON.parse(line) as Record<string, unknown>;
assert.equal(parsed.jsonrpc, "2.0");
return parsed;
});
}
test("parseTtlSeconds parses and rounds valid numeric values", () => {
assert.equal(parseTtlSeconds("30"), 30_000);
assert.equal(parseTtlSeconds("0"), 0);
assert.equal(parseTtlSeconds("1.49"), 1_490);
});
test("parseTtlSeconds rejects non-numeric values", () => {
assert.throws(() => parseTtlSeconds("abc"), InvalidArgumentError);
});
test("parseTtlSeconds rejects negative values", () => {
assert.throws(() => parseTtlSeconds("-1"), InvalidArgumentError);
});
test("parseAllowedTools parses empty and comma-separated values", () => {
assert.deepEqual(parseAllowedTools(""), []);
assert.deepEqual(parseAllowedTools("Read,Grep, Glob"), ["Read", "Grep", "Glob"]);
});
test("parseAllowedTools rejects empty entries", () => {
assert.throws(() => parseAllowedTools("Read,,Grep"), InvalidArgumentError);
});
test("parseMaxTurns accepts positive integers and rejects invalid values", () => {
assert.equal(parseMaxTurns("3"), 3);
assert.throws(() => parseMaxTurns("0"), InvalidArgumentError);
assert.throws(() => parseMaxTurns("1.5"), InvalidArgumentError);
});
test("formatPromptSessionBannerLine prints single-line prompt banner for matching cwd", () => {
const record = makeSessionRecord({
acpxRecordId: "abc123",
acpSessionId: "abc123",
agentCommand: "agent-a",
cwd: "/home/user/project",
name: "calm-forest",
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const line = formatPromptSessionBannerLine(record, "/home/user/project");
assert.equal(
line,
"[acpx] session calm-forest (abc123) · /home/user/project · agent needs reconnect",
);
});
test("formatPromptSessionBannerLine includes routed-from path when cwd differs", () => {
const record = makeSessionRecord({
acpxRecordId: "abc123",
acpSessionId: "abc123",
agentCommand: "agent-a",
cwd: "/home/user/project",
name: "calm-forest",
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const line = formatPromptSessionBannerLine(record, "/home/user/project/src/auth");
assert.equal(
line,
"[acpx] session calm-forest (abc123) · /home/user/project (routed from ./src/auth) · agent needs reconnect",
);
});
test("CLI resolves unknown subcommand names as raw agent commands", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const session = makeSessionRecord({
acpxRecordId: "custom-session",
acpSessionId: "custom-session",
agentCommand: "custom-agent",
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
await writeSessionRecord(homeDir, session);
const result = await runCli(
["--cwd", cwd, "--format", "quiet", "custom-agent", "sessions"],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /custom-session/);
});
});
test("global passthrough flags are present in help output", async () => {
await withTempHome(async (homeDir) => {
const result = await runCli(["--help"], homeDir);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /--model <id>/);
assert.match(result.stdout, /--allowed-tools <list>/);
assert.match(result.stdout, /--max-turns <count>/);
assert.match(result.stdout, /text, json, quiet/);
assert.match(result.stdout, /--suppress-reads/);
assert.match(result.stdout, /--no-terminal/);
});
});
test("sessions new command is present in help output", async () => {
await withTempHome(async (homeDir) => {
const result = await runCli(["sessions", "--help"], homeDir);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /\bnew\b/);
assert.match(result.stdout, /\bensure\b/);
assert.match(result.stdout, /\bread\b/);
assert.match(result.stdout, /\bprune\b/);
const newHelp = await runCli(["sessions", "new", "--help"], homeDir);
assert.equal(newHelp.code, 0, newHelp.stderr);
assert.match(newHelp.stdout, /--name <name>/);
assert.match(newHelp.stdout, /--resume-session <id>/);
const ensureHelp = await runCli(["sessions", "ensure", "--help"], homeDir);
assert.equal(ensureHelp.code, 0, ensureHelp.stderr);
assert.match(ensureHelp.stdout, /--name <name>/);
const readHelp = await runCli(["sessions", "read", "--help"], homeDir);
assert.equal(readHelp.code, 0, readHelp.stderr);
assert.match(readHelp.stdout, /--tail <count>/);
assert.match(ensureHelp.stdout, /--resume-session <id>/);
const pruneHelp = await runCli(["sessions", "prune", "--help"], homeDir);
assert.equal(pruneHelp.code, 0, pruneHelp.stderr);
assert.match(pruneHelp.stdout, /--dry-run/);
assert.match(pruneHelp.stdout, /--include-history/);
});
});
test("flow run command is present in help output", async () => {
await withTempHome(async (homeDir) => {
const flowHelp = await runCli(["flow", "--help"], homeDir);
assert.equal(flowHelp.code, 0, flowHelp.stderr);
assert.match(flowHelp.stdout, /\brun\b/);
const runHelp = await runCli(["flow", "run", "--help"], homeDir);
assert.equal(runHelp.code, 0, runHelp.stderr);
assert.match(runHelp.stdout, /--input-json <json>/);
assert.match(runHelp.stdout, /--input-file <path>/);
assert.match(runHelp.stdout, /--default-agent <name>/);
});
});
test("sessions new --resume-session loads ACP session and stores resumed ids", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_WITH_DISTINCT_CREATE_AND_LOAD_RUNTIME_SESSION_IDS,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const resumeSessionId = "cs_resume123";
const result = await runCli(
[
"--cwd",
cwd,
"--format",
"json",
"codex",
"sessions",
"new",
"--resume-session",
resumeSessionId,
],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as {
action?: unknown;
created?: unknown;
acpxRecordId?: unknown;
acpxSessionId?: unknown;
agentSessionId?: unknown;
};
assert.equal(payload.action, "session_ensured");
assert.equal(payload.created, true);
assert.equal(payload.acpxRecordId, resumeSessionId);
assert.equal(payload.acpxSessionId, resumeSessionId);
assert.equal(payload.agentSessionId, "resumed-runtime-session");
const storedRecordPath = path.join(
homeDir,
".acpx",
"sessions",
`${encodeURIComponent(resumeSessionId)}.json`,
);
const storedRecord = JSON.parse(await fs.readFile(storedRecordPath, "utf8")) as {
acp_session_id?: unknown;
agent_session_id?: unknown;
};
assert.equal(storedRecord.acp_session_id, resumeSessionId);
assert.equal(storedRecord.agent_session_id, "resumed-runtime-session");
});
});
test("sessions new --resume-session fails when agent does not support session/load", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_COMMAND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const result = await runCli(
["--cwd", cwd, "codex", "sessions", "new", "--resume-session", "cs_unsupported"],
homeDir,
);
assert.equal(result.code, 1, result.stderr);
assert.match(result.stderr, /does not support session\/load/i);
});
});
test("sessions new --resume-session surfaces not-found loadSession errors without fallback", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_WITH_LOAD_SESSION_NOT_FOUND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const resumeSessionId = "cs_missing";
const result = await runCli(
["--cwd", cwd, "codex", "sessions", "new", "--resume-session", resumeSessionId],
homeDir,
);
assert.equal(result.code, 4, result.stderr);
assert.match(result.stderr, /Failed to resume ACP session cs_missing: Resource not found/);
const sessionsDir = path.join(homeDir, ".acpx", "sessions");
const entries = await fs.readdir(sessionsDir).catch(() => [] as string[]);
assert.equal(entries.includes(`${encodeURIComponent(resumeSessionId)}.json`), false);
});
});
test("sessions ensure creates when missing and returns existing on subsequent calls", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_COMMAND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const first = await runCli(
["--cwd", cwd, "--format", "json", "codex", "sessions", "ensure"],
homeDir,
);
assert.equal(first.code, 0, first.stderr);
const firstPayload = JSON.parse(first.stdout.trim()) as Record<string, unknown>;
assert.equal(firstPayload.action, "session_ensured");
assert.equal(firstPayload.created, true);
const second = await runCli(
["--cwd", cwd, "--format", "json", "codex", "sessions", "ensure"],
homeDir,
);
assert.equal(second.code, 0, second.stderr);
const secondPayload = JSON.parse(second.stdout.trim()) as Record<string, unknown>;
assert.equal(secondPayload.action, "session_ensured");
assert.equal(secondPayload.created, false);
assert.equal(secondPayload.acpxRecordId, firstPayload.acpxRecordId);
});
});
test("sessions ensure --resume-session loads ACP session when creating missing session", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_WITH_DISTINCT_CREATE_AND_LOAD_RUNTIME_SESSION_IDS,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const resumeSessionId = "cs_ensure_resume";
const result = await runCli(
[
"--cwd",
cwd,
"--format",
"json",
"codex",
"sessions",
"ensure",
"--resume-session",
resumeSessionId,
],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as {
created?: unknown;
acpxRecordId?: unknown;
acpxSessionId?: unknown;
agentSessionId?: unknown;
};
assert.equal(payload.created, true);
assert.equal(payload.acpxRecordId, resumeSessionId);
assert.equal(payload.acpxSessionId, resumeSessionId);
assert.equal(payload.agentSessionId, "resumed-runtime-session");
});
});
test("sessions ensure exits even when agent ignores SIGTERM", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_IGNORING_SIGTERM,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "sessions", "ensure"],
homeDir,
{ timeoutMs: 8_000 },
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as {
action?: unknown;
created?: unknown;
acpxRecordId?: unknown;
};
assert.equal(payload.action, "session_ensured");
assert.equal(payload.created, true);
assert.equal(typeof payload.acpxRecordId, "string");
const storedRecord = JSON.parse(
await fs.readFile(
path.join(
homeDir,
".acpx",
"sessions",
`${encodeURIComponent(payload.acpxRecordId as string)}.json`,
),
"utf8",
),
) as SessionRecord;
if (storedRecord.pid != null) {
const exited = await waitForPidExit(storedRecord.pid, 2_000);
assert.equal(exited, true);
}
});
});
test("sessions ensure resolves existing session by directory walk", async () => {
await withTempHome(async (homeDir) => {
const root = path.join(homeDir, "workspace");
const child = path.join(root, "packages", "app");
await fs.mkdir(child, { recursive: true });
await fs.mkdir(path.join(root, ".git"), { recursive: true });
await writeSessionRecord(homeDir, {
acpxRecordId: "parent-session",
acpSessionId: "parent-session",
agentCommand: AGENT_REGISTRY.codex,
cwd: root,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(
["--cwd", child, "--format", "json", "codex", "sessions", "ensure"],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as Record<string, unknown>;
assert.equal(payload.acpxRecordId, "parent-session");
assert.equal(payload.action, "session_ensured");
assert.equal(payload.created, false);
});
});
test("sessions and status surface agentSessionId for codex and claude in JSON mode", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const runtimeScenarios = [
{
agentName: "codex",
command: MOCK_CODEX_AGENT_WITH_RUNTIME_SESSION_ID,
expectedRuntimeSessionId: "codex-runtime-session",
},
{
agentName: "claude",
command: MOCK_CLAUDE_AGENT_WITH_RUNTIME_SESSION_ID,
expectedRuntimeSessionId: "claude-runtime-session",
},
] as const;
const agentsConfig = Object.fromEntries(
runtimeScenarios.map((scenario) => [scenario.agentName, { command: scenario.command }]),
);
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: agentsConfig,
},
null,
2,
)}\n`,
"utf8",
);
for (const scenario of runtimeScenarios) {
const created = await runCli(
["--cwd", cwd, "--format", "json", scenario.agentName, "sessions", "new"],
homeDir,
);
assert.equal(created.code, 0, created.stderr);
const createdPayload = JSON.parse(created.stdout.trim()) as Record<string, unknown>;
assert.equal(createdPayload.action, "session_ensured");
assert.equal(createdPayload.created, true);
assert.equal(createdPayload.agentSessionId, scenario.expectedRuntimeSessionId);
const ensured = await runCli(
["--cwd", cwd, "--format", "json", scenario.agentName, "sessions", "ensure"],
homeDir,
);
assert.equal(ensured.code, 0, ensured.stderr);
const ensuredPayload = JSON.parse(ensured.stdout.trim()) as Record<string, unknown>;
assert.equal(ensuredPayload.action, "session_ensured");
assert.equal(ensuredPayload.created, false);
assert.equal(ensuredPayload.agentSessionId, scenario.expectedRuntimeSessionId);
const status = await runCli(
["--cwd", cwd, "--format", "json", scenario.agentName, "status"],
homeDir,
);
assert.equal(status.code, 0, status.stderr);
const statusPayload = JSON.parse(status.stdout.trim()) as Record<string, unknown>;
assert.equal(statusPayload.action, "status_snapshot");
assert.equal(statusPayload.agentSessionId, scenario.expectedRuntimeSessionId);
}
});
});
test("prompt reconciles agentSessionId from loadSession metadata", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_WITH_LOAD_RUNTIME_SESSION_ID,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const sessionId = "resume-runtime-session";
await writeSessionRecord(homeDir, {
acpxRecordId: sessionId,
acpSessionId: sessionId,
agentCommand: MOCK_AGENT_WITH_LOAD_RUNTIME_SESSION_ID,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const prompt = await runCli(
["--cwd", cwd, "--ttl", "0.01", "codex", "prompt", "echo hello"],
homeDir,
);
assert.equal(prompt.code, 0, prompt.stderr);
const storedRecordPath = path.join(
homeDir,
".acpx",
"sessions",
`${encodeURIComponent(sessionId)}.json`,
);
const storedRecord = JSON.parse(await fs.readFile(storedRecordPath, "utf8")) as Record<
string,
unknown
>;
assert.equal(storedRecord.agent_session_id, "loaded-runtime-session");
});
});
test("set-mode persists across load fallback and replays on fresh ACP sessions", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_WITH_LOAD_FALLBACK,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const sessionId = "mode-replay-session";
await writeSessionRecord(homeDir, {
acpxRecordId: sessionId,
acpSessionId: sessionId,
agentCommand: MOCK_AGENT_WITH_LOAD_FALLBACK,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const setPlan = await runCli(
["--cwd", cwd, "--format", "json", "codex", "set-mode", "plan"],
homeDir,
);
assert.equal(setPlan.code, 0, setPlan.stderr);
const setPlanPayload = JSON.parse(setPlan.stdout.trim()) as {
acpxSessionId?: unknown;
};
const checkPlan = await runCli(
["--cwd", cwd, "--format", "json", "codex", "set", "reasoning_effort", "high"],
homeDir,
);
assert.equal(checkPlan.code, 0, checkPlan.stderr);
const checkPlanPayload = JSON.parse(checkPlan.stdout.trim()) as {
acpxSessionId?: unknown;
configOptions?: Array<{ id?: string; currentValue?: string }>;
};
const modeAfterPlan =
checkPlanPayload.configOptions?.find((option) => option.id === "mode")?.currentValue ?? "";
assert.equal(modeAfterPlan, "plan");
assert.notEqual(checkPlanPayload.acpxSessionId, setPlanPayload.acpxSessionId);
const setAuto = await runCli(
["--cwd", cwd, "--format", "json", "codex", "set-mode", "auto"],
homeDir,
);
assert.equal(setAuto.code, 0, setAuto.stderr);
const setAutoPayload = JSON.parse(setAuto.stdout.trim()) as {
acpxSessionId?: unknown;
};
const checkAuto = await runCli(
["--cwd", cwd, "--format", "json", "codex", "set", "reasoning_effort", "medium"],
homeDir,
);
assert.equal(checkAuto.code, 0, checkAuto.stderr);
const checkAutoPayload = JSON.parse(checkAuto.stdout.trim()) as {
acpxSessionId?: unknown;
configOptions?: Array<{ id?: string; currentValue?: string }>;
};
const modeAfterAuto =
checkAutoPayload.configOptions?.find((option) => option.id === "mode")?.currentValue ?? "";
assert.equal(modeAfterAuto, "auto");
assert.notEqual(checkAutoPayload.acpxSessionId, setAutoPayload.acpxSessionId);
const storedRecordPath = path.join(
homeDir,
".acpx",
"sessions",
`${encodeURIComponent(sessionId)}.json`,
);
const storedRecord = JSON.parse(await fs.readFile(storedRecordPath, "utf8")) as {
acpx?: {
desired_mode_id?: string;
};
};
assert.equal(storedRecord.acpx?.desired_mode_id, "auto");
});
});
test("codex thought_level aliases to reasoning_effort", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_COMMAND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const sessionId = "codex-thought-level-alias";
await writeSessionRecord(homeDir, {
acpxRecordId: sessionId,
acpSessionId: sessionId,
agentCommand: MOCK_AGENT_COMMAND,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "set", "thought_level", "high"],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as {
action?: string;
configId?: string;
value?: string;
configOptions?: Array<{ id?: string; currentValue?: string; category?: string }>;
};
assert.equal(payload.action, "config_set");
assert.equal(payload.configId, "thought_level");
assert.equal(payload.value, "high");
const reasoningEffort = payload.configOptions?.find(
(option) => option.id === "reasoning_effort",
);
assert.equal(reasoningEffort?.currentValue, "high");
assert.equal(reasoningEffort?.category, "thought_level");
});
});
test("codex set model passes the requested model through unchanged", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_COMMAND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const sessionId = "codex-model-alias";
await writeSessionRecord(homeDir, {
acpxRecordId: sessionId,
acpSessionId: sessionId,
agentCommand: MOCK_AGENT_COMMAND,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "set", "model", "GPT-5-2"],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as {
action?: string;
modelId?: string;
};
assert.equal(payload.action, "model_set");
assert.equal(payload.modelId, "GPT-5-2");
});
});
test("set-mode load fallback failure does not persist the fresh session id to disk", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_WITH_LOAD_FALLBACK_AND_MODE_FAILURE,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const sessionId = "mode-replay-session";
await writeSessionRecord(homeDir, {
acpxRecordId: sessionId,
acpSessionId: sessionId,
agentCommand: MOCK_AGENT_WITH_LOAD_FALLBACK_AND_MODE_FAILURE,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
acpx: {
desired_mode_id: "plan",
},
});
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "set-mode", "plan"],
homeDir,
);
assert.equal(result.code, 1, result.stderr);
const error = parseSingleAcpErrorLine(result.stdout);
assert.equal(error.data?.acpxCode, "RUNTIME");
assert.equal(error.data?.detailCode, "SESSION_MODE_REPLAY_FAILED");
const storedRecordPath = path.join(
homeDir,
".acpx",
"sessions",
`${encodeURIComponent(sessionId)}.json`,
);
const storedRecord = JSON.parse(await fs.readFile(storedRecordPath, "utf8")) as {
acp_session_id?: string;
acpx?: {
desired_mode_id?: string;
};
};
assert.equal(storedRecord.acp_session_id, sessionId);
assert.equal(storedRecord.acpx?.desired_mode_id, "plan");
});
});
test("set-mode surfaces actionable guidance when agent rejects session/set_mode params", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_WITH_SET_MODE_INVALID_PARAMS,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const sessionId = "set-mode-invalid-params";
await writeSessionRecord(homeDir, {
acpxRecordId: sessionId,
acpSessionId: sessionId,
agentCommand: MOCK_AGENT_WITH_SET_MODE_INVALID_PARAMS,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "set-mode", "plan"],
homeDir,
);
assert.equal(result.code, 1, result.stderr);
const error = parseSingleAcpErrorLine(result.stdout);
assert.equal(typeof error.code, "number");
assert.match(error.message ?? "", /Internal error|session\/set_mode/);
});
});
test("set returns an error when agent rejects unsupported session config params", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_WITH_SET_CONFIG_INVALID_PARAMS,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const sessionId = "set-config-invalid-params";
await writeSessionRecord(homeDir, {
acpxRecordId: sessionId,
acpSessionId: sessionId,
agentCommand: MOCK_AGENT_WITH_SET_CONFIG_INVALID_PARAMS,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "set", "approval_policy", "strict"],
homeDir,
);
assert.equal(result.code, 1, result.stderr);
const error = parseSingleAcpErrorLine(result.stdout);
assert.equal(typeof error.code, "number");
assert.match(error.message ?? "", /Internal error|session\/set_config_option/);
});
});
test("--ttl flag is parsed for sessions commands", async () => {
await withTempHome(async (homeDir) => {
const ok = await runCli(["--ttl", "30", "--format", "json", "sessions"], homeDir);
assert.equal(ok.code, 0, ok.stderr);
assert.doesNotThrow(() => JSON.parse(ok.stdout.trim()));
const invalid = await runCli(["--ttl", "bad", "sessions"], homeDir);
assert.equal(invalid.code, 2);
assert.match(invalid.stderr, /TTL must be a non-negative number of seconds/);
const negative = await runCli(["--ttl", "-1", "sessions"], homeDir);
assert.equal(negative.code, 2);
assert.match(negative.stderr, /TTL must be a non-negative number of seconds/);
});
});
test("--auth-policy flag validates supported values", async () => {
await withTempHome(async (homeDir) => {
const ok = await runCli(["--auth-policy", "skip", "--format", "json", "sessions"], homeDir);
assert.equal(ok.code, 0, ok.stderr);
const invalid = await runCli(["--auth-policy", "bad", "sessions"], homeDir);
assert.equal(invalid.code, 2);
assert.match(invalid.stderr, /Invalid auth policy/);
});
});
test("--non-interactive-permissions validates supported values", async () => {
await withTempHome(async (homeDir) => {
const ok = await runCli(
["--non-interactive-permissions", "deny", "--format", "json", "sessions"],
homeDir,
);
assert.equal(ok.code, 0, ok.stderr);
const invalid = await runCli(
["--format", "json", "--non-interactive-permissions", "bad", "sessions"],
homeDir,
);
assert.equal(invalid.code, 2);
const error = parseSingleAcpErrorLine(invalid.stdout);
assert.equal(error.code, -32602);
assert.equal(error.data?.acpxCode, "USAGE");
assert.match(error.message ?? "", /Invalid non-interactive permission policy/);
});
});
test("--json-strict requires --format json", async () => {
await withTempHome(async (homeDir) => {
const result = await runCli(["--json-strict", "sessions"], homeDir);
assert.equal(result.code, 2);
assert.equal(result.stderr.trim(), "");
const error = parseSingleAcpErrorLine(result.stdout);
assert.equal(error.code, -32602);
assert.equal(error.data?.acpxCode, "USAGE");
assert.match(error.message ?? "", /--json-strict requires --format json/);
});
});
test("--json-strict rejects --verbose", async () => {
await withTempHome(async (homeDir) => {
const result = await runCli(
["--format", "json", "--json-strict", "--verbose", "sessions"],
homeDir,
);
assert.equal(result.code, 2);
assert.equal(result.stderr.trim(), "");
const error = parseSingleAcpErrorLine(result.stdout);
assert.equal(error.code, -32602);
assert.equal(error.data?.acpxCode, "USAGE");
assert.match(error.message ?? "", /--json-strict cannot be combined with --verbose/);
});
});
test("queued prompt failures emit exactly one JSON error event", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_COMMAND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const session = await runCli(
["--cwd", cwd, "--format", "json", "codex", "sessions", "new"],
homeDir,
);
assert.equal(session.code, 0, session.stderr);
const blocker = spawn(
process.execPath,
[CLI_PATH, "--cwd", cwd, "codex", "prompt", "sleep 1500"],
{
env: { ...process.env, HOME: homeDir },
stdio: ["ignore", "ignore", "ignore"],
},
);
try {
await new Promise<void>((resolve) => {
setTimeout(resolve, 200);
});
const writeResult = await runCli(
[
"--cwd",
cwd,
"--format",
"json",
"--non-interactive-permissions",
"fail",
"codex",
"prompt",
`write ${path.join(cwd, "x.txt")} hi`,
],
homeDir,
);
assert.equal(writeResult.code, 5, writeResult.stderr);
const events = writeResult.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line) => JSON.parse(line) as Record<string, unknown>);
const errors = events.filter(
(event) => typeof event.error === "object" && event.error !== null,
);
assert.equal(errors.length, 1, writeResult.stdout);
assert.equal((errors[0]?.error as { code?: unknown } | undefined)?.code, -32603);
assert.notEqual(
(errors[0]?.error as { data?: { sessionId?: unknown } } | undefined)?.data?.sessionId,
"unknown",
);
} finally {
if (blocker.exitCode === null && blocker.signalCode == null) {
blocker.kill("SIGKILL");
await new Promise<void>((resolve) => {
blocker.once("close", () => resolve());
});
}
}
});
});
test("json-strict queued prompt failure emits JSON-RPC lines only", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_COMMAND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const session = await runCli(
["--cwd", cwd, "--format", "json", "codex", "sessions", "new"],
homeDir,
);
assert.equal(session.code, 0, session.stderr);
const blocker = spawn(
process.execPath,
[CLI_PATH, "--cwd", cwd, "codex", "prompt", "sleep 1500"],
{
env: { ...process.env, HOME: homeDir },
stdio: ["ignore", "ignore", "ignore"],
},
);
try {
await new Promise<void>((resolve) => {
setTimeout(resolve, 200);
});
const writeResult = await runCli(
[
"--cwd",
cwd,
"--format",
"json",
"--json-strict",
"--non-interactive-permissions",
"fail",
"codex",
"prompt",
`write ${path.join(cwd, "x.txt")} hi`,
],
homeDir,
);
assert.equal(writeResult.code, 5, writeResult.stderr);
assert.equal(writeResult.stderr.trim(), "");
const events = parseJsonRpcLines(writeResult.stdout);
assert.equal(
events.some(
(event) =>
typeof event.error === "object" &&
event.error !== null &&
typeof (event.error as { code?: unknown }).code === "number",
),
true,
);
} finally {
if (blocker.exitCode === null && blocker.signalCode == null) {
blocker.kill("SIGKILL");
await new Promise<void>((resolve) => {
blocker.once("close", () => resolve());
});
}
}
});
});
test("queued prompt failures remain visible in quiet mode", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_COMMAND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const session = await runCli(
["--cwd", cwd, "--format", "json", "codex", "sessions", "new"],
homeDir,
);
assert.equal(session.code, 0, session.stderr);
const blocker = spawn(
process.execPath,
[CLI_PATH, "--cwd", cwd, "codex", "prompt", "sleep 1500"],
{
env: { ...process.env, HOME: homeDir },
stdio: ["ignore", "ignore", "ignore"],
},
);
try {
await new Promise<void>((resolve) => {
setTimeout(resolve, 200);
});
const writeResult = await runCli(
[
"--cwd",
cwd,
"--format",
"quiet",
"--non-interactive-permissions",
"fail",
"codex",
"prompt",
`write ${path.join(cwd, "x.txt")} hi`,
],
homeDir,
);
assert.equal(writeResult.code, 5);
assert.match(writeResult.stdout, /error:\s*Internal error/i);
assert.match(writeResult.stderr, /Permission prompt unavailable in non-interactive mode/);
} finally {
if (blocker.exitCode === null && blocker.signalCode == null) {
blocker.kill("SIGKILL");
await new Promise<void>((resolve) => {
blocker.once("close", () => resolve());
});
}
}
});
});
test("non-queued write permission denial exits with code 5", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_COMMAND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const session = await runCli(
["--cwd", cwd, "--format", "json", "codex", "sessions", "new"],
homeDir,
);
assert.equal(session.code, 0, session.stderr);
const writeResult = await runCli(
[
"--cwd",
cwd,
"--format",
"quiet",
"--approve-reads",
"codex",
"prompt",
`write ${path.join(cwd, "x.txt")} hi`,
],
homeDir,
);
assert.equal(writeResult.code, 5);
assert.match(writeResult.stdout, /error:\s*Internal error/i);
});
});
test("--json-strict suppresses session banners on stderr", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: {
command: MOCK_AGENT_COMMAND,
},
},
},
null,
2,
)}\n`,
"utf8",
);
const result = await runCli(
["--cwd", cwd, "--format", "json", "--json-strict", "codex", "sessions", "new"],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
assert.equal(result.stderr.trim(), "");
const payload = JSON.parse(result.stdout.trim()) as Record<string, unknown>;
assert.equal(payload.action, "session_ensured");
assert.equal(payload.created, true);
assert.equal(typeof payload.acpxRecordId, "string");
});
});
test("prompt exits with NO_SESSION when no session exists (no auto-create)", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace", "packages", "app");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(["--cwd", cwd, "codex", "hello"], homeDir);
assert.equal(result.code, 4);
const escapedCwd = escapeRegex(cwd);
assert.match(
result.stderr,
new RegExp(
`⚠ No acpx session found \\(searched up to ${escapedCwd}\\)\\.\\nCreate one: acpx codex sessions new\\n?`,
),
);
});
});
test("json format emits structured no-session error event", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(["--cwd", cwd, "--format", "json", "codex", "hello"], homeDir);
assert.equal(result.code, 4);
const error = parseSingleAcpErrorLine(result.stdout);
assert.equal(error.code, -32002);
assert.equal(error.data?.acpxCode, "NO_SESSION");
assert.match(error.message ?? "", /No acpx session found/);
});
});
test("set-mode exits with NO_SESSION when no session exists", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace", "packages", "app");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(["--cwd", cwd, "codex", "set-mode", "plan"], homeDir);
assert.equal(result.code, 4);
assert.match(result.stderr, /No acpx session found/);
});
});
test("set command exits with NO_SESSION when no session exists", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace", "packages", "app");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(["--cwd", cwd, "codex", "set", "temperature", "high"], homeDir);
assert.equal(result.code, 4);
assert.match(result.stderr, /No acpx session found/);
});
});
test("cancel prints nothing to cancel and exits success when no session exists", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace", "packages", "app");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(["--cwd", cwd, "codex", "cancel"], homeDir);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /nothing to cancel/);
});
});
test("cancel resolves named session when -s is before subcommand", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await writeSessionRecord(homeDir, {
acpxRecordId: "named-cancel-session",
acpSessionId: "named-cancel-session",
agentCommand: AGENT_REGISTRY.codex,
cwd,
name: "named",
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "-s", "named", "cancel"],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as Record<string, unknown>;
assert.equal(payload.action, "cancel_result");
assert.equal(payload.acpxRecordId, "named-cancel-session");
assert.equal(payload.cancelled, false);
});
});
test("status resolves named session when -s is before subcommand", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await writeSessionRecord(homeDir, {
acpxRecordId: "named-status-session",
acpSessionId: "named-status-session",
agentCommand: AGENT_REGISTRY.codex,
cwd,
name: "named",
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "-s", "named", "status"],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as Record<string, unknown>;
assert.equal(payload.action, "status_snapshot");
assert.equal(payload.acpxRecordId, "named-status-session");
assert.equal(payload.status, "idle");
assert.equal(payload.summary, "session idle; queue owner will start on next prompt");
assert.notEqual(payload.status, "no-session");
assert.equal(payload.agentSessionId, undefined);
});
});
test("status reports idle for resumable sessions without a live queue owner", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await writeSessionRecord(homeDir, {
acpxRecordId: "idle-status-session",
acpSessionId: "idle-status-session",
agentCommand: AGENT_REGISTRY.codex,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:01:00.000Z",
lastPromptAt: "2026-01-01T00:01:00.000Z",
closed: false,
pid: 12345,
agentStartedAt: "2026-01-01T00:00:00.000Z",
lastAgentExitCode: 0,
lastAgentExitAt: "2026-01-01T00:02:00.000Z",
});
const json = await runCli(["--cwd", cwd, "--format", "json", "codex", "status"], homeDir);
assert.equal(json.code, 0, json.stderr);
const payload = JSON.parse(json.stdout.trim()) as Record<string, unknown>;
assert.equal(payload.action, "status_snapshot");
assert.equal(payload.status, "idle");
assert.equal(payload.summary, "session idle; queue owner will start on next prompt");
assert.equal(payload.exitCode, undefined);
const text = await runCli(["--cwd", cwd, "codex", "status"], homeDir);
assert.equal(text.code, 0, text.stderr);
assert.match(text.stdout, /status: idle/);
assert.doesNotMatch(text.stdout, /exitCode:/);
});
});
test("set-mode resolves named session when -s is before subcommand", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
const missingAgentCommand = "acpx-test-missing-agent-binary";
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: { command: missingAgentCommand },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeSessionRecord(homeDir, {
acpxRecordId: "named-set-mode-session",
acpSessionId: "named-set-mode-session",
agentCommand: missingAgentCommand,
cwd,
name: "named",
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(
["--cwd", cwd, "codex", "-s", "named", "set-mode", "plan"],
homeDir,
);
assert.equal(result.code, 1);
assert.doesNotMatch(result.stderr, /No acpx session found/);
assert.match(result.stderr, /ENOENT|spawn|not found/i);
});
});
test("set resolves named session when -s is before subcommand", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
const missingAgentCommand = "acpx-test-missing-agent-binary-2";
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
agents: {
codex: { command: missingAgentCommand },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeSessionRecord(homeDir, {
acpxRecordId: "named-set-config-session",
acpSessionId: "named-set-config-session",
agentCommand: missingAgentCommand,
cwd,
name: "named",
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(
["--cwd", cwd, "codex", "-s", "named", "set", "approval_policy", "strict"],
homeDir,
);
assert.equal(result.code, 1);
assert.doesNotMatch(result.stderr, /No acpx session found/);
assert.match(result.stderr, /ENOENT|spawn|not found/i);
});
});
test("prompt reads from stdin when no prompt argument is provided", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(["--cwd", cwd, "codex"], homeDir, {
stdin: "fix the tests\n",
});
assert.equal(result.code, 4);
assert.match(result.stderr, /No acpx session found/);
assert.doesNotMatch(result.stderr, /Prompt is required/);
});
});
test("prompt reads from --file for persistent prompts", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.writeFile(path.join(cwd, "prompt.md"), "fix the tests\n", "utf8");
const result = await runCli(["--cwd", cwd, "codex", "--file", "prompt.md"], homeDir);
assert.equal(result.code, 4);
assert.match(result.stderr, /No acpx session found/);
assert.doesNotMatch(result.stderr, /Prompt is required/);
});
});
test("prompt supports --file - with additional argument text", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(
["--cwd", cwd, "codex", "--file", "-", "additional context"],
homeDir,
{ stdin: "from stdin\n" },
);
assert.equal(result.code, 4);
assert.match(result.stderr, /No acpx session found/);
assert.doesNotMatch(result.stderr, /Prompt is required/);
});
});
test("exec accepts structured ACP prompt blocks from stdin", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(
["--agent", MOCK_AGENT_COMMAND, "--cwd", cwd, "--format", "quiet", "exec"],
homeDir,
{
stdin: JSON.stringify([
{ type: "text", text: "inspect-prompt" },
{ type: "image", mimeType: "image/png", data: "aW1hZ2U=" },
]),
},
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as Array<Record<string, unknown>>;
assert.deepEqual(payload, [
{ type: "text", text: "inspect-prompt" },
{ type: "image", mimeType: "image/png", bytes: 8 },
]);
});
});
test("prompt preserves structured ACP prompt blocks through the queue owner", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const created = await runCli(
["--agent", MOCK_AGENT_COMMAND, "--cwd", cwd, "sessions", "new"],
homeDir,
);
assert.equal(created.code, 0, created.stderr);
const result = await runCli(
["--agent", MOCK_AGENT_COMMAND, "--cwd", cwd, "--format", "quiet", "prompt"],
homeDir,
{
stdin: JSON.stringify([
{ type: "text", text: "inspect-prompt" },
{ type: "image", mimeType: "image/png", data: "aW1hZ2U=" },
]),
},
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as Array<Record<string, unknown>>;
assert.deepEqual(payload, [
{ type: "text", text: "inspect-prompt" },
{ type: "image", mimeType: "image/png", bytes: 8 },
]);
});
});
test("exec rejects structured image prompts with invalid mime types", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(
["--agent", MOCK_AGENT_COMMAND, "--cwd", cwd, "--format", "quiet", "exec"],
homeDir,
{
stdin: JSON.stringify([
{ type: "text", text: "inspect-prompt" },
{ type: "image", mimeType: "application/json", data: "aW1hZ2U=" },
]),
},
);
assert.equal(result.code, 2);
assert.match(
`${result.stdout}\n${result.stderr}`,
/image block mimeType must start with image\//i,
);
});
});
test("exec rejects structured image prompts with invalid base64 payloads", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const result = await runCli(
["--agent", MOCK_AGENT_COMMAND, "--cwd", cwd, "--format", "quiet", "exec"],
homeDir,
{
stdin: JSON.stringify([
{ type: "text", text: "inspect-prompt" },
{ type: "image", mimeType: "image/png", data: "%%%" },
]),
},
);
assert.equal(result.code, 2);
assert.match(`${result.stdout}\n${result.stderr}`, /image block data must be valid base64/i);
});
});
test("prompt subcommand accepts --file without being consumed by parent command", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.writeFile(path.join(cwd, "prompt.md"), "fix the tests\n", "utf8");
const result = await runCli(["--cwd", cwd, "codex", "prompt", "--file", "prompt.md"], homeDir);
assert.equal(result.code, 4);
assert.match(result.stderr, /No acpx session found/);
assert.doesNotMatch(result.stderr, /unknown option/i);
});
});
test("exec subcommand accepts --file without being consumed by parent command", async () => {
await withTempHome(async (homeDir) => {
const promptPath = path.join(homeDir, "prompt.txt");
await fs.writeFile(promptPath, "say exactly: file-flag-test\n", "utf8");
const result = await runCli(["custom-agent", "exec", "--file", promptPath], homeDir);
assert.equal(result.code, 1);
assert.doesNotMatch(result.stderr, /unknown option/i);
});
});
test("sessions history prints stored history entries", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await writeSessionRecord(homeDir, {
acpxRecordId: "history-session",
acpSessionId: "history-session",
agentCommand: AGENT_REGISTRY.codex,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:10:00.000Z",
closed: false,
title: null,
messages: [
{
User: {
id: "7d7b0e67-9725-4f57-ba31-491bf4f97767",
content: [{ Text: "first message" }],
},
},
{
Agent: {
content: [{ Text: "second message" }],
tool_results: {},
},
},
],
updated_at: "2026-01-01T00:02:00.000Z",
cumulative_token_usage: {},
request_token_usage: {},
});
const result = await runCli(
["--cwd", cwd, "codex", "sessions", "history", "--limit", "1"],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /second message/);
assert.doesNotMatch(result.stdout, /first message/);
});
});
test("sessions prune dry-run previews closed sessions without deleting", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await writeSessionRecord(homeDir, {
acpxRecordId: "prune-dry-run",
acpSessionId: "prune-dry-run",
agentCommand: AGENT_REGISTRY.codex,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:10:00.000Z",
closed: true,
closedAt: "2026-01-01T00:10:00.000Z",
});
await writeSessionRecord(homeDir, {
acpxRecordId: "prune-open",
acpSessionId: "prune-open",
agentCommand: AGENT_REGISTRY.codex,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:20:00.000Z",
closed: false,
});
const result = await runCli(["--cwd", cwd, "codex", "sessions", "prune", "--dry-run"], homeDir);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /\[DRY RUN\] Would prune 1 session/);
assert.match(result.stdout, /prune-dry-run/);
assert.doesNotMatch(result.stdout, /prune-open/);
assert.ok(await fileExists(sessionFilePath(homeDir, "prune-dry-run")));
assert.ok(await fileExists(sessionFilePath(homeDir, "prune-open")));
});
});
test("sessions prune supports json output and include-history", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
const sessionDir = path.join(homeDir, ".acpx", "sessions");
await fs.mkdir(cwd, { recursive: true });
await writeSessionRecord(homeDir, {
acpxRecordId: "prune-json",
acpSessionId: "prune-json",
agentCommand: AGENT_REGISTRY.codex,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:10:00.000Z",
closed: true,
closedAt: "2026-01-01T00:10:00.000Z",
});
const safeId = encodeURIComponent("prune-json");
const streamFile = path.join(sessionDir, `${safeId}.stream.ndjson`);
await fs.writeFile(streamFile, "event-data\n", "utf8");
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "sessions", "prune", "--include-history"],
homeDir,
);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout) as {
action?: string;
dryRun?: boolean;
count?: number;
bytesFreed?: number;
pruned?: string[];
};
assert.equal(payload.action, "sessions_pruned");
assert.equal(payload.dryRun, false);
assert.equal(payload.count, 1);
assert.ok((payload.bytesFreed ?? 0) > 0);
assert.deepEqual(payload.pruned, ["prune-json"]);
assert.ok(!(await fileExists(sessionFilePath(homeDir, "prune-json"))));
assert.ok(!(await fileExists(streamFile)));
});
});
test("sessions read prints full history by default and supports --tail", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await writeSessionRecord(homeDir, {
acpxRecordId: "read-session",
acpSessionId: "read-session",
agentCommand: AGENT_REGISTRY.codex,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:10:00.000Z",
closed: false,
title: null,
messages: [
{
User: {
id: "4cb89fd7-0dd5-4bdd-8f50-3de20eaa58a5",
content: [{ Text: "first message" }],
},
},
{
Agent: {
content: [{ Text: "second message" }],
tool_results: {},
},
},
],
updated_at: "2026-01-01T00:02:00.000Z",
cumulative_token_usage: {},
request_token_usage: {},
});
const full = await runCli(["--cwd", cwd, "codex", "sessions", "read"], homeDir);
assert.equal(full.code, 0, full.stderr);
assert.match(full.stdout, /first message/);
assert.match(full.stdout, /second message/);
assert.match(full.stdout, /\(2\/2 shown\)/);
const tail = await runCli(["--cwd", cwd, "codex", "sessions", "read", "--tail", "1"], homeDir);
assert.equal(tail.code, 0, tail.stderr);
assert.match(tail.stdout, /second message/);
assert.doesNotMatch(tail.stdout, /first message/);
assert.match(tail.stdout, /\(1\/2 shown\)/);
});
});
test("status reports running queue owner when owner socket is reachable", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const sessionId = "status-live";
const keeper = await startKeeperProcess();
const { lockPath, socketPath } = queuePaths(homeDir, sessionId);
const server = net.createServer((socket) => {
socket.end();
});
try {
await writeSessionRecord(homeDir, {
acpxRecordId: sessionId,
acpSessionId: sessionId,
agentCommand: AGENT_REGISTRY.codex,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
lastPromptAt: "2026-01-01T00:00:00.000Z",
closed: false,
pid: keeper.pid,
agentStartedAt: "2026-01-01T00:00:00.000Z",
});
await writeQueueOwnerLock({
lockPath,
pid: keeper.pid,
sessionId,
socketPath,
});
await listenServer(server, socketPath);
const result = await runCli(["--cwd", cwd, "codex", "status"], homeDir);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /status: running/);
} finally {
await closeServer(server);
await cleanupOwnerArtifacts({ socketPath, lockPath });
stopProcess(keeper);
}
});
});
test("status reports dead when queue owner lease is present but unreachable", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
const sessionId = "status-unreachable-owner";
const keeper = await startKeeperProcess();
const { lockPath, socketPath } = queuePaths(homeDir, sessionId);
try {
await writeSessionRecord(homeDir, {
acpxRecordId: sessionId,
acpSessionId: sessionId,
agentCommand: AGENT_REGISTRY.codex,
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
lastPromptAt: "2026-01-01T00:00:00.000Z",
closed: false,
pid: keeper.pid,
agentStartedAt: "2026-01-01T00:00:00.000Z",
});
await writeQueueOwnerLock({
lockPath,
pid: keeper.pid,
sessionId,
socketPath,
});
const result = await runCli(["--cwd", cwd, "--format", "json", "codex", "status"], homeDir);
assert.equal(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout.trim()) as Record<string, unknown>;
assert.equal(payload.status, "dead");
assert.equal(payload.summary, "queue owner unavailable");
} finally {
await cleanupOwnerArtifacts({ socketPath, lockPath });
stopProcess(keeper);
}
});
});
test("config defaults are loaded from global and project config files", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
defaultAgent: "codex",
format: "json",
agents: {
"my-custom": { command: "custom-global" },
},
},
null,
2,
)}\n`,
"utf8",
);
await fs.writeFile(
path.join(cwd, ".acpxrc.json"),
`${JSON.stringify(
{
agents: {
"my-custom": { command: "custom-project" },
},
},
null,
2,
)}\n`,
"utf8",
);
await writeSessionRecord(homeDir, {
acpxRecordId: "custom-config-session",
acpSessionId: "custom-config-session",
agentCommand: "custom-project",
cwd,
createdAt: "2026-01-01T00:00:00.000Z",
lastUsedAt: "2026-01-01T00:00:00.000Z",
closed: false,
});
const result = await runCli(["--cwd", cwd, "my-custom", "sessions"], homeDir);
assert.equal(result.code, 0, result.stderr);
assert.doesNotThrow(() => JSON.parse(result.stdout.trim()));
assert.match(result.stdout, /custom-config-session/);
});
});
test("exec subcommand is blocked when disableExec is true", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
disableExec: true,
agents: {
codex: { command: MOCK_AGENT_COMMAND },
},
},
null,
2,
)}\n`,
"utf8",
);
const result = await runCli(["--cwd", cwd, "codex", "exec", "hello"], homeDir);
assert.equal(result.code, 1);
assert.match(result.stderr, /exec subcommand is disabled by configuration/);
});
});
test("exec subcommand is blocked in json format when disableExec is true", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
disableExec: true,
agents: {
codex: { command: MOCK_AGENT_COMMAND },
},
},
null,
2,
)}\n`,
"utf8",
);
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "exec", "hello"],
homeDir,
);
assert.equal(result.code, 1);
const payload = JSON.parse(result.stdout.trim()) as {
error?: { code?: number; data?: { acpxCode?: string } };
};
assert.equal(payload.error?.code, -32603);
assert.equal(payload.error?.data?.acpxCode, "EXEC_DISABLED");
});
});
test("exec subcommand works when disableExec is false", async () => {
await withTempHome(async (homeDir) => {
const cwd = path.join(homeDir, "workspace");
await fs.mkdir(cwd, { recursive: true });
await fs.mkdir(path.join(homeDir, ".acpx"), { recursive: true });
await fs.writeFile(
path.join(homeDir, ".acpx", "config.json"),
`${JSON.stringify(
{
disableExec: false,
agents: {
codex: { command: MOCK_AGENT_COMMAND },
},
},
null,
2,
)}\n`,
"utf8",
);
const result = await runCli(
["--cwd", cwd, "--format", "json", "codex", "exec", "echo hello"],
homeDir,
);
// exec should work (exit code 0) since disableExec is false
assert.equal(result.code, 0, result.stderr);
});
});
async function withTempHome(run: (homeDir: string) => Promise<void>): Promise<void> {
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "acpx-cli-test-home-"));
try {
await run(tempHome);
} finally {
await fs.rm(tempHome, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
}
}
type CliRunOptions = {
stdin?: string;
cwd?: string;
timeoutMs?: number;
env?: NodeJS.ProcessEnv;
};
async function runCli(
args: string[],
homeDir: string,
options: CliRunOptions = {},
): Promise<CliRunResult> {
return await new Promise<CliRunResult>((resolve) => {
const child = spawn(process.execPath, [CLI_PATH, ...args], {
env: {
...process.env,
HOME: homeDir,
...options.env,
},
cwd: options.cwd,
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdout += chunk;
});
child.stderr.on("data", (chunk: string) => {
stderr += chunk;
});
if (options.stdin != null) {
child.stdin.end(options.stdin);
} else {
child.stdin.end();
}
let timedOut = false;
let timeout: NodeJS.Timeout | undefined;
if (options.timeoutMs != null && options.timeoutMs > 0) {
timeout = setTimeout(() => {
timedOut = true;
if (child.exitCode == null && child.signalCode == null) {
child.kill("SIGKILL");
}
}, options.timeoutMs);
}
child.once("close", (code) => {
if (timeout) {
clearTimeout(timeout);
}
if (timedOut) {
stderr += `[test] timed out after ${options.timeoutMs}ms\n`;
}
resolve({ code, stdout, stderr });
});
});
}
async function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + Math.max(0, timeoutMs);
while (Date.now() < deadline) {
try {
process.kill(pid, 0);
} catch {
return true;
}
await new Promise<void>((resolve) => {
setTimeout(resolve, 50);
});
}
return false;
}
function makeSessionRecord(
record: Partial<SessionRecord> & {
acpxRecordId: string;
acpSessionId: string;
agentCommand: string;
cwd: string;
createdAt?: string;
lastUsedAt?: string;
},
): SessionRecord {
const timestamp = "2026-01-01T00:00:00.000Z";
return {
schema: "acpx.session.v1",
acpxRecordId: record.acpxRecordId,
acpSessionId: record.acpSessionId,
agentSessionId: record.agentSessionId,
agentCommand: record.agentCommand,
cwd: record.cwd,
name: record.name,
createdAt: record.createdAt ?? timestamp,
lastUsedAt: record.lastUsedAt ?? timestamp,
lastSeq: record.lastSeq ?? 0,
lastRequestId: record.lastRequestId,
eventLog: record.eventLog ?? {
active_path: `.stream.ndjson`,
segment_count: 1,
max_segment_bytes: 1024,
max_segments: 1,
last_write_at: record.lastUsedAt ?? timestamp,
last_write_error: null,
},
closed: record.closed ?? false,
closedAt: record.closedAt,
pid: record.pid,
agentStartedAt: record.agentStartedAt,
lastPromptAt: record.lastPromptAt,
lastAgentExitCode: record.lastAgentExitCode,
lastAgentExitSignal: record.lastAgentExitSignal,
lastAgentExitAt: record.lastAgentExitAt,
lastAgentDisconnectReason: record.lastAgentDisconnectReason,
protocolVersion: record.protocolVersion,
agentCapabilities: record.agentCapabilities,
title: record.title ?? null,
messages: record.messages ?? [],
updated_at: record.updated_at ?? record.lastUsedAt ?? timestamp,
cumulative_token_usage: record.cumulative_token_usage ?? {},
request_token_usage: record.request_token_usage ?? {},
acpx: record.acpx,
};
}
async function writeSessionRecord(
homeDir: string,
record: Partial<SessionRecord> & {
acpxRecordId: string;
acpSessionId: string;
agentCommand: string;
cwd: string;
},
): Promise<void> {
const sessionDir = path.join(homeDir, ".acpx", "sessions");
await fs.mkdir(sessionDir, { recursive: true });
const file = path.join(sessionDir, `${encodeURIComponent(record.acpxRecordId)}.json`);
await fs.writeFile(
file,
`${JSON.stringify(serializeSessionRecordForDisk(makeSessionRecord(record)), null, 2)}\n`,
"utf8",
);
}
function sessionFilePath(homeDir: string, acpxRecordId: string): string {
return path.join(homeDir, ".acpx", "sessions", `${encodeURIComponent(acpxRecordId)}.json`);
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}