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 <noreply@paperclip.ing>
This commit is contained in:
Priya Raman 2026-08-26 19:48:38 +00:00
parent c6dea6b3f9
commit 324e1331f8
No known key found for this signature in database
GPG Key ID: 4861541D36B2037E
2 changed files with 66 additions and 6 deletions

View File

@ -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");

View File

@ -1765,9 +1765,14 @@ export async function readSshEnvLabFixtureState(
statePath: string,
): Promise<SshEnvLabFixtureState | null> {
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<SshEnvLabFixtureState> {
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