diff --git a/packages/server/src/server/hub/relationship-owned-executions.test.ts b/packages/server/src/server/hub/relationship-owned-executions.test.ts index 686d4e99c5..8c2cd0fa62 100644 --- a/packages/server/src/server/hub/relationship-owned-executions.test.ts +++ b/packages/server/src/server/hub/relationship-owned-executions.test.ts @@ -60,7 +60,7 @@ test("failed Hub auto-archive creates release their lifecycle subscriptions", as const hub = await launchRelationship(); const subscriptionBaseline = hub.agentSubscriptionCount(); - hub.failNextProviderPromptStart(); + hub.failProviderPromptStart(); hub.beginOwnedCreate("failed-prompt-create-1", "failed-prompt-execution-1", { autoArchive: true, worktree: { mode: "branch-off", newBranch: "failed-prompt-1" }, @@ -76,7 +76,7 @@ test("failed Hub auto-archive creates release their lifecycle subscriptions", as expect(await hub.listedWorktrees()).toHaveLength(1); expect(hub.agentSubscriptionCount()).toBe(subscriptionBaseline); - hub.failNextProviderPromptStart(); + hub.failProviderPromptStart(); hub.beginOwnedCreate("failed-prompt-create-2", "failed-prompt-execution-2", { autoArchive: true, worktree: { mode: "branch-off", newBranch: "failed-prompt-2" }, @@ -95,7 +95,7 @@ test("failed Hub auto-archive creates release their lifecycle subscriptions", as test("failed Hub create cleans durable state when provider close rejects", async () => { const hub = await launchRelationship(); - hub.failNextProviderPromptStart(); + hub.failProviderPromptStart(); hub.failNextProviderSessionClose(); hub.beginOwnedCreate("failed-close-create", "failed-close-execution", { worktree: { mode: "branch-off", newBranch: "failed-close-worktree" }, @@ -138,9 +138,11 @@ test("failed create never archives a reused worktree", async () => { expect(worktreeCwd).toEqual(expect.any(String)); await hub.ownedTurnCompletion(original.payload.agentId!); - hub.failNextProviderPromptStart(); + const failedPrompt = "Fail the reused worktree create"; + hub.failProviderPromptStart(failedPrompt); hub.beginOwnedCreate("reused-create", "reused-execution", { autoArchive: true, + prompt: failedPrompt, worktree: { mode: "branch-off", newBranch: "shared-hub-worktree" }, }); const reused = await hub.ownedCreateResult("reused-create"); diff --git a/packages/server/src/server/hub/test-utils/relationship-harness.ts b/packages/server/src/server/hub/test-utils/relationship-harness.ts index 87c06f7ca5..2d98cde428 100644 --- a/packages/server/src/server/hub/test-utils/relationship-harness.ts +++ b/packages/server/src/server/hub/test-utils/relationship-harness.ts @@ -1,7 +1,7 @@ import { execFile } from "node:child_process"; import { execFileSync } from "node:child_process"; import { EventEmitter } from "node:events"; -import { existsSync, readFileSync, statSync, watch } from "node:fs"; +import { existsSync, readFileSync, realpathSync, statSync, watch } from "node:fs"; import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import { networkInterfaces, platform, tmpdir } from "node:os"; import path from "node:path"; @@ -446,7 +446,7 @@ export class HubRelationshipHarness { private readonly providerPrompts: AgentPromptInput[] = []; private readonly cliProcesses = new Set>(); private readonly claimedCliSockets = new Set(); - private failNextPromptStart = false; + private readonly promptsToFail = new Set(); private failNextSessionClose = false; private observedEnrollments = 0; private observedSockets = 0; @@ -459,8 +459,8 @@ export class HubRelationshipHarness { throw new Error("Requested provider session close failure"); }, onStartTurn: (prompt) => { - if (this.failNextPromptStart) { - this.failNextPromptStart = false; + const promptText = typeof prompt === "string" ? prompt : JSON.stringify(prompt); + if (this.promptsToFail.delete(promptText)) { throw new Error("Requested provider prompt startup failure"); } this.providerPrompts.push(prompt); @@ -594,8 +594,8 @@ export class HubRelationshipHarness { this.remote.failRevocations(count); } - failNextProviderPromptStart(): void { - this.failNextPromptStart = true; + failProviderPromptStart(prompt = "Create through the Hub"): void { + this.promptsToFail.add(prompt); } failNextProviderSessionClose(): void { @@ -1305,7 +1305,14 @@ export class HubRelationshipHarness { } private comparablePath(value: string): string { - const normalized = path.normalize(path.resolve(value)); + const resolved = path.resolve(value); + let canonical = resolved; + try { + canonical = realpathSync.native(resolved); + } catch { + // An archived worktree no longer exists, so compare its normalized target path. + } + const normalized = path.normalize(canonical); return platform() === "win32" ? normalized.toLowerCase() : normalized; }