fix: align Claude bootstrap with the selected configuration home (#565)

When a Claude seat uses an explicitly selected CLAUDE_CONFIG_DIR, OpenRig now writes its onboarding and workspace-trust bootstrap to that selected home, so the seat is not stranded at first-run screens. The classic launch (permissionMode omitted) keeps its existing home; a binding without a working directory uses the same stored-node selection as the launch; malformed existing state is preserved with a warning. The rig setup and README disclosures now name the right file. Native sign-in and Keychain behaviour are not verified. Refs #154.
This commit is contained in:
Mike Schwarz authored and GitHub committed 2026-10-02 17:50:20 -07:00
1 parent b2bda7443c
commit 254122872c
7 files changed
+260 -42

No files matched your search

+5 -4
View File
@@ -100,16 +100,17 @@ using a published package, since repository guidance can be ahead of npm.
The provider files are separate from instance state. Here `~` means the daemon
user's home; changing `OPENRIG_HOME` alone does not isolate provider configuration.
- **Claude Code:** managed startup writes workspace trust and onboarding completion
to `~/.claude.json`. In the workspace, `.claude/settings.local.json` receives
- **Claude Code:** startup writes workspace trust and onboarding completion.
With an explicit permission mode, it uses the launch-selected `HOME/.claude.json`,
or `<CLAUDE_CONFIG_DIR>/.claude.json` when that variable is set. Classic startup
retains the daemon's `~/.claude.json` path. In the workspace, `.claude/settings.local.json` receives
the context collector's `statusLine` command and selected activity hooks;
helper scripts live under `.openrig/`. Selected settings/MCP resources can also
change that settings file and `.mcp.json`. The shared settings resource sets
`permissions.defaultMode` to `acceptEdits` and enables Exa/Context7 MCP entries;
selected MCP resources configure those external services. Built-in bootstrap
no longer writes a command allowlist to `~/.claude/settings.json` or removes
older allowances. The trust writer uses the daemon home, so a custom
`CLAUDE_CONFIG_DIR` is not a general relocation of these writes.
older allowances. `CLAUDE_CONFIG_DIR` does not relocate the project-local writes.
- **Codex:** writes the daemon's `CODEX_HOME/config.toml` (normally
`~/.codex/config.toml`). Startup enables hooks, adds the OpenRig activity relay
commands and pre-writes trust hashes for those commands. Seat startup adds
+3
View File
@@ -250,6 +250,9 @@ OpenRig performs best-effort deterministic runtime configuration for managed ses
installations may retain one; existing user settings are not removed.
- Claude global state: `~/.claude.json`
Purpose: pre-trust managed workspaces and mark onboarding complete for fresh managed sessions.
Explicit permission modes use the launch-selected `HOME/.claude.json`, or
`<CLAUDE_CONFIG_DIR>/.claude.json` when that variable is set. Classic startup
retains the daemon-home path.
- Claude project-local config: `.claude/settings.local.json`
Purpose: apply context collector/activity hooks and selected `claude_settings_fragment` resources inside the project without committing them to git.
- Claude project-local MCP config: `.mcp.json`
+2 -2
View File
@@ -122,8 +122,8 @@ const BASE_RUNTIME_CONFIG_DISCLOSURE: RuntimeConfigDisclosure[] = [
{
scope: "global",
runtime: "claude-code",
path: "~/.claude.json",
purpose: "Pre-trust managed workspaces and mark Claude onboarding complete.",
path: "~/.claude.json or <CLAUDE_CONFIG_DIR>/.claude.json",
purpose: "Pre-trust managed workspaces and mark Claude onboarding complete. Explicit permission modes use the launch-selected HOME/config; classic startup uses the daemon home.",
},
{
scope: "project",
+2 -2
View File
@@ -56,8 +56,8 @@ function expectRuntimeConfigDisclosure(result: SetupResult): void {
{
scope: "global",
runtime: "claude-code",
path: "~/.claude.json",
purpose: "Pre-trust managed workspaces and mark Claude onboarding complete.",
path: "~/.claude.json or <CLAUDE_CONFIG_DIR>/.claude.json",
purpose: "Pre-trust managed workspaces and mark Claude onboarding complete. Explicit permission modes use the launch-selected HOME/config; classic startup uses the daemon home.",
},
{
scope: "project",
@@ -359,7 +359,7 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
}
/** Best-effort public seam for user-scope Claude bootstrap used by managed sessions. */
ensureManagedBootstrap(binding: { cwd?: string | null; tmuxSession?: string | null }): void {
ensureManagedBootstrap(binding: { nodeId?: string; cwd?: string | null; tmuxSession?: string | null; permissionMode?: NodeBinding["permissionMode"] }): void {
this.provisionManagedBootstrap(binding);
}
@@ -675,11 +675,35 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
return resolveConcreteHint(path, content);
}
private provisionManagedBootstrap(binding: { cwd?: string | null; tmuxSession?: string | null }): void {
private provisionManagedBootstrap(binding: { nodeId?: string; cwd?: string | null; tmuxSession?: string | null; permissionMode?: NodeBinding["permissionMode"] }): void {
// OPR.0.4.8.2 agnostic rip-out: provisionRigPermissions (C2) removed — OpenRig no longer
// authors any config-file permission policy. Trust/onboarding (C3/C4) are neutral plumbing, kept.
this.provisionWorkspaceTrust(binding.cwd ?? null);
this.provisionOnboardingState();
const home = this.fs.homedir ?? (typeof process !== "undefined" ? process.env.HOME : undefined);
// Match launchHarness: omitted permissionMode keeps the classic home path.
const managed = binding.permissionMode !== undefined ? this.claudeManagedLaunch : undefined;
const cwd = binding.cwd === undefined && managed && binding.nodeId
? managed.boundCwd(binding.nodeId) : binding.cwd;
const statePath = managed
? (cwd ? managed.configPaths(cwd).statePath : undefined)
: (home ? nodePath.join(home, ".claude.json") : undefined);
if (!statePath) return;
// Preserve every existing field. Unreadable/malformed/non-object state is
// left untouched by the caller's existing best-effort bootstrap boundary.
const state = this.fs.exists(statePath) ? this.readJsonObjectStrict(statePath) : {};
if (cwd) {
const projects = this.readJsonObjectField(state, "projects");
if (Object.hasOwn(state, "projects") && projects !== state["projects"]) throw new Error("Claude bootstrap projects must be a JSON object; existing state preserved.");
for (const trustKey of this.workspaceTrustKeys(cwd)) {
const projectState = this.readJsonObjectField(projects, trustKey);
if (Object.hasOwn(projects, trustKey) && projectState !== projects[trustKey]) throw new Error("Claude bootstrap project state must be a JSON object; existing state preserved.");
projectState["hasTrustDialogAccepted"] = true;
projects[trustKey] = projectState;
}
state["projects"] = projects;
}
state["hasCompletedOnboarding"] = true;
this.fs.mkdirp(nodePath.dirname(statePath));
this.fs.writeFile(statePath, JSON.stringify(state, null, 2));
}
// OPR.0.4.8.2 agnostic rip-out: the CONVENIENCE_BASELINE (global `Bash(rig:*)` allow) and its
@@ -688,35 +712,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
// policy; the harness-native permission surface is the control surface. Existing provenance-marked
// user files are NOT retro-scrubbed — the new code simply never touches settings.json.
private provisionWorkspaceTrust(cwd: string | null): void {
if (!cwd) return;
const home = this.fs.homedir ?? (typeof process !== "undefined" ? process.env.HOME : undefined);
if (!home) return;
const statePath = nodePath.join(home, ".claude.json");
const state = this.readJsonObject(statePath);
const projects = this.readJsonObjectField(state, "projects");
for (const trustKey of this.workspaceTrustKeys(cwd)) {
const projectState = this.readJsonObjectField(projects, trustKey);
projectState["hasTrustDialogAccepted"] = true;
projects[trustKey] = projectState;
}
state["projects"] = projects;
this.fs.writeFile(statePath, JSON.stringify(state, null, 2));
}
private provisionOnboardingState(): void {
const home = this.fs.homedir ?? (typeof process !== "undefined" ? process.env.HOME : undefined);
if (!home) return;
const statePath = nodePath.join(home, ".claude.json");
const state = this.readJsonObject(statePath);
state["hasCompletedOnboarding"] = true;
this.fs.writeFile(statePath, JSON.stringify(state, null, 2));
}
private workspaceTrustKeys(cwd: string): string[] {
const keys = new Set<string>([nodePath.resolve(cwd)]);
try {
@@ -43,13 +43,29 @@ export class ClaudeManagedLaunch {
return row;
}
/** Bootstrap shares prepare's stored-node cwd when the binding omits it. */
boundCwd(nodeId: string): string {
return this.target(nodeId).cwd!;
}
/** Bootstrap and the launched child must use the same native selection.
* Unset config uses HOME/.claude.json; even an explicit default-looking
* directory selects <configDir>/.claude.json instead (#154/#225).
*/
configPaths(cwd: string): { configDir: string; statePath: string } {
const { HOME, CLAUDE_CONFIG_DIR } = this.sessionEnv;
if (!HOME || !path.isAbsolute(HOME)) throw new Error("Claude managed launch context is unresolved: absolute HOME is required.");
const configDir = path.resolve(cwd, CLAUDE_CONFIG_DIR ?? path.join(HOME, ".claude"));
return { configDir, statePath: path.join(CLAUDE_CONFIG_DIR === undefined ? HOME : configDir, ".claude.json") };
}
private context(cwd: string) {
const { PATH, HOME, CLAUDE_CONFIG_DIR } = this.sessionEnv;
if (!PATH || !HOME || !path.isAbsolute(HOME)) throw new Error("Claude managed launch context is unresolved: managed PATH and absolute HOME are required.");
// Relative/empty PATH entries are interpreted at the intended seat cwd,
// including for /usr/bin/env shebangs inside the selected executable.
const search = PATH.split(path.delimiter).map(p => path.resolve(cwd, p));
const configDir = path.resolve(cwd, CLAUDE_CONFIG_DIR ?? path.join(HOME, ".claude"));
const { configDir } = this.configPaths(cwd);
const env: Record<string, string> = { PATH: search.join(path.delimiter), HOME };
// Session storage needs a directory, but exporting the default changes
// Claude's global config selection. Preserve an unset native selection.
@@ -0,0 +1,203 @@
// Maintainer-owned file/launch fixtures only: no native Claude, daemon, tmux,
// provider or real user configuration. The fake reads the selected state; this
// proves OpenRig's routing, not Claude ingestion or successful login.
import { afterEach, describe, expect, it } from "vitest";
import Database from "better-sqlite3";
import { execFileSync } from "node:child_process";
import * as fs from "node:fs";
import path from "node:path";
import { tmpdir } from "node:os";
import { ClaudeManagedLaunch } from "../src/domain/claude-managed-launch.js";
import { StartupOrchestrator } from "../src/domain/startup-orchestrator.js";
import type { SessionRegistry } from "../src/domain/session-registry.js";
import type { EventBus } from "../src/domain/event-bus.js";
import { ClaudeCodeAdapter, type ClaudeAdapterFsOps } from "../src/adapters/claude-code-adapter.js";
import type { TmuxAdapter } from "../src/adapters/tmux.js";
import type { NodeBinding } from "../src/domain/runtime-adapter.js";
const cleanup: Array<() => void> = [];
afterEach(() => { for (const close of cleanup.splice(0).reverse()) close(); });
const selections = ["unset", "default-looking", "alternate", "relative", "empty"] as const;
type Selection = typeof selections[number];
const sentinel = { syntheticAccount: { value: "fixture-only-preserve-me" }, theme: "chosen", arbitrary: [1, 2],
projects: { elsewhere: { untouched: true } } };
function fixture(selection: Selection, separateHomes: boolean, populated: boolean) {
const root = fs.realpathSync(fs.mkdtempSync(path.join(tmpdir(), "claude-bootstrap-")));
cleanup.push(() => fs.rmSync(root, { recursive: true, force: true }));
const cwd = path.join(root, "seat's project");
const home = path.join(root, "managed-home");
const daemonHome = separateHomes ? path.join(root, "daemon-home") : home;
const bin = path.join(root, "bin");
for (const dir of [cwd, home, daemonHome, bin]) fs.mkdirSync(dir, { recursive: true });
const env: Record<string, string> = { PATH: bin, HOME: home };
if (selection !== "unset") env.CLAUDE_CONFIG_DIR = selection === "default-looking" ? path.join(home, ".claude")
: selection === "alternate" ? path.join(root, "selected-config") : selection === "empty" ? "" : "./relative-config";
const configDir = path.resolve(cwd, env.CLAUDE_CONFIG_DIR ?? path.join(home, ".claude"));
const statePath = env.CLAUDE_CONFIG_DIR === undefined ? path.join(home, ".claude.json") : path.join(configDir, ".claude.json");
const untouched = new Map<string, string>();
for (const file of new Set([path.join(daemonHome, ".claude.json"), path.join(home, ".claude.json"), path.join(home, ".claude", ".claude.json")])) {
if (file === statePath) continue;
fs.mkdirSync(path.dirname(file), { recursive: true });
const text = JSON.stringify({ ...sentinel, location: file });
fs.writeFileSync(file, text); untouched.set(file, text);
}
if (populated) { fs.mkdirSync(path.dirname(statePath), { recursive: true }); fs.writeFileSync(statePath, JSON.stringify(sentinel)); }
const executable = path.join(bin, "claude");
fs.writeFileSync(executable, `#!${process.execPath}\nconst fs = require('node:fs'), path = require('node:path');
if (process.argv.includes('--help')) { console.log('--permission-mode <mode> (choices: "acceptEdits", "default")'); process.exit(0); }
const dir = process.env.CLAUDE_CONFIG_DIR;
const statePath = dir === undefined ? path.join(process.env.HOME, '.claude.json') : path.join(dir, '.claude.json');
if (process.argv.includes('--fork-session')) {
const sessions = path.join(dir ?? path.join(process.env.HOME, '.claude'), 'sessions'); fs.mkdirSync(sessions, {recursive:true});
fs.writeFileSync(path.join(sessions, 'fixture.json'), JSON.stringify({name:'seat',sessionId:'fixture-fork'}));
}
console.log(JSON.stringify({env:{HOME:process.env.HOME,CLAUDE_CONFIG_DIR:dir}, statePath,
state:fs.existsSync(statePath)?JSON.parse(fs.readFileSync(statePath,'utf8')):null,args:process.argv.slice(2)}));\n`);
fs.chmodSync(executable, 0o700);
const db = new Database(":memory:"); cleanup.push(() => db.close());
db.exec(`CREATE TABLE nodes(id TEXT, runtime TEXT, cwd TEXT);
CREATE TABLE bindings(id TEXT, node_id TEXT, tmux_session TEXT, tmux_pane TEXT);
CREATE TABLE occupant_tenures(node_id TEXT, generation_uuid TEXT, generation_ordinal INTEGER);`);
db.prepare("INSERT INTO nodes VALUES ('node','claude-code',?)").run(cwd);
db.exec("INSERT INTO bindings VALUES ('binding','node','seat','%1'); INSERT INTO occupant_tenures VALUES ('node','generation',1)");
const managed = new ClaudeManagedLaunch(db, env, {});
const reads: string[] = [], writes: string[] = [], launches: any[] = [];
const fsOps: ClaudeAdapterFsOps = {
homedir: daemonHome, exists: fs.existsSync, mkdirp: p => { fs.mkdirSync(p, { recursive: true }); },
readFile: p => { reads.push(p); return fs.readFileSync(p, "utf8"); },
writeFile: (p, text) => { writes.push(p); fs.writeFileSync(p, text); },
copyFile: fs.copyFileSync, readdir: p => fs.readdirSync(p),
};
const tmux = { sendShellCommand: async (_target: string, command: string, check: () => void) => {
check(); launches.push(JSON.parse(execFileSync("/bin/sh", ["-c", command], { encoding: "utf8", env: { HOME: daemonHome, PATH: "/usr/bin:/bin" } })));
return { ok: true as const };
}, getPaneCommand: async () => "claude", capturePaneContent: async () => "Claude Code\n>" } as unknown as TmuxAdapter;
const adapter = new ClaudeCodeAdapter({ tmux, fsOps, claudeManagedLaunch: managed, sessionIdFactory: () => "fixture-fresh", sleep: async () => {} });
const binding = { id: "binding", nodeId: "node", cwd, tmuxSession: "seat", tmuxPane: "%1", permissionMode: "acceptEdits" } as NodeBinding;
return { root, cwd, env, statePath, untouched, reads, writes, launches, adapter, binding, fsOps, tmux, db };
}
describe.skipIf(process.platform === "win32")("Claude managed bootstrap selects the launch config home", () => {
it.each(["fresh", "resume"])("classic %s keeps bootstrap in its inherited default home", async mode => {
const f = fixture("alternate", false, true);
const alternateBefore = fs.readFileSync(f.statePath, "utf8");
let pending = "";
f.tmux.sendText = async (_target, text) => { pending = text; return { ok: true }; };
f.tmux.sendKeys = async () => {
f.launches.push(JSON.parse(execFileSync("/bin/sh", ["-c", pending], {
encoding: "utf8", cwd: f.cwd, env: { HOME: f.env.HOME, PATH: f.env.PATH + ":/usr/bin:/bin" },
})));
return { ok: true };
};
const binding = { ...f.binding, permissionMode: undefined };
await f.adapter.deliverStartup([], binding);
expect(await f.adapter.launchHarness(binding, { name: "seat", ...(mode === "resume" ? { resumeToken: "fixture-original" } : {}) })).toMatchObject({ ok: true });
const observed = f.launches[0];
expect(observed.env).not.toHaveProperty("CLAUDE_CONFIG_DIR");
expect(observed.statePath).toBe(path.join(f.env.HOME, ".claude.json"));
expect(observed.state).toMatchObject({ ...sentinel, hasCompletedOnboarding: true, projects: { ...sentinel.projects, [f.cwd]: { hasTrustDialogAccepted: true } } });
expect(new Set(f.writes)).toEqual(new Set([observed.statePath]));
expect(fs.readFileSync(f.statePath, "utf8")).toBe(alternateBefore);
});
it.each(["{broken", "[]", "null", '{"projects":[]}', "invalid project"])("classic bootstrap preserves unmergeable state (%s)", async value => {
const f = fixture("alternate", false, true);
const file = path.join(f.env.HOME, ".claude.json");
const before = value === "invalid project" ? JSON.stringify({ projects: { [f.cwd]: [] } }) : value;
fs.writeFileSync(file, before);
await f.adapter.deliverStartup([], { ...f.binding, permissionMode: undefined });
expect(fs.readFileSync(file, "utf8")).toBe(before);
expect(f.writes).toEqual([]);
});
it.each(["relative", "unset"] as const)("startup with missing binding cwd bootstraps the stored node's %s selection", async selection => {
const f = fixture(selection, true, true);
f.db.exec(`CREATE TABLE node_permission_selections(node_id TEXT, runtime TEXT, mode TEXT);
CREATE TABLE node_startup_context(node_id TEXT PRIMARY KEY, projection_entries_json TEXT, resolved_files_json TEXT, startup_actions_json TEXT, runtime TEXT);`);
const statuses: string[] = [];
// Persistence/readiness boundaries are synthetic; orchestration, projection,
// bootstrap, prepare and the emitted child command are the production path.
const registry = { db: f.db, currentOccupantTenure: () => null,
updateStartupStatus: (_id: string, state: string) => statuses.push(state),
updateResumeToken: () => {} } as unknown as SessionRegistry;
const eventBus = { db: f.db, emit: () => {} } as unknown as EventBus;
f.adapter.checkReady = async () => ({ ready: true });
const orchestrator = new StartupOrchestrator({ db: f.db, sessionRegistry: registry, eventBus, tmuxAdapter: f.tmux });
const binding = { ...f.binding };
delete (binding as Partial<NodeBinding>).cwd;
const result = await orchestrator.startNode({ rigId: "rig", nodeId: "node", sessionId: "session",
binding, adapter: f.adapter,
plan: { runtime: "claude-code", cwd: f.cwd, entries: [], startup: { files: [], actions: [] }, conflicts: [], noOps: [], diagnostics: [] },
resolvedStartupFiles: [], startupActions: [], isRestore: false });
expect(result).toMatchObject({ ok: true });
expect(statuses).toEqual(["pending", "ready"]);
expect(f.launches[0].statePath).toBe(f.statePath);
expect(f.launches[0].state).toMatchObject({ ...sentinel, hasCompletedOnboarding: true,
projects: { ...sentinel.projects, [f.cwd]: { hasTrustDialogAccepted: true } } });
expect(new Set(f.writes)).toEqual(new Set([f.statePath]));
for (const [file, before] of f.untouched) expect(fs.readFileSync(file, "utf8")).toBe(before);
});
it("classic bootstrap without cwd preserves known-home onboarding", () => {
const f = fixture("alternate", false, true);
f.adapter.ensureManagedBootstrap({ ...f.binding, cwd: undefined, permissionMode: undefined });
expect(JSON.parse(fs.readFileSync(path.join(f.env.HOME, ".claude.json"), "utf8")))
.toMatchObject({ ...sentinel, hasCompletedOnboarding: true });
});
it.each(["projects", "project entry"])("preserves an unmergeable %s field", async field => {
const f = fixture("alternate", true, true);
const state = { ...sentinel, projects: field === "projects" ? [] : { [f.cwd]: [] } };
const before = JSON.stringify(state);
fs.writeFileSync(f.statePath, before);
expect(await f.adapter.deliverStartup([], f.binding)).toEqual({ delivered: 0, failed: [] });
expect(fs.readFileSync(f.statePath, "utf8")).toBe(before);
expect(f.writes).toEqual([]);
});
it.each(["{broken", "[]", "null"])("preserves unmergeable selected state (%s), without falling back", async text => {
const f = fixture("alternate", true, true);
fs.writeFileSync(f.statePath, text);
expect(await f.adapter.deliverStartup([], f.binding)).toEqual({ delivered: 0, failed: [] });
expect(fs.readFileSync(f.statePath, "utf8")).toBe(text);
expect(f.writes).toEqual([]);
for (const [file, before] of f.untouched) expect(fs.readFileSync(file, "utf8")).toBe(before);
});
it("preserves unreadable selected state without writing another home", async () => {
const f = fixture("alternate", true, true);
f.fsOps.readFile = () => { throw Error("synthetic read refusal"); };
expect(await f.adapter.deliverStartup([], f.binding)).toEqual({ delivered: 0, failed: [] });
expect(JSON.parse(fs.readFileSync(f.statePath, "utf8"))).toEqual(sentinel);
expect(f.writes).toEqual([]);
});
for (const selection of selections) for (const separateHomes of [false, true]) for (const populated of [false, true]) {
it(`${selection}; ${separateHomes ? "two homes" : "same home"}; ${populated ? "populated" : "fresh"}`, async () => {
const f = fixture(selection, separateHomes, populated);
// The ordinary prelaunch delivery + launch seams, with no rigbundle involved.
expect(await f.adapter.deliverStartup([], f.binding)).toEqual({ delivered: 0, failed: [] });
expect(await f.adapter.launchHarness(f.binding, { name: "seat" })).toMatchObject({ ok: true });
const observed = f.launches[0];
if (process.env.BOOTSTRAP_FIXTURE_RECEIPTS) fs.appendFileSync(process.env.BOOTSTRAP_FIXTURE_RECEIPTS, JSON.stringify({
selection, separateHomes, populated, expected: f.statePath, reads: f.reads, writes: f.writes,
launchSelection: observed.env, selectedStatePresent: observed.state !== null,
onboarding: observed.state?.hasCompletedOnboarding, trust: observed.state?.projects?.[f.cwd]?.hasTrustDialogAccepted,
}) + "\n");
expect.soft(observed.state).toMatchObject({ hasCompletedOnboarding: true, projects: { [f.cwd]: { hasTrustDialogAccepted: true } } });
expect.soft(new Set(f.writes)).toEqual(new Set([f.statePath]));
expect.soft(f.reads.filter(p => p.endsWith(".claude.json")).every(p => p === f.statePath)).toBe(true);
for (const [file, text] of f.untouched) expect.soft(fs.readFileSync(file, "utf8")).toBe(text);
if (populated) expect.soft(observed.state).toMatchObject(sentinel);
expect(observed.env.HOME).toBe(f.env.HOME);
if (selection === "unset") expect(observed.env).not.toHaveProperty("CLAUDE_CONFIG_DIR");
else expect(observed.env.CLAUDE_CONFIG_DIR).toBe(path.dirname(f.statePath));
expect(observed.args.slice(0, 2)).toEqual(["--permission-mode", "acceptEdits"]);
});
}
it.each(["resume", "fork"])("%s uses the same selected bootstrap path", async mode => {
const f = fixture("relative", true, true);
await f.adapter.deliverStartup([], f.binding);
const result = await f.adapter.launchHarness(f.binding, mode === "resume"
? { name: "seat", resumeToken: "fixture-original" }
: { name: "seat", forkSource: { kind: "native_id", value: "fixture-original" } });
expect(result).toMatchObject({ ok: true });
expect(f.launches[0].state).toMatchObject({ ...sentinel, hasCompletedOnboarding: true, projects: { ...sentinel.projects, [f.cwd]: { hasTrustDialogAccepted: true } } });
expect(new Set(f.writes)).toEqual(new Set([f.statePath]));
for (const [file, text] of f.untouched) expect(fs.readFileSync(file, "utf8")).toBe(text);
});
});