From 324e1331f8701ac57334f8404d3c613e99ddb4d9 Mon Sep 17 00:00:00 2001 From: Priya Raman Date: Wed, 26 Aug 2026 19:48:38 +0000 Subject: [PATCH] fix(adapter-utils): resolve relative SSH env-lab fixture statePath before use startSshEnvLabFixture derived rootDir and persisted its statePath field directly from the caller's input, so a relative statePath produced relative fixture paths. The state validator added for signal safety rejects any non-absolute path, so start, status, and stop broke on a relative statePath. Resolve statePath to an absolute path once, in both startSshEnvLabFixture and readSshEnvLabFixtureState, before it derives or persists any path. Co-authored-by: Paperclip --- .../adapter-utils/src/ssh-fixture.test.ts | 50 +++++++++++++++++++ packages/adapter-utils/src/ssh.ts | 22 +++++--- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/adapter-utils/src/ssh-fixture.test.ts b/packages/adapter-utils/src/ssh-fixture.test.ts index e66df7d16d..c9c7657f56 100644 --- a/packages/adapter-utils/src/ssh-fixture.test.ts +++ b/packages/adapter-utils/src/ssh-fixture.test.ts @@ -194,6 +194,56 @@ describe("ssh env-lab fixture", () => { expect(stopped.running).toBe(false); }, SSH_FIXTURE_TEST_TIMEOUT_MS); + it("resolves a relative statePath to the same absolute state across start, status, and stop", async () => { + const rootDir = await createFixtureRootDir(); + const absoluteStatePath = path.join(rootDir, "state.json"); + // A path relative to the test process's own working directory. This is + // the shape a caller outside this file's own resolveEnvLabSshStatePath + // helper can pass; startSshEnvLabFixture must resolve it up front so the + // persisted state and every derived path stay absolute. + const relativeStatePath = path.relative(process.cwd(), absoluteStatePath); + + if (sshEnvLabUnsupportedReason) { + console.warn(`Skipping relative statePath test: ${sshEnvLabUnsupportedReason}`); + return; + } + const support = await getSshEnvLabSupport(); + if (!support.supported) { + sshEnvLabUnsupportedReason = support.reason ?? "unsupported environment"; + console.warn(`Skipping relative statePath test: ${sshEnvLabUnsupportedReason}`); + return; + } + + const entry = fixtureTeardowns.find((candidate) => candidate.rootDir === rootDir); + if (!entry) { + throw new Error(`No fixture teardown entry for ${rootDir}.`); + } + + let state: SshEnvLabFixtureState; + try { + state = await startSshEnvLabFixture({ statePath: relativeStatePath }); + } catch (error) { + sshEnvLabUnsupportedReason = error instanceof Error ? error.message : String(error); + console.warn(`Skipping relative statePath test: ${sshEnvLabUnsupportedReason}`); + return; + } + entry.state = state; + + expect(state.statePath).toBe(absoluteStatePath); + expect(state.rootDir).toBe(rootDir); + + const running = await readSshEnvLabFixtureStatus(relativeStatePath); + expect(running.running).toBe(true); + expect(running.state?.statePath).toBe(absoluteStatePath); + + const stopped = await stopSshEnvLabFixture(relativeStatePath); + expect(stopped).toBe(true); + entry.state = null; + + const afterStop = await readSshEnvLabFixtureStatus(relativeStatePath); + expect(afterStop.running).toBe(false); + }, SSH_FIXTURE_TEST_TIMEOUT_MS); + it("forwards stdin to remote SSH commands", async () => { const rootDir = await createFixtureRootDir(); const statePath = path.join(rootDir, "state.json"); diff --git a/packages/adapter-utils/src/ssh.ts b/packages/adapter-utils/src/ssh.ts index 2dc4d71bfc..084bb0803d 100644 --- a/packages/adapter-utils/src/ssh.ts +++ b/packages/adapter-utils/src/ssh.ts @@ -1765,9 +1765,14 @@ export async function readSshEnvLabFixtureState( statePath: string, ): Promise { try { - const raw = JSON.parse(await fs.readFile(statePath, "utf8")) as SshEnvLabFixtureState; + // Resolve a relative statePath against the current working directory + // before use. The state validator below only accepts absolute paths, and + // a relative statePath must resolve to the same absolute directory every + // time a caller reads it, no matter the process working directory. + const resolvedStatePath = path.resolve(statePath); + const raw = JSON.parse(await fs.readFile(resolvedStatePath, "utf8")) as SshEnvLabFixtureState; if (!raw || raw.kind !== "ssh_openbsd") return null; - if (!isValidSshEnvLabFixtureState(raw, path.dirname(statePath))) return null; + if (!isValidSshEnvLabFixtureState(raw, path.dirname(resolvedStatePath))) return null; return raw; } catch { return null; @@ -1858,7 +1863,12 @@ export async function startSshEnvLabFixture(input: { // real 10 second wait. readinessTimeoutMs?: number; }): Promise { - const existing = await readSshEnvLabFixtureState(input.statePath); + // Resolve a relative statePath against the current working directory once, + // up front. Every derived path (rootDir and the persisted statePath field) + // must be absolute, so the state validator in readSshEnvLabFixtureState + // accepts the file that this function writes. + const statePath = path.resolve(input.statePath); + const existing = await readSshEnvLabFixtureState(statePath); if (existing && await isSshEnvLabFixtureProcess(existing)) { return existing; } @@ -1877,7 +1887,7 @@ export async function startSshEnvLabFixture(input: { const bindHost = input.bindHost ?? "127.0.0.1"; const host = input.host ?? bindHost; - const rootDir = path.dirname(input.statePath); + const rootDir = path.dirname(statePath); await fs.mkdir(rootDir, { recursive: true }); const username = os.userInfo().username; @@ -1949,7 +1959,7 @@ export async function startSshEnvLabFixture(input: { username, rootDir, workspaceDir, - statePath: input.statePath, + statePath, pid: child.pid ?? 0, createdAt: new Date().toISOString(), clientPrivateKeyPath, @@ -1975,7 +1985,7 @@ export async function startSshEnvLabFixture(input: { const config = await buildSshEnvLabFixtureConfig(state); await ensureSshWorkspaceReady(config); }, { timeoutMs: input.readinessTimeoutMs ?? 10_000, intervalMs: 250 }); - await fs.writeFile(input.statePath, JSON.stringify(state, null, 2), { mode: 0o600 }); + await fs.writeFile(statePath, JSON.stringify(state, null, 2), { mode: 0o600 }); return state; } catch (error) { // No state file exists on this path yet, so a later stopSshEnvLabFixture