diff --git a/.superpowers/sdd/2026-08-03-git-workspace-registry/task-7-report.md b/.superpowers/sdd/2026-08-03-git-workspace-registry/task-7-report.md index 8a6239d6..53111d0d 100644 --- a/.superpowers/sdd/2026-08-03-git-workspace-registry/task-7-report.md +++ b/.superpowers/sdd/2026-08-03-git-workspace-registry/task-7-report.md @@ -26,6 +26,24 @@ completed with 22 passing tests. - `git diff --check` completed cleanly. +## Review fixes — round 3 + +- The active registry snapshot that located a session now remains the authorization and mutation + config for response, steer, events, close/delete, archive/group/rename, documents, and detail. + A pruned historical revision cannot block an already-located session's active lifecycle. +- Only Resume resolves the retained pinned descriptor because Pi needs that immutable config to + restart safely. A pruned pin therefore returns the existing sanitized + `workspace_revision_unavailable` 409 solely for Resume. + +### Round 3 verification + +- RED: with a manifest found through an active registry snapshot and `readPinned` forced to fail, + `POST /sessions/:id/response` returned 409 instead of forwarding the active gate response. +- GREEN: `npx vitest run test/routes-sessions.test.ts test/tht-runner.test.ts test/routes-settings.test.ts && npx tsc --noEmit -p .` + — 102 tests passed with a clean type check. The regression confirms response, close, and delete + use the locating snapshot without calling `readPinned`, while Resume returns a sanitized 409. +- `git diff --check` completed cleanly. + ## Review fixes — round 2 - Lifecycle authorization no longer selects the installation-default workspace. The backend now diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index f6068a9c..65ffbf2b 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -121,10 +121,8 @@ export function sessionRoutes( }; /** RLS makes a foreign session indistinguishable from a missing one. */ - const authorize = async (principal: PrincipalContext, id: string): Promise => { - const located = await locateSession(principal, id); - return located && await resolveSessionWorkspace(located); - }; + const authorize = async (principal: PrincipalContext, id: string): Promise => + await locateSession(principal, id); const storageFailure = (reply: any) => reply.code(503).send({ error: "session storage is unavailable" }); const lifecycleFailure = (reply: any, error: unknown) => diff --git a/backend/test/routes-sessions.test.ts b/backend/test/routes-sessions.test.ts index fddfe701..068dbbe2 100644 --- a/backend/test/routes-sessions.test.ts +++ b/backend/test/routes-sessions.test.ts @@ -603,6 +603,54 @@ test("POST /sessions/:id/resume returns a sanitized error when its retained revi expect(response.json()).toMatchObject({ code: "workspace_revision_unavailable" }); }); +test("a pruned pin blocks Resume but not active or mutation lifecycle routes", async () => { + const activePath = "/registry/snapshots/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/b-workspace.yaml"; + const prunedError = "cannot read /registry/snapshots/secret-pruned-revision/b-workspace.yaml"; + const calls: string[] = []; + const readPinned = vi.fn(async () => { throw new Error(prunedError); }); + let active: any = { bridge: { respond: () => true } }; + const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), { + thtRunner: { + sessionShow: async (_id: string, workspace: string) => { + expect(workspace).toBe(activePath); + return { + id: "pruned", status: "open", archived: false, + workspace_id: "b-workspace", workspace_revision: "b".repeat(40), + }; + }, + closeSession: async (_id: string, workspace: string) => { calls.push(`close:${workspace}`); }, + deleteSession: async (_id: string, workspace: string) => { calls.push(`delete:${workspace}`); }, + } as any, + mgr: { + get: () => active, + teardownIfCurrent: (_id: string, expected: any) => { + if (active !== expected) return false; + active = undefined; + return true; + }, + } as any, + workspaceRegistry: { + list: async () => [{ + id: "b-workspace", commit: "a".repeat(40), blob: "a".repeat(40), snapshotPath: activePath, state: "operational", + }], + readPinned, + } as any, + }); + + expect((await app.inject({ method: "POST", url: "/sessions/pruned/response", payload: { ui_response: {} } })).statusCode) + .toBe(204); + expect((await app.inject({ method: "POST", url: "/sessions/pruned/close" })).statusCode).toBe(200); + expect((await app.inject({ method: "DELETE", url: "/sessions/pruned" })).statusCode).toBe(204); + expect(calls).toEqual([`close:${activePath}`, `delete:${activePath}`]); + expect(readPinned).not.toHaveBeenCalled(); + + const resume = await app.inject({ method: "POST", url: "/sessions/pruned/resume" }); + expect(resume.statusCode).toBe(409); + expect(resume.body).not.toContain(prunedError); + expect(resume.json()).toMatchObject({ code: "workspace_revision_unavailable" }); + expect(readPinned).toHaveBeenCalledWith("b-workspace", "b".repeat(40)); +}); + test("POST /sessions/:id/resume refuses a pinned finalized session before reading its snapshot", async () => { const readPinned = vi.fn(async () => { throw new Error("must not resolve"); }); const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {