feat: pin sessions to workspace revisions
This commit is contained in:
@@ -26,6 +26,24 @@
|
|||||||
completed with 22 passing tests.
|
completed with 22 passing tests.
|
||||||
- `git diff --check` completed cleanly.
|
- `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
|
## Review fixes — round 2
|
||||||
|
|
||||||
- Lifecycle authorization no longer selects the installation-default workspace. The backend now
|
- Lifecycle authorization no longer selects the installation-default workspace. The backend now
|
||||||
|
|||||||
@@ -121,10 +121,8 @@ export function sessionRoutes(
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** RLS makes a foreign session indistinguishable from a missing one. */
|
/** RLS makes a foreign session indistinguishable from a missing one. */
|
||||||
const authorize = async (principal: PrincipalContext, id: string): Promise<LocatedSession | undefined> => {
|
const authorize = async (principal: PrincipalContext, id: string): Promise<LocatedSession | undefined> =>
|
||||||
const located = await locateSession(principal, id);
|
await locateSession(principal, id);
|
||||||
return located && await resolveSessionWorkspace(located);
|
|
||||||
};
|
|
||||||
|
|
||||||
const storageFailure = (reply: any) => reply.code(503).send({ error: "session storage is unavailable" });
|
const storageFailure = (reply: any) => reply.code(503).send({ error: "session storage is unavailable" });
|
||||||
const lifecycleFailure = (reply: any, error: unknown) =>
|
const lifecycleFailure = (reply: any, error: unknown) =>
|
||||||
|
|||||||
@@ -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" });
|
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 () => {
|
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 readPinned = vi.fn(async () => { throw new Error("must not resolve"); });
|
||||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||||
|
|||||||
Reference in New Issue
Block a user