mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-12 18:48:31 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
384413e830 |
No files matched your search
@@ -72,6 +72,8 @@ export interface NodeEntry {
|
||||
lifecycleState?: "running" | "detached" | "recoverable" | "attention_required";
|
||||
tmuxAttachCommand: string | null;
|
||||
resumeCommand: string | null;
|
||||
/** What the resume command couldn't include, when something the seat chose couldn't be read. */
|
||||
resumeCommandNote?: string;
|
||||
latestError: string | null;
|
||||
/** Slice 15 — `terminal-active` primitive. true=producing output,
|
||||
* false=silent past threshold, null=no signal. NEVER derived from
|
||||
@@ -231,6 +233,7 @@ const ALLOWED_NODE_FIELDS = new Set([
|
||||
"lifecycleState",
|
||||
"tmuxAttachCommand",
|
||||
"resumeCommand",
|
||||
"resumeCommandNote",
|
||||
"latestError",
|
||||
"terminalActive",
|
||||
"hasAssignedWork",
|
||||
|
||||
@@ -2,6 +2,8 @@ import { currentClaudeMcpMenu } from "./claude-mcp-menu.js";
|
||||
import { shellQuote } from "../adapters/shell-quote.js";
|
||||
import { claudePostureFlag, codexPostureArg } from "../adapters/yolo-mode.js";
|
||||
import { claudeMcpArgs, type ClaudeMcp } from "./claude-mcp.js";
|
||||
import { codexMemberEnvelope, type CodexMemberPermissions } from "./codex-member-permissions.js";
|
||||
import { codexEnvelopeArgs } from "./codex-rebuild-permissions.js";
|
||||
|
||||
// L3 adds `attention_required` for the Claude resume-selection prompt proxy.
|
||||
// Distinct from `inconclusive` (we don't know yet) and `failed` (terminal
|
||||
@@ -52,6 +54,8 @@ export function buildNativeResumeCommand(
|
||||
/** Codex: the seat's recorded daemon choice; only a positive true adds --no-daemon (absent and false print as before). */
|
||||
codexDaemonOptOut?: boolean,
|
||||
claudeMcp?: ClaudeMcp | null,
|
||||
/** Codex: the seat's own saved permission choice; it replaces the posture decision, as on the seat's managed resume. */
|
||||
codexPermissions?: CodexMemberPermissions,
|
||||
): string | null {
|
||||
if (!resumeToken) return null;
|
||||
if (runtime === "claude-code") {
|
||||
@@ -60,11 +64,19 @@ export function buildNativeResumeCommand(
|
||||
return `claude ${claudePostureFlag(process.env)} --resume ${shellQuote(resumeToken)}${nameSuffix}${mcpSuffix}`;
|
||||
}
|
||||
if (runtime === "codex") {
|
||||
return buildCodexResumeCore(resumeToken, codexConfigProfile, undefined, undefined, undefined, undefined, undefined, codexDaemonOptOut === true);
|
||||
return buildCodexResumeCore(resumeToken, codexConfigProfile, undefined, undefined, undefined, undefined,
|
||||
codexSeatPostureArg(codexConfigProfile, codexPermissions), codexDaemonOptOut === true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A Codex seat's own permission choice as the managed resume emits it: the profile, then the choice's flags, in place
|
||||
* of the posture decision. Undefined without a choice, so the posture decision stands. */
|
||||
export function codexSeatPostureArg(codexConfigProfile: string | null | undefined, permissions: CodexMemberPermissions | undefined): string | undefined {
|
||||
if (!permissions) return undefined;
|
||||
return `${codexConfigProfile ? ` -p ${shellQuote(codexConfigProfile)}` : ""}${codexEnvelopeArgs(codexMemberEnvelope(permissions))}`;
|
||||
}
|
||||
|
||||
/** Managed Codex seats run tool commands in a non-login shell, so they keep the seat's own environment, including the
|
||||
* OpenRig command folder at the front of PATH. Codex otherwise gives each command the user's login-shell environment
|
||||
* and puts back only its own directories, so a login file that sets PATH outright drops that folder. */
|
||||
|
||||
@@ -24,7 +24,8 @@ import { RigRepository, findLatestUsableSnapshot, findLatestUsableSnapshotsForAl
|
||||
import { hasCompleteGuidanceBlock, seatGuidanceFile } from "./restore-guidance.js";
|
||||
import { resolveNodeWorkspace } from "./workspace/workspace-resolver.js";
|
||||
import { deriveCanonicalSessionName } from "./session-name.js";
|
||||
import { buildNativeResumeCommand, buildCodexResumeCore } from "./native-resume-probe.js";
|
||||
import { buildNativeResumeCommand, buildCodexResumeCore, codexSeatPostureArg } from "./native-resume-probe.js";
|
||||
import { validateCodexMemberPermissions, type CodexMemberPermissions } from "./codex-member-permissions.js";
|
||||
import { AppliedLaunchObservationStore } from "./applied-launch-observation-store.js";
|
||||
import { readReadyFreshLaunchFences } from "./fresh-occupant-relation.js";
|
||||
import { daemonTmuxServerArgs, daemonTmuxServerInfo, tmuxAttachCommand } from "../adapters/tmux-server.js";
|
||||
@@ -114,13 +115,38 @@ interface BindingRow {
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
function computeResumeCommand(runtime: string | null, resumeToken: string | null, codexConfigProfile?: string | null, codexDaemonOptOut?: boolean, claudeMcp?: ClaudeMcp | null): string | null {
|
||||
return buildNativeResumeCommand(runtime, resumeToken, null, codexConfigProfile, codexDaemonOptOut, claudeMcp);
|
||||
function computeResumeCommand(runtime: string | null, resumeToken: string | null, codexConfigProfile?: string | null, codexDaemonOptOut?: boolean, claudeMcp?: ClaudeMcp | null, codexChoice?: CodexSeatChoice): string | null {
|
||||
return buildNativeResumeCommand(runtime, resumeToken, null, codexConfigProfile, codexDaemonOptOut, claudeMcp,
|
||||
codexChoice && "permissions" in codexChoice ? codexChoice.permissions : undefined);
|
||||
}
|
||||
|
||||
const CODEX_DAEMON_CHOICE_NOT_RECORDED = "Not recorded whether this seat's last managed launch used --no-daemon. If `codex --help` lists --no-daemon, "
|
||||
+ "add it right after `codex`, as the managed launch does.";
|
||||
|
||||
/** Shown beside a Codex resume command when the seat's saved permission choice couldn't be read: the command still shows. */
|
||||
const CODEX_PERMISSIONS_NOT_INCLUDED = "This seat's saved Codex permission choice couldn't be read, so the resume command doesn't include it and "
|
||||
+ "carries the default posture instead. Repair it with rig seat set-permissions <seat> --codex-permissions <file> --reason <reason>.";
|
||||
|
||||
/** A Codex seat's saved permission choice: the choice, or unreadable. A seat with none has no entry. */
|
||||
type CodexSeatChoice = { permissions: CodexMemberPermissions } | { unreadable: true };
|
||||
|
||||
/** Every seat's saved Codex permission choice, in one read. A schema without the column (a minimal fixture) has none. */
|
||||
function readCodexSeatChoices(db: Database.Database): Map<string, CodexSeatChoice> {
|
||||
const choices = new Map<string, CodexSeatChoice>();
|
||||
let rows: Array<{ id: string; codex_permissions_json: string }>;
|
||||
try {
|
||||
rows = db.prepare("SELECT id, codex_permissions_json FROM nodes WHERE codex_permissions_json IS NOT NULL").all() as typeof rows;
|
||||
} catch (error) {
|
||||
if ((error as { code?: string }).code === "SQLITE_ERROR" && /no such column: codex_permissions_json\b/.test((error as Error).message)) return choices;
|
||||
throw error;
|
||||
}
|
||||
for (const row of rows) {
|
||||
try { choices.set(row.id, { permissions: validateCodexMemberPermissions(JSON.parse(row.codex_permissions_json)) }); }
|
||||
catch { choices.set(row.id, { unreadable: true }); }
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
|
||||
function computeRecoveryGuidance(input: {
|
||||
runtime: string | null;
|
||||
resumeToken: string | null;
|
||||
@@ -130,8 +156,10 @@ function computeRecoveryGuidance(input: {
|
||||
/** Codex: the current generation's recorded daemon choice; undefined when none was recorded. */
|
||||
codexDaemonOptOut?: boolean;
|
||||
claudeMcp?: ClaudeMcp;
|
||||
/** Codex: the seat's saved permission choice; undefined when it has none. */
|
||||
codexChoice?: CodexSeatChoice;
|
||||
}): NodeRecoveryGuidance | null {
|
||||
const { runtime, resumeToken, cwd, sessionName, codexConfigProfile, codexDaemonOptOut, claudeMcp } = input;
|
||||
const { runtime, resumeToken, cwd, sessionName, codexConfigProfile, codexDaemonOptOut, claudeMcp, codexChoice } = input;
|
||||
|
||||
if (runtime === "claude-code") {
|
||||
const commands: string[] = [];
|
||||
@@ -165,14 +193,20 @@ function computeRecoveryGuidance(input: {
|
||||
const notes: string[] = [];
|
||||
|
||||
const daemonOptOut = codexDaemonOptOut === true;
|
||||
// The seat's own permission choice replaces the posture decision, as on its managed resume.
|
||||
const seatPosture = codexSeatPostureArg(codexConfigProfile, codexChoice && "permissions" in codexChoice ? codexChoice.permissions : undefined);
|
||||
const lastCommand = buildCodexResumeCore("", codexConfigProfile, true, undefined, undefined, undefined, seatPosture, daemonOptOut);
|
||||
if (resumeToken) {
|
||||
commands.push(buildCodexResumeCore(resumeToken, codexConfigProfile, undefined, undefined, undefined, undefined, undefined, daemonOptOut));
|
||||
commands.push(buildCodexResumeCore(resumeToken, codexConfigProfile, undefined, undefined, undefined, undefined, seatPosture, daemonOptOut));
|
||||
}
|
||||
if (cwd) {
|
||||
commands.push(`cd ${cwd}`);
|
||||
}
|
||||
if (!resumeToken) {
|
||||
commands.push(buildCodexResumeCore("", codexConfigProfile, true, undefined, undefined, undefined, undefined, daemonOptOut));
|
||||
commands.push(lastCommand);
|
||||
}
|
||||
if (codexChoice && "unreadable" in codexChoice) {
|
||||
notes.push(CODEX_PERMISSIONS_NOT_INCLUDED);
|
||||
}
|
||||
|
||||
notes.push("Use workspace and recent prompt text to identify the right conversation.");
|
||||
@@ -186,8 +220,11 @@ function computeRecoveryGuidance(input: {
|
||||
notes.push(CODEX_DAEMON_CHOICE_NOT_RECORDED);
|
||||
}
|
||||
|
||||
const ownChoice = "it carries this seat's own permission choice, as its managed launch does.";
|
||||
return {
|
||||
summary: resumeToken
|
||||
summary: seatPosture
|
||||
? (resumeToken ? `Try native Codex resume first; ${ownChoice}` : `No stored Codex resume token. Try ${lastCommand}; ${ownChoice}`)
|
||||
: resumeToken
|
||||
? "Try native Codex resume first; the managed launch sets the explicit -s workspace-write floor flag unless a named config profile is present."
|
||||
: `No stored Codex resume token. Try codex${daemonOptOut ? " --no-daemon" : ""} -s workspace-write resume --last; the managed launch sets the explicit -s workspace-write floor flag unless a named config profile is present.`,
|
||||
commands,
|
||||
@@ -612,11 +649,12 @@ export function getNodeInventory(db: Database.Database, rigId: string): NodeInve
|
||||
const restoreOutcomes = buildRestoreOutcomeMap(db, rigId);
|
||||
const orienteds = buildOrientedMap(db, rigId);
|
||||
const codexDaemonOptOuts = new AppliedLaunchObservationStore(db).readCurrentDaemonOptOuts();
|
||||
const codexSeatChoices = readCodexSeatChoices(db);
|
||||
|
||||
const rows = queryInventoryRows(db, rigId);
|
||||
|
||||
return rows.map((row) =>
|
||||
buildInventoryEntry(db, row, { usableSnapshot, workspaceSpec, identityVerdicts, restoreOutcomes, orienteds, codexDaemonOptOuts }),
|
||||
buildInventoryEntry(db, row, { usableSnapshot, workspaceSpec, identityVerdicts, restoreOutcomes, orienteds, codexDaemonOptOuts, codexSeatChoices }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -731,6 +769,8 @@ interface InventoryBuildContext {
|
||||
orienteds: Map<string, NodeOriented>;
|
||||
/** Each node's current recorded Codex daemon choice (one read per projection); absent = not recorded. */
|
||||
codexDaemonOptOuts: Map<string, boolean>;
|
||||
/** Each seat's saved Codex permission choice (one read per projection); absent = none. */
|
||||
codexSeatChoices: Map<string, CodexSeatChoice>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -743,7 +783,8 @@ function buildInventoryEntry(
|
||||
row: InventoryRow,
|
||||
ctx: InventoryBuildContext,
|
||||
): NodeInventoryEntry {
|
||||
const { usableSnapshot, workspaceSpec, identityVerdicts, restoreOutcomes, orienteds, codexDaemonOptOuts } = ctx;
|
||||
const { usableSnapshot, workspaceSpec, identityVerdicts, restoreOutcomes, orienteds, codexDaemonOptOuts, codexSeatChoices } = ctx;
|
||||
const codexChoice = row.runtime === "codex" ? codexSeatChoices.get(row.node_id) : undefined;
|
||||
const launchWrapper = parseStoredLaunchWrapper(row.launch_wrapper, `Seat ${row.node_id} launch_wrapper`);
|
||||
if (launchWrapper && !isLaunchWrapperRuntime(row.runtime)) {
|
||||
throw new Error(`Seat ${row.node_id} launch_wrapper: only valid when runtime is "pi", "codex", "claude-code" or a supported community harness`);
|
||||
@@ -823,7 +864,8 @@ function buildInventoryEntry(
|
||||
? { tmuxServerArgs: [...daemonTmuxServerArgs()] } : {}),
|
||||
...(row.binding_attachment_type === "tmux" && row.session_name && daemonTmuxServerInfo()
|
||||
? { tmuxServerMode: daemonTmuxServerInfo()!.mode } : {}),
|
||||
resumeCommand: computeResumeCommand(row.runtime, row.resume_token, row.codex_config_profile, codexDaemonOptOuts.get(row.node_id), row.claude_mcp),
|
||||
resumeCommand: computeResumeCommand(row.runtime, row.resume_token, row.codex_config_profile, codexDaemonOptOuts.get(row.node_id), row.claude_mcp, codexChoice),
|
||||
...(row.resume_token && codexChoice && "unreadable" in codexChoice ? { resumeCommandNote: CODEX_PERMISSIONS_NOT_INCLUDED } : {}),
|
||||
// OPR.0.4.0.26: recoveryGuidance is NOT inlined per node in the LIST
|
||||
// payload. It duplicated ~47KB of templated prose across all nodes and
|
||||
// no node-list consumer reads it. The full guidance is recomputed on
|
||||
@@ -921,6 +963,7 @@ function buildInventoryMapFromRows(db: Database.Database, rows: InventoryRow[]):
|
||||
const restoreOutcomes = buildRestoreOutcomeMap(db); // one fleet restore-outcome scan
|
||||
const orienteds = buildOrientedMap(db); // one fleet startup-orientation scan
|
||||
const codexDaemonOptOuts = new AppliedLaunchObservationStore(db).readCurrentDaemonOptOuts(); // one launch-fact read
|
||||
const codexSeatChoices = readCodexSeatChoices(db); // one permission-choice read
|
||||
const out = new Map<string, NodeInventoryEntry[]>();
|
||||
for (const row of rows) {
|
||||
const entry = buildInventoryEntry(db, row, {
|
||||
@@ -930,6 +973,7 @@ function buildInventoryMapFromRows(db: Database.Database, rows: InventoryRow[]):
|
||||
restoreOutcomes,
|
||||
orienteds,
|
||||
codexDaemonOptOuts,
|
||||
codexSeatChoices,
|
||||
});
|
||||
let list = out.get(row.rig_id);
|
||||
if (!list) { list = []; out.set(row.rig_id, list); }
|
||||
@@ -1137,6 +1181,7 @@ export function getNodeDetail(
|
||||
codexConfigProfile: entry.codexConfigProfile,
|
||||
codexDaemonOptOut: entry.runtime === "codex" ? new AppliedLaunchObservationStore(db).readCurrent(nodeId)?.daemonOptOut : undefined,
|
||||
claudeMcp: entry.runtime === "claude-code" ? readSeatClaudeMcp(db, nodeId) : undefined,
|
||||
codexChoice: entry.runtime === "codex" ? readCodexSeatChoices(db).get(nodeId) : undefined,
|
||||
}),
|
||||
binding,
|
||||
startupFiles,
|
||||
|
||||
@@ -814,6 +814,9 @@ export interface NodeInventoryEntry {
|
||||
/** Effective mode; inherited non-default sockets also have server arguments. */
|
||||
tmuxServerMode?: "inherit" | "isolated";
|
||||
resumeCommand: string | null;
|
||||
/** Present when the resume command couldn't include something the seat chose (its saved Codex permission choice
|
||||
* couldn't be read): the command still shows, and this says what it lacks. */
|
||||
resumeCommandNote?: string;
|
||||
recoveryGuidance: NodeRecoveryGuidance | null;
|
||||
latestError: string | null;
|
||||
// Extended fields
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// The Codex resume command OpenRig shows a person for a seat (`rig ps --nodes` resumeCommand and the node detail's
|
||||
// recovery guidance) carries the seat's own saved permission choice, as its managed resume does. A choice that can't be
|
||||
// read still shows the command, with a note that it doesn't include it.
|
||||
import { mockShellCommand } from "./helpers/shell-command-mock.js";
|
||||
import type Database from "better-sqlite3";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createFullTestDb } from "./helpers/test-app.js";
|
||||
import { SessionRegistry } from "../src/domain/session-registry.js";
|
||||
import { AppliedLaunchObservationStore } from "../src/domain/applied-launch-observation-store.js";
|
||||
import { observeCodexSandbox, type AppliedLaunchObservation } from "../src/domain/permission-drift.js";
|
||||
import { getNodeDetail, getNodeInventory } from "../src/domain/node-inventory.js";
|
||||
import { CodexResumeAdapter } from "../src/adapters/codex-resume.js";
|
||||
import type { CodexMemberPermissions } from "../src/domain/codex-member-permissions.js";
|
||||
import type { TmuxAdapter } from "../src/adapters/tmux.js";
|
||||
|
||||
// No real Codex, shell or tmux runs here.
|
||||
const processMocks = vi.hoisted(() => ({
|
||||
execFile: vi.fn(),
|
||||
execSync: vi.fn<() => string>(() => { throw new Error("Unexpected synchronous process execution"); }),
|
||||
}));
|
||||
vi.mock("node:child_process", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("node:child_process")>()), execFile: processMocks.execFile, execSync: processMocks.execSync,
|
||||
}));
|
||||
|
||||
const workspace: CodexMemberPermissions = { sandbox: "workspace-write", approval: "on-request", network_access: true, writable_roots: ["/data"] };
|
||||
const WORKSPACE_ARGS = " -s workspace-write -a on-request -c 'sandbox_workspace_write.writable_roots=[\"/data\"]'"
|
||||
+ " -c 'sandbox_workspace_write.network_access=true' -c 'sandbox_workspace_write.exclude_tmpdir_env_var=false'"
|
||||
+ " -c 'sandbox_workspace_write.exclude_slash_tmp=false'";
|
||||
const NOT_INCLUDED = "This seat's saved Codex permission choice couldn't be read, so the resume command doesn't include it and "
|
||||
+ "carries the default posture instead. Repair it with rig seat set-permissions <seat> --codex-permissions <file> --reason <reason>.";
|
||||
|
||||
describe("the Codex resume command a person is shown carries the seat's own permission choice", () => {
|
||||
let db: Database.Database;
|
||||
let registry: SessionRegistry;
|
||||
let sessionId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createFullTestDb();
|
||||
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run("rig-2", "test-rig");
|
||||
db.prepare("INSERT INTO pods (id, rig_id, namespace, label) VALUES (?, ?, ?, ?)").run("pod-2", "rig-2", "dev", "Dev");
|
||||
db.prepare(
|
||||
"INSERT INTO nodes (id, rig_id, logical_id, runtime, cwd, pod_id, agent_ref, profile, resolved_spec_name, resolved_spec_version, resolved_spec_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
).run("node-codex", "rig-2", "dev.qa", "codex", "/workspace/app", "pod-2", "local:agents/qa", "default", "qa", "1.0.0", "hash");
|
||||
registry = new SessionRegistry(db);
|
||||
sessionId = registry.registerSession("node-codex", "dev-qa@test-rig").id;
|
||||
});
|
||||
afterEach(() => db.close());
|
||||
|
||||
const choose = (stored: string) => db.prepare("UPDATE nodes SET codex_permissions_json = ? WHERE id = ?").run(stored, "node-codex");
|
||||
const shown = () => {
|
||||
const entry = getNodeInventory(db, "rig-2").find((e) => e.logicalId === "dev.qa")!;
|
||||
const detail = getNodeDetail(db, "rig-2", "dev.qa")!;
|
||||
return { entry, guidance: detail.recoveryGuidance! };
|
||||
};
|
||||
|
||||
describe("with a stored conversation", () => {
|
||||
beforeEach(() => registry.updateResumeToken(sessionId, "codex_id", "sess-456", "hook"));
|
||||
|
||||
it("a workspace-write choice replaces the default posture, in the command and the guidance", () => {
|
||||
choose(JSON.stringify(workspace));
|
||||
const { entry, guidance } = shown();
|
||||
expect(entry.resumeCommand).toBe(`codex${WORKSPACE_ARGS} resume 'sess-456'`);
|
||||
expect(entry).not.toHaveProperty("resumeCommandNote");
|
||||
expect(guidance.commands).toEqual([`codex${WORKSPACE_ARGS} resume 'sess-456'`, "cd /workspace/app"]);
|
||||
expect(guidance.summary).toBe("Try native Codex resume first; it carries this seat's own permission choice, as its managed launch does.");
|
||||
expect(guidance.notes).not.toContain(NOT_INCLUDED);
|
||||
});
|
||||
|
||||
it("a read-only choice keeps the seat's profile and a recorded --no-daemon", () => {
|
||||
choose(JSON.stringify({ sandbox: "read-only", approval: "never" }));
|
||||
db.prepare("UPDATE nodes SET codex_config_profile = ? WHERE id = ?").run("team", "node-codex");
|
||||
const generation = registry.currentOccupantTenure("node-codex")!.generationUuid;
|
||||
new AppliedLaunchObservationStore(db).recordGeneration(generation, { ...observeCodexSandbox("-s workspace-write"), daemonOptOut: true } as AppliedLaunchObservation);
|
||||
expect(shown().entry.resumeCommand).toBe("codex --no-daemon -p 'team' -s read-only -a never resume 'sess-456'");
|
||||
});
|
||||
|
||||
it("prints the same permission arguments the seat's managed resume sends", async () => {
|
||||
choose(JSON.stringify(workspace));
|
||||
const sendText = vi.fn(async () => ({ ok: true as const }));
|
||||
const tmux = mockShellCommand({
|
||||
sendText, hasSession: vi.fn(async () => true), getPaneCommand: vi.fn(async () => "codex"),
|
||||
capturePaneContent: vi.fn(async () => "OpenAI Codex (v0.0.0)\n› Ask Codex to do anything"),
|
||||
sendKeys: vi.fn(async () => ({ ok: true as const })), getPanePid: vi.fn(async () => null),
|
||||
listSessions: vi.fn(async () => []), listWindows: vi.fn(async () => []), listPanes: vi.fn(async () => []),
|
||||
} as unknown as TmuxAdapter);
|
||||
const adapter = new CodexResumeAdapter(tmux, { sleep: async () => {}, maxWaitMs: 0 });
|
||||
const result = await adapter.resume("dev-qa@test-rig", "codex_id", "sess-456", "/workspace/app", null, undefined,
|
||||
undefined, undefined, undefined, undefined, undefined, undefined, workspace);
|
||||
expect(result.ok).toBe(true);
|
||||
const managed = (sendText.mock.calls as unknown[][]).map((call) => String(call[1])).find((text) => text.includes(" resume "))!;
|
||||
expect(managed).toContain(`codex${WORKSPACE_ARGS}`);
|
||||
expect(shown().entry.resumeCommand).toBe(`codex${WORKSPACE_ARGS} resume 'sess-456'`);
|
||||
});
|
||||
|
||||
it("a choice that can't be read still shows the command, with a note that it doesn't include it", () => {
|
||||
choose(JSON.stringify({ sandbox: "danger-full-access", approval: "never" }));
|
||||
const { entry, guidance } = shown();
|
||||
expect(entry.resumeCommand).toBe("codex -s workspace-write resume 'sess-456'");
|
||||
expect(entry.resumeCommandNote).toBe(NOT_INCLUDED);
|
||||
expect(guidance.commands).toEqual(["codex -s workspace-write resume 'sess-456'", "cd /workspace/app"]);
|
||||
expect(guidance.notes).toContain(NOT_INCLUDED);
|
||||
});
|
||||
|
||||
it("a seat with no choice shows the command as before, with no note", () => {
|
||||
const { entry, guidance } = shown();
|
||||
expect(entry.resumeCommand).toBe("codex -s workspace-write resume 'sess-456'");
|
||||
expect(entry).not.toHaveProperty("resumeCommandNote");
|
||||
expect(guidance.notes).not.toContain(NOT_INCLUDED);
|
||||
});
|
||||
});
|
||||
|
||||
it("without a stored conversation, the picker command and the summary carry the choice", () => {
|
||||
choose(JSON.stringify(workspace));
|
||||
const { entry, guidance } = shown();
|
||||
expect(entry.resumeCommand).toBeNull();
|
||||
expect(entry).not.toHaveProperty("resumeCommandNote");
|
||||
expect(guidance.commands).toEqual(["cd /workspace/app", `codex${WORKSPACE_ARGS} resume --last`]);
|
||||
expect(guidance.summary).toBe(`No stored Codex resume token. Try codex${WORKSPACE_ARGS} resume --last; it carries this seat's own permission choice, as its managed launch does.`);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user