From 8bd2e2d51bcd024492303faa0de9e343c5b60f5e Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:04:08 +0530 Subject: [PATCH 1/3] test: exclude configuration fixtures from test discovery --- src/bin-launcher.test.ts | 2 +- src/cli-show-changes.test.ts | 2 +- src/cli.test.ts | 2 +- src/local-agent-profiles.test.ts | 2 +- src/server.test.ts | 2 +- src/skills.test.ts | 2 +- src/test-support/{config.test.ts => config.ts} | 0 src/workspace-conversation.test.ts | 2 +- src/workspaces.test.ts | 2 +- test/package-install-smoke.test.ts | 2 +- tsconfig.build.json | 2 +- 11 files changed, 10 insertions(+), 10 deletions(-) rename src/test-support/{config.test.ts => config.ts} (100%) diff --git a/src/bin-launcher.test.ts b/src/bin-launcher.test.ts index 00b43a99f..f1f5fba37 100644 --- a/src/bin-launcher.test.ts +++ b/src/bin-launcher.test.ts @@ -4,7 +4,7 @@ import { cpSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } fr import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const projectRoot = fileURLToPath(new URL("..", import.meta.url)); const tsxRoot = join(projectRoot, "node_modules", "tsx"); diff --git a/src/cli-show-changes.test.ts b/src/cli-show-changes.test.ts index 40763b86e..b147b7605 100644 --- a/src/cli-show-changes.test.ts +++ b/src/cli-show-changes.test.ts @@ -9,7 +9,7 @@ import test from "node:test"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { createReviewCheckpointManager } from "./review-checkpoints.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); const require = createRequire(import.meta.url); diff --git a/src/cli.test.ts b/src/cli.test.ts index 0983417c3..604a7ec52 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -11,7 +11,7 @@ import { loadConfig } from "./config.js"; import { localAgentDaemonPaths } from "./local-agent-daemon-lifecycle.js"; import { encodeLocalAgentDaemonResponse } from "./local-agent-daemon-protocol.js"; import { LocalAgentStore } from "./local-agent-store.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); const require = createRequire(import.meta.url); diff --git a/src/local-agent-profiles.test.ts b/src/local-agent-profiles.test.ts index d802c17f3..012de72f8 100644 --- a/src/local-agent-profiles.test.ts +++ b/src/local-agent-profiles.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { loadConfig } from "./config.js"; import { loadLocalAgentProfiles } from "./local-agent-profiles.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const root = await mkdtemp(join(tmpdir(), "devspace-agent-profiles-test-")); diff --git a/src/server.test.ts b/src/server.test.ts index 254ce0a61..ae1b02b13 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -17,7 +17,7 @@ import { ProcessSessionManager } from "./process-sessions.js"; import { createMcpServer, createServer } from "./server.js"; import { SqliteWorkspaceStore } from "./workspace-store.js"; import { WorkspaceRegistry } from "./workspaces.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); diff --git a/src/skills.test.ts b/src/skills.test.ts index d758dee81..e69de688d 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -9,7 +9,7 @@ import { loadWorkspaceSkills, resolveSkillReadPath, } from "./skills.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const root = await mkdtemp(join(tmpdir(), "devspace-skills-test-")); const originalHome = process.env.HOME; diff --git a/src/test-support/config.test.ts b/src/test-support/config.ts similarity index 100% rename from src/test-support/config.test.ts rename to src/test-support/config.ts diff --git a/src/workspace-conversation.test.ts b/src/workspace-conversation.test.ts index 45c45a14d..4880a10b5 100644 --- a/src/workspace-conversation.test.ts +++ b/src/workspace-conversation.test.ts @@ -9,7 +9,7 @@ import { loadConfig, type ServerConfig } from "./config.js"; import { openDatabase } from "./db/client.js"; import { SqliteWorkspaceStore } from "./workspace-store.js"; import { WorkspaceRegistry } from "./workspaces.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); diff --git a/src/workspaces.test.ts b/src/workspaces.test.ts index e3506937f..10cce7c3c 100644 --- a/src/workspaces.test.ts +++ b/src/workspaces.test.ts @@ -9,7 +9,7 @@ import { loadConfig, type ServerConfig } from "./config.js"; import { GitWorktreeError } from "./git-worktrees.js"; import { SqliteWorkspaceStore } from "./workspace-store.js"; import { WorkspaceRegistry } from "./workspaces.js"; -import { writeTestDevspaceConfig } from "./test-support/config.test.js"; +import { writeTestDevspaceConfig } from "./test-support/config.js"; const execFileAsync = promisify(execFile); diff --git a/test/package-install-smoke.test.ts b/test/package-install-smoke.test.ts index c53477fd8..4400628ed 100644 --- a/test/package-install-smoke.test.ts +++ b/test/package-install-smoke.test.ts @@ -4,7 +4,7 @@ import { mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { writeTestDevspaceConfig } from "../src/test-support/config.test.js"; +import { writeTestDevspaceConfig } from "../src/test-support/config.js"; const projectRoot = fileURLToPath(new URL("..", import.meta.url)); diff --git a/tsconfig.build.json b/tsconfig.build.json index a48941b6a..2407dc014 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["src/ui/**/*", "src/**/*.test.ts"] + "exclude": ["src/ui/**/*", "src/**/*.test.ts", "src/test-support/**/*"] } From b29e9fbdadc8b77d0d8811f7d5717751221dc916 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:05:20 +0530 Subject: [PATCH 2/3] test: assert lifecycle outcomes in named runtime scenarios --- src/cli.test.ts | 2 - src/local-agent-runtime.test.ts | 395 ++++++++++++++++++-------------- src/local-agent-store.test.ts | 1 - src/ui/card-types.test.ts | 16 +- 4 files changed, 236 insertions(+), 178 deletions(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 604a7ec52..343358617 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -7,7 +7,6 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; -import { loadConfig } from "./config.js"; import { localAgentDaemonPaths } from "./local-agent-daemon-lifecycle.js"; import { encodeLocalAgentDaemonResponse } from "./local-agent-daemon-protocol.js"; import { LocalAgentStore } from "./local-agent-store.js"; @@ -253,7 +252,6 @@ try { }); } - assert.equal(loadConfig(cliConfigEnv).subagents.enabled, true); } finally { rmSync(root, { recursive: true, force: true }); } diff --git a/src/local-agent-runtime.test.ts b/src/local-agent-runtime.test.ts index da2364005..f055e2f09 100644 --- a/src/local-agent-runtime.test.ts +++ b/src/local-agent-runtime.test.ts @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import test from "node:test"; import { Result, type Result as BetterResult } from "better-result"; import { AgentProviderExecutionError, @@ -22,19 +23,6 @@ const context: LocalAgentRuntimeContext = { }; const input: LocalAgentRunInput = { prompt: "inspect", workspaceRoot: "/tmp/project" }; -for (const [code, retryable] of [["ENOENT", false], ["ECONNREFUSED", true], ["ENOTFOUND", true]] as const) { - const classified = await captureAgentProviderResult({ - provider: "codex", - operation: "connect", - run: () => { throw Object.assign(new Error(code), { code }); }, - }); - assert.equal(classified.isErr(), true); - if (classified.isErr()) { - assert.equal(classified.error.code, "PROVIDER_UNAVAILABLE"); - assert.equal(classified.error.retryable, retryable); - } -} - class FakeRuntime implements LocalAgentRuntime { readonly provider = "codex" as const; alive = true; @@ -89,141 +77,189 @@ class FakeRuntime implements LocalAgentRuntime { } } -const runtime = new FakeRuntime(); -let createCount = 0; -const driver: LocalAgentDriver = { - provider: "codex", - idleTimeoutMs: Number.POSITIVE_INFINITY, - runtimeKey: () => "shared", - createRuntime: async () => { - createCount += 1; - await Promise.resolve(); - return Result.ok(runtime); - }, -}; +test("provider connection errors preserve retryability", async (t) => { + for (const [code, retryable] of [["ENOENT", false], ["ECONNREFUSED", true], ["ENOTFOUND", true]] as const) { + const classified = await captureAgentProviderResult({ + provider: "codex", + operation: "connect", + run: () => { throw Object.assign(new Error(code), { code }); }, + }); + assert.equal(classified.isErr(), true); + if (classified.isErr()) { + assert.equal(classified.error.code, "PROVIDER_UNAVAILABLE"); + assert.equal(classified.error.retryable, retryable); + } + } +}); -const pool = new LocalAgentRuntimePool(); -const [first, second] = await Promise.all([ - pool.run(driver, context, input), - pool.run(driver, { ...context, agentId: "agt_other" }, { ...input, prompt: "second" }), -]); -assert.equal(createCount, 1, "runtime creation is single-flight per runtime key"); -assert.equal(unwrap(first).finalResponse, "done:inspect"); -assert.equal(unwrap(second).finalResponse, "done:second"); -assert.equal(runtime.runCount, 2); - -const running = pool.run(driver, context, { ...input, prompt: "wait", providerSessionId: "thread_1" }); -await new Promise((resolve) => setImmediate(resolve)); -await pool.evictIdle(Date.now() + 10_000_000); -assert.equal(runtime.closeCount, 0, "active runtimes are not evicted"); -runtime.releaseWait(); -await running; - -await pool.close(); -await pool.close(); -assert.equal(runtime.closeCount, 1, "runtime close is idempotent"); -assert.deepEqual(runtime.releasedSessions, [], "shutdown closes the runtime without racing session release"); -assert.equal(pool.size, 0); - -let clock = 0; -const sessionRuntime = new FakeRuntime(); -const sessionPool = new LocalAgentRuntimePool({ - now: () => clock, - sessionIdleTimeoutMs: 10, +test("concurrent runs share a runtime that stays alive until shutdown", async (t) => { + const runtime = new FakeRuntime(); + let createCount = 0; + const driver: LocalAgentDriver = { + provider: "codex", + idleTimeoutMs: Number.POSITIVE_INFINITY, + runtimeKey: () => "shared", + createRuntime: async () => { + createCount += 1; + await Promise.resolve(); + return Result.ok(runtime); + }, + }; + + const pool = new LocalAgentRuntimePool(); + t.after(async () => { + runtime.finishSessionRelease(); + runtime.releaseWait(); + await pool.close(); + }); + const [first, second] = await Promise.all([ + pool.run(driver, context, input), + pool.run(driver, { ...context, agentId: "agt_other" }, { ...input, prompt: "second" }), + ]); + assert.equal(createCount, 1, "runtime creation is single-flight per runtime key"); + assert.equal(unwrap(first).finalResponse, "done:inspect"); + assert.equal(unwrap(second).finalResponse, "done:second"); + assert.equal(runtime.runCount, 2); + + const running = pool.run(driver, context, { ...input, prompt: "wait", providerSessionId: "thread_1" }); + await new Promise((resolve) => setImmediate(resolve)); + await pool.evictIdle(Date.now() + 10_000_000); + assert.equal(runtime.closeCount, 0, "active runtimes are not evicted"); + runtime.releaseWait(); + await running; + + await pool.close(); + await pool.close(); + assert.equal(runtime.closeCount, 1, "runtime close is idempotent"); + assert.deepEqual(runtime.releasedSessions, [], "shutdown closes the runtime without racing session release"); + assert.equal(pool.size, 0); }); -const sessionDriver: LocalAgentDriver = { - provider: "codex", - idleTimeoutMs: Number.POSITIVE_INFINITY, - runtimeKey: () => "session-runtime", - createRuntime: async () => Result.ok(sessionRuntime), -}; -await sessionPool.run(sessionDriver, context, input); -clock = 11; -sessionRuntime.releaseBlocked = true; -const releasing = sessionPool.evictIdle(); -await waitFor(() => sessionRuntime.releaseStarted); -const reused = sessionPool.run(sessionDriver, context, { ...input, providerSessionId: "thread_1", prompt: "reuse" }); -await new Promise((resolve) => setImmediate(resolve)); -assert.equal(sessionRuntime.runCount, 1, "reuse waits for the in-flight session release"); -sessionRuntime.finishSessionRelease(); -await releasing; -await reused; -sessionRuntime.releaseBlocked = false; -assert.equal(sessionRuntime.releaseInFlight, false); -assert.deepEqual(sessionRuntime.releasedSessions, ["thread_1"]); -assert.equal(sessionPool.size, 1, "releasing an idle session does not close the runtime"); -await sessionPool.close(); - -const shutdownReleaseRuntime = new FakeRuntime(); -const shutdownReleasePool = new LocalAgentRuntimePool({ - now: () => clock, - sessionIdleTimeoutMs: 10, + +test("session reuse waits for release to finish", async (t) => { + let clock = 0; + const sessionRuntime = new FakeRuntime(); + const sessionPool = new LocalAgentRuntimePool({ + now: () => clock, + sessionIdleTimeoutMs: 10, + }); + t.after(async () => { + sessionRuntime.finishSessionRelease(); + sessionRuntime.releaseWait(); + await sessionPool.close(); + }); + const sessionDriver: LocalAgentDriver = { + provider: "codex", + idleTimeoutMs: Number.POSITIVE_INFINITY, + runtimeKey: () => "session-runtime", + createRuntime: async () => Result.ok(sessionRuntime), + }; + await sessionPool.run(sessionDriver, context, input); + clock = 11; + sessionRuntime.releaseBlocked = true; + const releasing = sessionPool.evictIdle(); + await waitFor(() => sessionRuntime.releaseStarted); + const reused = sessionPool.run(sessionDriver, context, { ...input, providerSessionId: "thread_1", prompt: "reuse" }); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(sessionRuntime.runCount, 1, "reuse waits for the in-flight session release"); + sessionRuntime.finishSessionRelease(); + await releasing; + assert.equal(unwrap(await reused).finalResponse, "done:reuse"); + sessionRuntime.releaseBlocked = false; + assert.equal(sessionRuntime.releaseInFlight, false); + assert.deepEqual(sessionRuntime.releasedSessions, ["thread_1"]); + assert.equal(sessionPool.size, 1, "releasing an idle session does not close the runtime"); + await sessionPool.close(); }); -const shutdownReleaseDriver: LocalAgentDriver = { - provider: "codex", - idleTimeoutMs: Number.POSITIVE_INFINITY, - runtimeKey: () => "shutdown-release-runtime", - createRuntime: async () => Result.ok(shutdownReleaseRuntime), -}; -await shutdownReleasePool.run(shutdownReleaseDriver, context, input); -shutdownReleaseRuntime.releaseBlocked = true; -const shutdownRelease = shutdownReleasePool.evictIdle(30); -await waitFor(() => shutdownReleaseRuntime.releaseStarted); -const shutdownReuse = shutdownReleasePool.run( - shutdownReleaseDriver, - context, - { ...input, providerSessionId: "thread_1", prompt: "reuse during shutdown" }, -); -await new Promise((resolve) => setImmediate(resolve)); -const shutdown = shutdownReleasePool.close(); -await new Promise((resolve) => setImmediate(resolve)); -assert.equal(shutdownReleaseRuntime.closeCount, 0, "shutdown waits for an in-flight session release"); -shutdownReleaseRuntime.finishSessionRelease(); -await shutdownRelease; -const shutdownReuseResult = await shutdownReuse; -assert.equal(shutdownReuseResult.isErr(), true); -if (shutdownReuseResult.isErr()) { - assert.equal(shutdownReuseResult.error.code, "PROVIDER_UNAVAILABLE"); - assert.equal(shutdownReuseResult.error.retryable, true); -} -await shutdown; -assert.equal(shutdownReleaseRuntime.closeCount, 1); -class CleanupFailureRuntime extends FakeRuntime { - override async close(): Promise { - throw new Error("cleanup failed"); +test("shutdown waits for release and rejects queued reuse", async (t) => { + const shutdownReleaseRuntime = new FakeRuntime(); + const shutdownReleasePool = new LocalAgentRuntimePool({ + now: () => 0, + sessionIdleTimeoutMs: 10, + }); + t.after(async () => { + shutdownReleaseRuntime.finishSessionRelease(); + shutdownReleaseRuntime.releaseWait(); + await shutdownReleasePool.close(); + }); + const shutdownReleaseDriver: LocalAgentDriver = { + provider: "codex", + idleTimeoutMs: Number.POSITIVE_INFINITY, + runtimeKey: () => "shutdown-release-runtime", + createRuntime: async () => Result.ok(shutdownReleaseRuntime), + }; + await shutdownReleasePool.run(shutdownReleaseDriver, context, input); + shutdownReleaseRuntime.releaseBlocked = true; + const shutdownRelease = shutdownReleasePool.evictIdle(30); + await waitFor(() => shutdownReleaseRuntime.releaseStarted); + const shutdownReuse = shutdownReleasePool.run( + shutdownReleaseDriver, + context, + { ...input, providerSessionId: "thread_1", prompt: "reuse during shutdown" }, + ); + await new Promise((resolve) => setImmediate(resolve)); + const shutdown = shutdownReleasePool.close(); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(shutdownReleaseRuntime.closeCount, 0, "shutdown waits for an in-flight session release"); + shutdownReleaseRuntime.finishSessionRelease(); + await shutdownRelease; + const shutdownReuseResult = await shutdownReuse; + assert.equal(shutdownReuseResult.isErr(), true); + if (shutdownReuseResult.isErr()) { + assert.equal(shutdownReuseResult.error.code, "PROVIDER_UNAVAILABLE"); + assert.equal(shutdownReuseResult.error.retryable, true); } + await shutdown; + assert.equal(shutdownReleaseRuntime.closeCount, 1); +}); - override async run(): Promise> { - this.alive = false; - return Result.err(new AgentProviderExecutionError({ - code: "PROVIDER_EXECUTION_ERROR", - provider: this.provider, - operation: "run", - retryable: false, - message: "provider failed", - })); +test("cleanup errors preserve the original provider failure", async (t) => { + class CleanupFailureRuntime extends FakeRuntime { + override async close(): Promise { + throw new Error("cleanup failed"); + } + + override async run(): Promise> { + this.alive = false; + return Result.err(new AgentProviderExecutionError({ + code: "PROVIDER_EXECUTION_ERROR", + provider: this.provider, + operation: "run", + retryable: false, + message: "provider failed", + })); + } } -} -const cleanupPool = new LocalAgentRuntimePool(); -const cleanupRuntime = new CleanupFailureRuntime(); -const cleanupDriver: LocalAgentDriver = { - provider: "codex", - runtimeKey: () => "cleanup-runtime", - createRuntime: async () => Result.ok(cleanupRuntime), -}; -const cleanupFailure = await cleanupPool.run(cleanupDriver, context, input); -assert.equal(cleanupFailure.isErr(), true); -if (cleanupFailure.isErr()) assert.equal(cleanupFailure.error.message, "provider failed"); + const cleanupRuntime = new CleanupFailureRuntime(); + const cleanupPool = new LocalAgentRuntimePool(); + t.after(async () => { + cleanupRuntime.finishSessionRelease(); + cleanupRuntime.releaseWait(); + await cleanupPool.close(); + }); + const cleanupDriver: LocalAgentDriver = { + provider: "codex", + runtimeKey: () => "cleanup-runtime", + createRuntime: async () => Result.ok(cleanupRuntime), + }; + const cleanupFailure = await cleanupPool.run(cleanupDriver, context, input); + assert.equal(cleanupFailure.isErr(), true); + if (cleanupFailure.isErr()) assert.equal(cleanupFailure.error.message, "provider failed"); +}); -{ +test("a dead runtime is replaced despite cleanup failure", async (t) => { const deadRuntime = new FakeRuntime(); deadRuntime.alive = false; deadRuntime.close = async () => { throw new Error("dead runtime cleanup failed"); }; const replacementRuntime = new FakeRuntime(); let attempts = 0; const recoveryPool = new LocalAgentRuntimePool(); + t.after(async () => { + replacementRuntime.finishSessionRelease(); + replacementRuntime.releaseWait(); + await recoveryPool.close(); + }); const recoveryDriver: LocalAgentDriver = { provider: "codex", runtimeKey: () => "dead-runtime-recovery", @@ -234,9 +270,9 @@ if (cleanupFailure.isErr()) assert.equal(cleanupFailure.error.message, "provider assert.equal(recovered.isOk(), true, "cleanup failure does not hide successful runtime recovery"); assert.equal(attempts, 2); await recoveryPool.close(); -} +}); -{ +test("shutdown preserves an already completed turn", async (t) => { let closeDuringRun: Promise | undefined; let completedTurnPool!: LocalAgentRuntimePool; class ClosingAfterTurnRuntime extends FakeRuntime { @@ -248,6 +284,11 @@ if (cleanupFailure.isErr()) assert.equal(cleanupFailure.error.message, "provider } const completedTurnRuntime = new ClosingAfterTurnRuntime(); completedTurnPool = new LocalAgentRuntimePool(); + t.after(async () => { + completedTurnRuntime.finishSessionRelease(); + completedTurnRuntime.releaseWait(); + await completedTurnPool.close(); + }); const completedTurnDriver: LocalAgentDriver = { provider: "codex", runtimeKey: () => "completed-turn-during-close", @@ -258,43 +299,55 @@ if (cleanupFailure.isErr()) assert.equal(cleanupFailure.error.message, "provider assert.equal(completedTurn.isOk(), true, "shutdown does not discard an already completed provider turn"); if (completedTurn.isOk()) assert.equal(completedTurn.value.finalResponse, "done:inspect"); await closeDuringRun; -} +}); -let resolveCreation!: (runtime: BetterResult) => void; -const creating = new Promise>((resolve) => { resolveCreation = resolve; }); -const raceRuntime = new FakeRuntime(); -const racePool = new LocalAgentRuntimePool(); -const raceDriver: LocalAgentDriver = { - provider: "codex", - runtimeKey: () => "creation-race", - createRuntime: () => creating, -}; -const pendingRun = racePool.run(raceDriver, context, input); -await new Promise((resolve) => setImmediate(resolve)); -const pendingClose = racePool.close(); -resolveCreation(Result.ok(raceRuntime)); -await pendingClose; -const closedDuringCreation = await pendingRun; -assert.equal(closedDuringCreation.isErr(), true); -if (closedDuringCreation.isErr()) { - assert.equal(closedDuringCreation.error.code, "PROVIDER_UNAVAILABLE"); - assert.equal(closedDuringCreation.error.retryable, true); -} -assert.equal(raceRuntime.closeCount, 1, "a runtime created during shutdown is closed"); +test("shutdown closes a runtime created while stopping", async (t) => { + let resolveCreation!: (runtime: BetterResult) => void; + const creating = new Promise>((resolve) => { resolveCreation = resolve; }); + const raceRuntime = new FakeRuntime(); + const racePool = new LocalAgentRuntimePool(); + t.after(async () => { + resolveCreation(Result.ok(raceRuntime)); + raceRuntime.releaseWait(); + await racePool.close(); + }); + const raceDriver: LocalAgentDriver = { + provider: "codex", + runtimeKey: () => "creation-race", + createRuntime: () => creating, + }; + const pendingRun = racePool.run(raceDriver, context, input); + await new Promise((resolve) => setImmediate(resolve)); + const pendingClose = racePool.close(); + resolveCreation(Result.ok(raceRuntime)); + await pendingClose; + const closedDuringCreation = await pendingRun; + assert.equal(closedDuringCreation.isErr(), true); + if (closedDuringCreation.isErr()) { + assert.equal(closedDuringCreation.error.code, "PROVIDER_UNAVAILABLE"); + assert.equal(closedDuringCreation.error.retryable, true); + } + assert.equal(raceRuntime.closeCount, 1, "a runtime created during shutdown is closed"); -const afterClose = await racePool.run(raceDriver, context, input); -assert.equal(afterClose.isErr(), true); -if (afterClose.isErr()) { - assert.equal(afterClose.error.code, "PROVIDER_UNAVAILABLE"); - assert.equal(afterClose.error.retryable, true); -} + const afterClose = await racePool.run(raceDriver, context, input); + assert.equal(afterClose.isErr(), true); + if (afterClose.isErr()) { + assert.equal(afterClose.error.code, "PROVIDER_UNAVAILABLE"); + assert.equal(afterClose.error.retryable, true); + } +}); -{ +test("failed concurrent creation can recover on a later run", async (t) => { const creationStarted = deferred(); const finishCreation = deferred(); let createAttempts = 0; const recoveryRuntime = new FakeRuntime(); const creationPool = new LocalAgentRuntimePool(); + t.after(async () => { + recoveryRuntime.finishSessionRelease(); + recoveryRuntime.releaseWait(); + await creationPool.close(); + }); const creationDriver: LocalAgentDriver = { provider: "codex", runtimeKey: () => "creation-failure", @@ -326,12 +379,13 @@ if (afterClose.isErr()) { assert.equal(results.every((result) => result.status === "fulfilled" && result.value.isErr()), true); assert.equal(creationPool.size, 0, "a failed starting entry is removed from the pool"); - await creationPool.run(creationDriver, context, input); + const recovered = unwrap(await creationPool.run(creationDriver, context, input)); + assert.equal(recovered.finalResponse, "done:inspect"); assert.equal(createAttempts, 2, "a later caller can retry after creation fails"); await creationPool.close(); -} +}); -{ +test("failed session release remains reusable and retries later", async (t) => { class FlakyReleaseRuntime extends FakeRuntime { releaseAttempts = 0; @@ -348,6 +402,11 @@ if (afterClose.isErr()) { now: () => releaseClock, sessionIdleTimeoutMs: 10, }); + t.after(async () => { + releaseRuntime.finishSessionRelease(); + releaseRuntime.releaseWait(); + await releasePool.close(); + }); const releaseDriver: LocalAgentDriver = { provider: "codex", idleTimeoutMs: Number.POSITIVE_INFINITY, @@ -369,7 +428,7 @@ if (afterClose.isErr()) { "a failed release returns the session to retained state so it can be reused and retried", ); await releasePool.close(); -} +}); function unwrap(result: BetterResult): T { if (result.isErr()) throw result.error; diff --git a/src/local-agent-store.test.ts b/src/local-agent-store.test.ts index 829940f92..95eacef0f 100644 --- a/src/local-agent-store.test.ts +++ b/src/local-agent-store.test.ts @@ -21,7 +21,6 @@ try { effort: "high", }); - assert.match(created.id, /^agt_[a-f0-9]{8}$/); assert.equal(created.status, "starting"); assert.equal(store.getById(created.id)?.effort, "high"); assert.equal(store.getById(created.id)?.profileName, "reviewer"); diff --git a/src/ui/card-types.test.ts b/src/ui/card-types.test.ts index 0e1a387c9..eb21bdb71 100644 --- a/src/ui/card-types.test.ts +++ b/src/ui/card-types.test.ts @@ -2,13 +2,15 @@ import assert from "node:assert/strict"; import test from "node:test"; import { isExpandableCard } from "./card-types.js"; -test("aggregate review opens when a patch is available", () => { - const card = { - tool: "show_changes" as const, - files: [{ path: "src/a.ts", type: "change" as const }], - payload: { patch: "diff --git a/src/a.ts b/src/a.ts" }, - }; - assert.equal(isExpandableCard(card), true); +test("review expansion requires files or a patch", () => { + for (const [card, expected] of [ + [{ tool: "show_changes" }, false], + [{ tool: "show_changes", files: [], payload: { patch: "" } }, false], + [{ tool: "show_changes", payload: { patch: "diff --git a/a.ts b/a.ts" } }, true], + [{ tool: "show_changes", files: [{ path: "a.ts", type: "change" }] }, true], + ] satisfies Array<[Parameters[0], boolean]>) { + assert.equal(isExpandableCard(card), expected, JSON.stringify(card)); + } }); test("workspace details open only when there is useful context", () => { From 204ec79182a28b83c66722a056dd38773c0c7527 Mon Sep 17 00:00:00 2001 From: Waishnav <86405648+Waishnav@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:06:53 +0530 Subject: [PATCH 3/3] test: clean up owned resources even when assertions fail --- AGENTS.md | 21 +- src/apply-patch.test.ts | 367 ++++++++++++++++++----------------- src/process-sessions.test.ts | 276 +++++++++++++------------- 3 files changed, 344 insertions(+), 320 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fa13d5081..f21e0f691 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,4 +115,23 @@ Start at the boundary named by the problem and follow the data. Keep policy in D - Preserve host and provider data unless DevSpace has a concrete reason to normalize it. - Add compatibility behavior only for an identified consumer with a real upgrade path. - Reuse glossary terms in schemas, types, documentation, and errors. -- Keep the execution layer small, reliable, and unsurprising. \ No newline at end of file +- Keep the execution layer small, reliable, and unsurprising. + +## Tests that matter + +Start with an observable contract and a plausible bug the test should catch. +Prefer public MCP workflows for ordinary product behavior, and focused tests +with controlled event ordering for races and partial failures. Use real temporary +files, Git, and SQLite when they are the boundary being tested; fake external +providers only where their timing or failures need to be controlled. + +Give independent scenarios names and fresh state. Keep fixtures minimal, assert +outcomes rather than only calls or attempts, and register cleanup as soon as a +resource is acquired. Check error results as well as thrown errors. For bug +fixes, demonstrate the test failing before the fix and passing afterward. + +Extend an existing scenario before duplicating coverage. Do not pin opaque IDs, +incidental formatting, or implementation call counts without a consumer contract. +Do not add tests merely because a function exists or experimental behavior was +removed. Delete redundant tests only after identifying where their meaningful +failure is still covered. Keep test helpers out of test discovery and builds. diff --git a/src/apply-patch.test.ts b/src/apply-patch.test.ts index 3a846f4f7..528ff97cb 100644 --- a/src/apply-patch.test.ts +++ b/src/apply-patch.test.ts @@ -1,34 +1,36 @@ import assert from "node:assert/strict"; -import { chmod, mkdtemp, readFile, stat, symlink, writeFile } from "node:fs/promises"; +import { chmod, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { applyPatch, isSamePatchFile, parsePatch, replaceFile } from "./apply-patch.js"; -const root = await mkdtemp(join(tmpdir(), "devspace-apply-patch-")); -const replacement = join(root, "replacement.txt"); -const replacementTemporary = join(root, "replacement.tmp"); -await writeFile(replacement, "old\n"); -await writeFile(replacementTemporary, "new\n"); -await replaceFile(replacementTemporary, replacement, true, "win32"); -assert.equal(await readFile(replacement, "utf8"), "new\n"); +const fixtureRoot = await mkdtemp(join(tmpdir(), "devspace-patch-fixtures-")); +try { + const root = await mkdtemp(join(fixtureRoot, "devspace-apply-patch-")); + const replacement = join(root, "replacement.txt"); + const replacementTemporary = join(root, "replacement.tmp"); + await writeFile(replacement, "old\n"); + await writeFile(replacementTemporary, "new\n"); + await replaceFile(replacementTemporary, replacement, true, "win32"); + assert.equal(await readFile(replacement, "utf8"), "new\n"); -const sameIdentity = async (): Promise<{ dev: number; ino: number }> => ({ dev: 1, ino: 2 }); -const differentIdentity = async (path: string): Promise<{ dev: number; ino: number }> => ({ - dev: 1, - ino: path.endsWith("foo.txt") ? 3 : 2, -}); -assert.equal(await isSamePatchFile("/tmp/Foo.txt", "/tmp/Foo.txt"), true); -assert.equal(await isSamePatchFile("/tmp/Foo.txt", "/tmp/foo.txt", sameIdentity), true); -assert.equal(await isSamePatchFile("/tmp/Foo.txt", "/tmp/bar.txt", sameIdentity), false); -assert.equal(await isSamePatchFile("/tmp/Foo.txt", "/tmp/foo.txt", differentIdentity), false); + const sameIdentity = async (): Promise<{ dev: number; ino: number }> => ({ dev: 1, ino: 2 }); + const differentIdentity = async (path: string): Promise<{ dev: number; ino: number }> => ({ + dev: 1, + ino: path.endsWith("foo.txt") ? 3 : 2, + }); + assert.equal(await isSamePatchFile("/tmp/Foo.txt", "/tmp/Foo.txt"), true); + assert.equal(await isSamePatchFile("/tmp/Foo.txt", "/tmp/foo.txt", sameIdentity), true); + assert.equal(await isSamePatchFile("/tmp/Foo.txt", "/tmp/bar.txt", sameIdentity), false); + assert.equal(await isSamePatchFile("/tmp/Foo.txt", "/tmp/foo.txt", differentIdentity), false); -await writeFile(join(root, "alpha.txt"), "one\ntwo\nthree\n"); -await writeFile(join(root, "remove.txt"), "remove me\n"); -await writeFile(join(root, "windows.txt"), "first\r\nsecond\r\n"); + await writeFile(join(root, "alpha.txt"), "one\ntwo\nthree\n"); + await writeFile(join(root, "remove.txt"), "remove me\n"); + await writeFile(join(root, "windows.txt"), "first\r\nsecond\r\n"); -const result = await applyPatch( - root, - `*** Begin Patch + const result = await applyPatch( + root, + `*** Begin Patch *** Add File: nested/added.txt +new +file @@ -45,27 +47,27 @@ const result = await applyPatch( +updated *** Delete File: remove.txt *** End Patch`, -); + ); -assert.deepEqual(result.files, [ - { path: "nested/added.txt", operation: "add" }, - { path: "alpha.txt", operation: "update" }, - { path: "windows.txt", operation: "update" }, - { path: "remove.txt", operation: "delete" }, -]); -assert.equal(result.additions, 4); -assert.equal(result.removals, 3); -assert.match(result.patch, /diff --git a\/alpha\.txt b\/alpha\.txt/); -assert.match(result.patch, /-two\n\+changed/); -assert.equal(await readFile(join(root, "nested/added.txt"), "utf8"), "new\nfile\n"); -assert.equal(await readFile(join(root, "alpha.txt"), "utf8"), "one\nchanged\nthree\n"); -assert.equal(await readFile(join(root, "windows.txt"), "utf8"), "first\r\nupdated\r\n"); -await assert.rejects(readFile(join(root, "remove.txt"), "utf8"), /ENOENT/); + assert.deepEqual(result.files, [ + { path: "nested/added.txt", operation: "add" }, + { path: "alpha.txt", operation: "update" }, + { path: "windows.txt", operation: "update" }, + { path: "remove.txt", operation: "delete" }, + ]); + assert.equal(result.additions, 4); + assert.equal(result.removals, 3); + assert.match(result.patch, /diff --git a\/alpha\.txt b\/alpha\.txt/); + assert.match(result.patch, /-two\n\+changed/); + assert.equal(await readFile(join(root, "nested/added.txt"), "utf8"), "new\nfile\n"); + assert.equal(await readFile(join(root, "alpha.txt"), "utf8"), "one\nchanged\nthree\n"); + assert.equal(await readFile(join(root, "windows.txt"), "utf8"), "first\r\nupdated\r\n"); + await assert.rejects(readFile(join(root, "remove.txt"), "utf8"), /ENOENT/); -if (process.platform !== "win32") await chmod(join(root, "alpha.txt"), 0o755); -const moveResult = await applyPatch( - root, - `*** Begin Patch + if (process.platform !== "win32") await chmod(join(root, "alpha.txt"), 0o755); + const moveResult = await applyPatch( + root, + `*** Begin Patch *** Update File: alpha.txt *** Move to: moved/alpha.txt @@ @@ -73,58 +75,58 @@ const moveResult = await applyPatch( +ONE changed *** End Patch`, -); -assert.deepEqual(moveResult.files, [ - { path: "moved/alpha.txt", previousPath: "alpha.txt", operation: "move" }, -]); -assert.equal(await readFile(join(root, "moved/alpha.txt"), "utf8"), "ONE\nchanged\nthree\n"); -if (process.platform !== "win32") { - assert.notEqual((await stat(join(root, "moved/alpha.txt"))).mode & 0o111, 0); -} -await assert.rejects(readFile(join(root, "alpha.txt"), "utf8"), /ENOENT/); + ); + assert.deepEqual(moveResult.files, [ + { path: "moved/alpha.txt", previousPath: "alpha.txt", operation: "move" }, + ]); + assert.equal(await readFile(join(root, "moved/alpha.txt"), "utf8"), "ONE\nchanged\nthree\n"); + if (process.platform !== "win32") { + assert.notEqual((await stat(join(root, "moved/alpha.txt"))).mode & 0o111, 0); + } + await assert.rejects(readFile(join(root, "alpha.txt"), "utf8"), /ENOENT/); -await assert.rejects( - applyPatch( - root, - `*** Begin Patch + await assert.rejects( + applyPatch( + root, + `*** Begin Patch *** Add File: ../escape.txt +no *** End Patch`, - ), - /path escapes the workspace/, -); + ), + /path escapes the workspace/, + ); -const outside = await mkdtemp(join(tmpdir(), "devspace-apply-patch-outside-")); -await symlink(outside, join(root, "outside-link"), process.platform === "win32" ? "junction" : "dir"); -await assert.rejects( - applyPatch( - root, - `*** Begin Patch + const outside = await mkdtemp(join(fixtureRoot, "devspace-apply-patch-outside-")); + await symlink(outside, join(root, "outside-link"), process.platform === "win32" ? "junction" : "dir"); + await assert.rejects( + applyPatch( + root, + `*** Begin Patch *** Add File: outside-link/escape.txt +no *** End Patch`, - ), - /path resolves outside the workspace/, -); + ), + /path resolves outside the workspace/, + ); -await assert.rejects( - applyPatch( - root, - `*** Begin Patch + await assert.rejects( + applyPatch( + root, + `*** Begin Patch *** Update File: moved/alpha.txt @@ -not present +replacement *** End Patch`, - ), - /could not find hunk context/, -); -assert.equal(await readFile(join(root, "moved/alpha.txt"), "utf8"), "ONE\nchanged\nthree\n"); + ), + /could not find hunk context/, + ); + assert.equal(await readFile(join(root, "moved/alpha.txt"), "utf8"), "ONE\nchanged\nthree\n"); -await assert.rejects( - applyPatch( - root, - `*** Begin Patch + await assert.rejects( + applyPatch( + root, + `*** Begin Patch *** Add File: should-not-exist.txt +staged *** Update File: moved/alpha.txt @@ -132,20 +134,20 @@ await assert.rejects( -missing context +replacement *** End Patch`, - ), - /could not find hunk context/, -); -await assert.rejects(readFile(join(root, "should-not-exist.txt"), "utf8"), /ENOENT/); -assert.equal(await readFile(join(root, "moved/alpha.txt"), "utf8"), "ONE\nchanged\nthree\n"); + ), + /could not find hunk context/, + ); + await assert.rejects(readFile(join(root, "should-not-exist.txt"), "utf8"), /ENOENT/); + assert.equal(await readFile(join(root, "moved/alpha.txt"), "utf8"), "ONE\nchanged\nthree\n"); -const splitHunkRoot = await mkdtemp(join(tmpdir(), "devspace-apply-patch-split-hunk-")); -await writeFile( - join(splitHunkRoot, "long.txt"), - Array.from({ length: 20 }, (_, index) => String(index + 1)).join("\n") + "\n", -); -const splitHunkResult = await applyPatch( - splitHunkRoot, - `*** Begin Patch + const splitHunkRoot = await mkdtemp(join(fixtureRoot, "devspace-apply-patch-split-hunk-")); + await writeFile( + join(splitHunkRoot, "long.txt"), + Array.from({ length: 20 }, (_, index) => String(index + 1)).join("\n") + "\n", + ); + const splitHunkResult = await applyPatch( + splitHunkRoot, + `*** Begin Patch *** Update File: long.txt @@ 1 @@ -158,84 +160,84 @@ const splitHunkResult = await applyPatch( +eighteen 19 *** End Patch`, -); -assert.equal(splitHunkResult.patch.match(/^@@ /gm)?.length, 2); -assert.equal( - await readFile(join(splitHunkRoot, "long.txt"), "utf8"), - [ - "1", "two", "3", "4", "5", "6", "7", "8", "9", "10", - "11", "12", "13", "14", "15", "16", "17", "eighteen", "19", "20", - ].join("\n") + "\n", -); + ); + assert.equal(splitHunkResult.patch.match(/^@@ /gm)?.length, 2); + assert.equal( + await readFile(join(splitHunkRoot, "long.txt"), "utf8"), + [ + "1", "two", "3", "4", "5", "6", "7", "8", "9", "10", + "11", "12", "13", "14", "15", "16", "17", "eighteen", "19", "20", + ].join("\n") + "\n", + ); -const trailingSpaceRoot = await mkdtemp(join(tmpdir(), "devspace-apply-patch-trailing-space-")); -await writeFile(join(trailingSpaceRoot, "spaces.txt"), "old\n"); -const trailingSpaceResult = await applyPatch( - trailingSpaceRoot, - `*** Begin Patch + const trailingSpaceRoot = await mkdtemp(join(fixtureRoot, "devspace-apply-patch-trailing-space-")); + await writeFile(join(trailingSpaceRoot, "spaces.txt"), "old\n"); + const trailingSpaceResult = await applyPatch( + trailingSpaceRoot, + `*** Begin Patch *** Update File: spaces.txt @@ -old +new${" "} *** End Patch`, -); -assert.equal(trailingSpaceResult.patch.endsWith("+new "), true); -assert.equal(await readFile(join(trailingSpaceRoot, "spaces.txt"), "utf8"), "new \n"); + ); + assert.equal(trailingSpaceResult.patch.endsWith("+new "), true); + assert.equal(await readFile(join(trailingSpaceRoot, "spaces.txt"), "utf8"), "new \n"); -assert.throws(() => parsePatch("*** Begin Patch\n*** End Patch"), /contains no file actions/); -assert.throws(() => parsePatch("*** Add File: bad.txt\n+x"), /missing .* marker/); -assert.throws( - () => parsePatch("*** Begin Patch\n*** Add File: empty.txt\n*** End Patch"), - /has no content/, -); + assert.throws(() => parsePatch("*** Begin Patch\n*** End Patch"), /contains no file actions/); + assert.throws(() => parsePatch("*** Add File: bad.txt\n+x"), /missing .* marker/); + assert.throws( + () => parsePatch("*** Begin Patch\n*** Add File: empty.txt\n*** End Patch"), + /has no content/, + ); -const overwriteRoot = await mkdtemp(join(tmpdir(), "devspace-apply-patch-overwrite-")); -await writeFile(join(overwriteRoot, "duplicate.txt"), "old content\n"); -const overwriteResult = await applyPatch( - overwriteRoot, - `*** Begin Patch + const overwriteRoot = await mkdtemp(join(fixtureRoot, "devspace-apply-patch-overwrite-")); + await writeFile(join(overwriteRoot, "duplicate.txt"), "old content\n"); + const overwriteResult = await applyPatch( + overwriteRoot, + `*** Begin Patch *** Add File: duplicate.txt +new content *** End Patch`, -); -assert.deepEqual(overwriteResult.files, [ - { path: "duplicate.txt", operation: "update" }, -]); -assert.equal(await readFile(join(overwriteRoot, "duplicate.txt"), "utf8"), "new content\n"); + ); + assert.deepEqual(overwriteResult.files, [ + { path: "duplicate.txt", operation: "update" }, + ]); + assert.equal(await readFile(join(overwriteRoot, "duplicate.txt"), "utf8"), "new content\n"); -await writeFile(join(overwriteRoot, "source.txt"), "from\n"); -await writeFile(join(overwriteRoot, "destination.txt"), "existing\n"); -await applyPatch( - overwriteRoot, - `*** Begin Patch + await writeFile(join(overwriteRoot, "source.txt"), "from\n"); + await writeFile(join(overwriteRoot, "destination.txt"), "existing\n"); + await applyPatch( + overwriteRoot, + `*** Begin Patch *** Update File: source.txt *** Move to: destination.txt @@ -from +new *** End Patch`, -); -assert.equal(await readFile(join(overwriteRoot, "destination.txt"), "utf8"), "new\n"); -await assert.rejects(readFile(join(overwriteRoot, "source.txt"), "utf8"), /ENOENT/); + ); + assert.equal(await readFile(join(overwriteRoot, "destination.txt"), "utf8"), "new\n"); + await assert.rejects(readFile(join(overwriteRoot, "source.txt"), "utf8"), /ENOENT/); -const noNewlineRoot = await mkdtemp(join(tmpdir(), "devspace-apply-patch-newline-")); -await writeFile(join(noNewlineRoot, "no-newline.txt"), "old"); -await applyPatch( - noNewlineRoot, - `*** Begin Patch + const noNewlineRoot = await mkdtemp(join(fixtureRoot, "devspace-apply-patch-newline-")); + await writeFile(join(noNewlineRoot, "no-newline.txt"), "old"); + await applyPatch( + noNewlineRoot, + `*** Begin Patch *** Update File: no-newline.txt @@ -old +new *** End Patch`, -); -assert.equal(await readFile(join(noNewlineRoot, "no-newline.txt"), "utf8"), "new\n"); + ); + assert.equal(await readFile(join(noNewlineRoot, "no-newline.txt"), "utf8"), "new\n"); -const eofRoot = await mkdtemp(join(tmpdir(), "devspace-apply-patch-eof-")); -await writeFile(join(eofRoot, "tail.txt"), "first\nsecond\n"); -await applyPatch( - eofRoot, - `*** Begin Patch + const eofRoot = await mkdtemp(join(fixtureRoot, "devspace-apply-patch-eof-")); + await writeFile(join(eofRoot, "tail.txt"), "first\nsecond\n"); + await applyPatch( + eofRoot, + `*** Begin Patch *** Update File: tail.txt @@ first @@ -243,27 +245,27 @@ await applyPatch( +second updated *** End of File *** End Patch`, -); -assert.equal(await readFile(join(eofRoot, "tail.txt"), "utf8"), "first\nsecond updated\n"); -await assert.rejects( - applyPatch( - eofRoot, - `*** Begin Patch + ); + assert.equal(await readFile(join(eofRoot, "tail.txt"), "utf8"), "first\nsecond updated\n"); + await assert.rejects( + applyPatch( + eofRoot, + `*** Begin Patch *** Update File: tail.txt @@ first +not tail *** End of File *** End Patch`, - ), - /could not find hunk context/, -); + ), + /could not find hunk context/, + ); -const lenientRoot = await mkdtemp(join(tmpdir(), "devspace-apply-patch-lenient-")); -await writeFile(join(lenientRoot, "file.txt"), "one\n"); -await applyPatch( - lenientRoot, - `<<'EOF' + const lenientRoot = await mkdtemp(join(fixtureRoot, "devspace-apply-patch-lenient-")); + await writeFile(join(lenientRoot, "file.txt"), "one\n"); + await applyPatch( + lenientRoot, + `<<'EOF' *** Begin Patch *** Update File: file.txt @@ @@ -271,41 +273,44 @@ await applyPatch( +two *** End Patch EOF`, -); -assert.equal(await readFile(join(lenientRoot, "file.txt"), "utf8"), "two\n"); + ); + assert.equal(await readFile(join(lenientRoot, "file.txt"), "utf8"), "two\n"); -await applyPatch( - lenientRoot, - `*** Begin Patch + await applyPatch( + lenientRoot, + `*** Begin Patch *** Environment ID: ignored *** Update File: file.txt two +three *** End Patch`, -); -assert.equal(await readFile(join(lenientRoot, "file.txt"), "utf8"), "two\nthree\n"); + ); + assert.equal(await readFile(join(lenientRoot, "file.txt"), "utf8"), "two\nthree\n"); -await assert.rejects( - applyPatch( - lenientRoot, - `*** Begin Patch + await assert.rejects( + applyPatch( + lenientRoot, + `*** Begin Patch *** Add File: ${join(lenientRoot, "absolute.txt")} +no *** End Patch`, - ), - /path must be relative/, -); + ), + /path must be relative/, + ); -await writeFile(join(lenientRoot, "binary.dat"), Buffer.from([0, 159, 146, 150])); -await assert.rejects( - applyPatch( - lenientRoot, - `*** Begin Patch + await writeFile(join(lenientRoot, "binary.dat"), Buffer.from([0, 159, 146, 150])); + await assert.rejects( + applyPatch( + lenientRoot, + `*** Begin Patch *** Update File: binary.dat @@ -x +y *** End Patch`, - ), - /not valid UTF-8|binary/, -); + ), + /not valid UTF-8|binary/, + ); +} finally { + await rm(fixtureRoot, { recursive: true, force: true }); +} diff --git a/src/process-sessions.test.ts b/src/process-sessions.test.ts index b050e7903..0c15a1c73 100644 --- a/src/process-sessions.test.ts +++ b/src/process-sessions.test.ts @@ -33,152 +33,152 @@ const manager = new ProcessSessionManager({ completedSessionTtlMs: 1_000, }); -const node = process.platform === "win32" - ? `"${process.execPath}"` - : JSON.stringify(process.execPath); - -const foreground = await manager.start({ - workspaceId: "workspace-a", - cwd: process.cwd(), - command: `${node} -e "console.log('foreground')"`, - yieldTimeMs: 2_000, -}); -assert.equal(foreground.running, false); -assert.equal(foreground.exitCode, 0); -assert.match(foreground.output, /foreground/); -assert.equal(foreground.sessionId, undefined); - -const environment = await manager.start({ - workspaceId: "workspace-a", - workspaceRoot: "/tmp/devspace-workspace-a", - cwd: process.cwd(), - command: `${node} -e "console.log([process.env.NO_COLOR, process.env.TERM, process.env.PAGER, process.env.GIT_PAGER, process.env.GH_PAGER, process.env.CODEX_CI, process.env.DEVSPACE_WORKSPACE_ID, process.env.DEVSPACE_WORKSPACE_ROOT].join(','))"`, - yieldTimeMs: 2_000, -}); -assert.equal(environment.running, false); -assert.match(environment.output, /1,dumb,cat,cat,cat,1,workspace-a,\/tmp\/devspace-workspace-a/); - -const background = await manager.start({ - workspaceId: "workspace-a", - cwd: process.cwd(), - command: `${node} -e "setTimeout(() => console.log('finished'), 100)"`, - yieldTimeMs: 5, -}); -assert.equal(background.running, true); -assert.ok(background.sessionId); -assert.equal(typeof background.sessionId, "number"); +try { + const node = process.platform === "win32" + ? `"${process.execPath}"` + : JSON.stringify(process.execPath); + + const foreground = await manager.start({ + workspaceId: "workspace-a", + cwd: process.cwd(), + command: `${node} -e "console.log('foreground')"`, + yieldTimeMs: 2_000, + }); + assert.equal(foreground.running, false); + assert.equal(foreground.exitCode, 0); + assert.match(foreground.output, /foreground/); + assert.equal(foreground.sessionId, undefined); -await assert.rejects( - manager.write({ - workspaceId: "workspace-b", + const environment = await manager.start({ + workspaceId: "workspace-a", + workspaceRoot: "/tmp/devspace-workspace-a", + cwd: process.cwd(), + command: `${node} -e "console.log([process.env.NO_COLOR, process.env.TERM, process.env.PAGER, process.env.GIT_PAGER, process.env.GH_PAGER, process.env.CODEX_CI, process.env.DEVSPACE_WORKSPACE_ID, process.env.DEVSPACE_WORKSPACE_ROOT].join(','))"`, + yieldTimeMs: 2_000, + }); + assert.equal(environment.running, false); + assert.match(environment.output, /1,dumb,cat,cat,cat,1,workspace-a,\/tmp\/devspace-workspace-a/); + + const background = await manager.start({ + workspaceId: "workspace-a", + cwd: process.cwd(), + command: `${node} -e "setTimeout(() => console.log('finished'), 100)"`, + yieldTimeMs: 5, + }); + assert.equal(background.running, true); + assert.ok(background.sessionId); + assert.equal(typeof background.sessionId, "number"); + + await assert.rejects( + manager.write({ + workspaceId: "workspace-b", + sessionId: background.sessionId, + yieldTimeMs: 1, + }), + /does not belong to workspace/, + ); + + const completed = await manager.write({ + workspaceId: "workspace-a", sessionId: background.sessionId, - yieldTimeMs: 1, - }), - /does not belong to workspace/, -); - -const completed = await manager.write({ - workspaceId: "workspace-a", - sessionId: background.sessionId, - yieldTimeMs: 2_000, -}); -assert.equal(completed.running, false); -assert.equal(completed.exitCode, 0); -assert.match(completed.output, /finished/); - -const interactive = await manager.start({ - workspaceId: "workspace-a", - cwd: process.cwd(), - command: `${node} -e "process.stdin.once('data', data => { console.log('input:' + data.toString().trim()); process.exit(0); })"`, - yieldTimeMs: 5, -}); -assert.equal(interactive.running, true); -assert.ok(interactive.sessionId); -assert.equal(typeof interactive.sessionId, "number"); - -const inputResult = await manager.write({ - workspaceId: "workspace-a", - sessionId: interactive.sessionId, - chars: "hello\n", - yieldTimeMs: 2_000, -}); -assert.equal(inputResult.running, false); -assert.match(inputResult.output, /input:hello/); - -const defaultInteractive = await manager.start({ - workspaceId: "workspace-a", - cwd: process.cwd(), - command: `${node} -e "process.stdin.once('data', data => setTimeout(() => { console.log('default-input:' + data.toString().trim()); process.exit(0); }, 100))"`, - yieldTimeMs: 5, -}); -assert.equal(defaultInteractive.running, true); -assert.ok(defaultInteractive.sessionId); + yieldTimeMs: 2_000, + }); + assert.equal(completed.running, false); + assert.equal(completed.exitCode, 0); + assert.match(completed.output, /finished/); -const defaultInputResult = await manager.write({ - workspaceId: "workspace-a", - sessionId: defaultInteractive.sessionId, - chars: "hello\n", -}); -assert.equal(defaultInputResult.running, false); -assert.match(defaultInputResult.output, /default-input:hello/); - -const noisyInteractive = await manager.start({ - workspaceId: "workspace-a", - cwd: process.cwd(), - command: `${node} -e "setInterval(() => console.log('tick'), 10); process.stdin.once('data', data => { console.log('input:' + data.toString().trim()); process.exit(0); })"`, - yieldTimeMs: 100, -}); -assert.equal(noisyInteractive.running, true); -assert.ok(noisyInteractive.sessionId); - -await new Promise((resolve) => setTimeout(resolve, 50)); -const noisyInputResult = await manager.write({ - workspaceId: "workspace-a", - sessionId: noisyInteractive.sessionId, - chars: "hello\n", - yieldTimeMs: 2_000, -}); -assert.equal(noisyInputResult.running, false); -assert.match(noisyInputResult.output, /input:hello/); - -const interruptible = await manager.start({ - workspaceId: "workspace-a", - cwd: process.cwd(), - command: `${node} -e "setInterval(() => console.log('tick'), 10)"`, - yieldTimeMs: 100, -}); -assert.equal(interruptible.running, true); -assert.ok(interruptible.sessionId); - -await new Promise((resolve) => setTimeout(resolve, 50)); -const interrupted = await manager.write({ - workspaceId: "workspace-a", - sessionId: interruptible.sessionId, - chars: "\u0003", - yieldTimeMs: 2_000, -}); -assert.equal(interrupted.running, false); -if (process.platform !== "win32") assert.equal(interrupted.signal, "SIGINT"); - -let buffered = await manager.start({ - workspaceId: "workspace-a", - cwd: process.cwd(), - command: `${node} -e "console.log('x'.repeat(5000)); setTimeout(() => {}, 100)"`, - yieldTimeMs: 50, - maxOutputTokens: 100, -}); -if (!buffered.outputTruncated && buffered.sessionId) { - buffered = await manager.write({ + const interactive = await manager.start({ + workspaceId: "workspace-a", + cwd: process.cwd(), + command: `${node} -e "process.stdin.once('data', data => { console.log('input:' + data.toString().trim()); process.exit(0); })"`, + yieldTimeMs: 5, + }); + assert.equal(interactive.running, true); + assert.ok(interactive.sessionId); + assert.equal(typeof interactive.sessionId, "number"); + + const inputResult = await manager.write({ workspaceId: "workspace-a", - sessionId: buffered.sessionId, + sessionId: interactive.sessionId, + chars: "hello\n", yieldTimeMs: 2_000, + }); + assert.equal(inputResult.running, false); + assert.match(inputResult.output, /input:hello/); + + const defaultInteractive = await manager.start({ + workspaceId: "workspace-a", + cwd: process.cwd(), + command: `${node} -e "process.stdin.once('data', data => setTimeout(() => { console.log('default-input:' + data.toString().trim()); process.exit(0); }, 100))"`, + yieldTimeMs: 5, + }); + assert.equal(defaultInteractive.running, true); + assert.ok(defaultInteractive.sessionId); + + const defaultInputResult = await manager.write({ + workspaceId: "workspace-a", + sessionId: defaultInteractive.sessionId, + chars: "hello\n", + }); + assert.equal(defaultInputResult.running, false); + assert.match(defaultInputResult.output, /default-input:hello/); + + const noisyInteractive = await manager.start({ + workspaceId: "workspace-a", + cwd: process.cwd(), + command: `${node} -e "setInterval(() => console.log('tick'), 10); process.stdin.once('data', data => { console.log('input:' + data.toString().trim()); process.exit(0); })"`, + yieldTimeMs: 100, + }); + assert.equal(noisyInteractive.running, true); + assert.ok(noisyInteractive.sessionId); + + await new Promise((resolve) => setTimeout(resolve, 50)); + const noisyInputResult = await manager.write({ + workspaceId: "workspace-a", + sessionId: noisyInteractive.sessionId, + chars: "hello\n", + yieldTimeMs: 2_000, + }); + assert.equal(noisyInputResult.running, false); + assert.match(noisyInputResult.output, /input:hello/); + + const interruptible = await manager.start({ + workspaceId: "workspace-a", + cwd: process.cwd(), + command: `${node} -e "setInterval(() => console.log('tick'), 10)"`, + yieldTimeMs: 100, + }); + assert.equal(interruptible.running, true); + assert.ok(interruptible.sessionId); + + await new Promise((resolve) => setTimeout(resolve, 50)); + const interrupted = await manager.write({ + workspaceId: "workspace-a", + sessionId: interruptible.sessionId, + chars: "\u0003", + yieldTimeMs: 2_000, + }); + assert.equal(interrupted.running, false); + if (process.platform !== "win32") assert.equal(interrupted.signal, "SIGINT"); + + let buffered = await manager.start({ + workspaceId: "workspace-a", + cwd: process.cwd(), + command: `${node} -e "console.log('x'.repeat(5000)); setTimeout(() => {}, 100)"`, + yieldTimeMs: 50, maxOutputTokens: 100, }); -} -assert.equal(buffered.outputTruncated, true); -if (buffered.sessionId) manager.terminate("workspace-a", buffered.sessionId); + if (!buffered.outputTruncated && buffered.sessionId) { + buffered = await manager.write({ + workspaceId: "workspace-a", + sessionId: buffered.sessionId, + yieldTimeMs: 2_000, + maxOutputTokens: 100, + }); + } + assert.equal(buffered.outputTruncated, true); + if (buffered.sessionId) manager.terminate("workspace-a", buffered.sessionId); -try { if (process.platform === "win32") { const pty = await manager.start({ workspaceId: "workspace-a",