feat: pin sessions to workspace revisions
This commit is contained in:
@@ -11,6 +11,10 @@ const FAKE = path.resolve("../harness/tests/fake_pi/fake_pi_rpc.mjs");
|
||||
const SCRIPT = path.resolve("../harness/tests/fake_pi/scripts/f1_disambiguation.json");
|
||||
|
||||
const defaultWorkspaceRegistry = {
|
||||
list: vi.fn(async () => [{
|
||||
id: "default", commit: "e".repeat(40), blob: "f".repeat(40),
|
||||
snapshotPath: `/data/workspace-registry/snapshots/${"e".repeat(40)}/default.yaml`, state: "operational",
|
||||
}]),
|
||||
read: vi.fn(async (id: string) => ({
|
||||
workspace: {
|
||||
llm_policy: {
|
||||
@@ -25,7 +29,10 @@ const defaultWorkspaceRegistry = {
|
||||
};
|
||||
|
||||
function buildApp(config: Parameters<typeof buildRealApp>[0], deps: Record<string, unknown> = {}) {
|
||||
return buildRealApp(config, { workspaceRegistry: defaultWorkspaceRegistry as any, ...deps } as any);
|
||||
return buildRealApp(config, {
|
||||
...deps,
|
||||
workspaceRegistry: { ...defaultWorkspaceRegistry, ...(deps.workspaceRegistry as object | undefined) },
|
||||
} as any);
|
||||
}
|
||||
|
||||
function mutApp(thtRunner: any) {
|
||||
@@ -233,6 +240,90 @@ test("creates a session from the configured default workspace revision when work
|
||||
}));
|
||||
});
|
||||
|
||||
test("session lifecycle locates a B session when installation default is A", async () => {
|
||||
const aPath = "/registry/snapshots/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a-workspace.yaml";
|
||||
const bPath = "/registry/snapshots/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/b-workspace.yaml";
|
||||
const bPinnedPath = "/registry/snapshots/cccccccccccccccccccccccccccccccccccccccc/b-workspace.yaml";
|
||||
const bManifest = {
|
||||
id: "session-b", status: "open", archived: false,
|
||||
workspace_id: "b-workspace", workspace_revision: "c".repeat(40),
|
||||
provider: "zai", model: "glm-5.2", thinking: "low",
|
||||
};
|
||||
const calls: string[] = [];
|
||||
let active: any;
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {
|
||||
sessionNew: async (input: any) => {
|
||||
calls.push(`new:${input.workspaceConfigPath}`);
|
||||
return { id: "session-b" };
|
||||
},
|
||||
searchPack: async () => {},
|
||||
sessionList: async (workspace: string) => {
|
||||
calls.push(`list:${workspace}`);
|
||||
return workspace === bPath ? [{ id: "session-b", status: "open", question: "B question" }] : [];
|
||||
},
|
||||
sessionShow: async (id: string, workspace: string) => {
|
||||
calls.push(`show:${workspace}`);
|
||||
if (id === "session-b" && workspace === bPath) return bManifest;
|
||||
throw new Error("session not found");
|
||||
},
|
||||
reopenSession: async (id: string, workspace: string) => {
|
||||
calls.push(`reopen:${workspace}`);
|
||||
expect(id).toBe("session-b");
|
||||
},
|
||||
} as any,
|
||||
readiness: { ensure: async () => ({ ok: true }) } as any,
|
||||
mgr: {
|
||||
get: () => active,
|
||||
createFor: () => {
|
||||
active = { bridge: { onClientEvent: () => {}, respond: () => true, turnState: () => "idle" } };
|
||||
return active;
|
||||
},
|
||||
configure: async () => {}, start: () => {},
|
||||
teardownForPrincipal: () => [],
|
||||
teardownIfCurrent: (_id: string, expected: any) => {
|
||||
if (active !== expected) return false;
|
||||
active = undefined;
|
||||
return true;
|
||||
},
|
||||
} as any,
|
||||
getSettings: () => ({ workspace: "a-workspace", provider: "zai", model: "glm-5.2", thinking: "low" }) as any,
|
||||
listModels: async () => [{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true }],
|
||||
workspaceRegistry: {
|
||||
read: async (id: string) => ({
|
||||
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
|
||||
revision: { id, commit: "b".repeat(40), blob: "d".repeat(40), snapshotPath: bPath, state: "operational" },
|
||||
}),
|
||||
list: async () => [
|
||||
{ id: "a-workspace", commit: "a".repeat(40), blob: "a".repeat(40), snapshotPath: aPath, state: "operational" },
|
||||
{ id: "b-workspace", commit: "b".repeat(40), blob: "b".repeat(40), snapshotPath: bPath, state: "operational" },
|
||||
],
|
||||
readPinned: vi.fn(async (id: string, revision: string) => {
|
||||
expect([id, revision]).toEqual(["b-workspace", "c".repeat(40)]);
|
||||
return { workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } }, workspaceConfigPath: bPinnedPath };
|
||||
}),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect((await app.inject({ method: "POST", url: "/sessions", payload: {
|
||||
question: "B question", workspaceId: "b-workspace", provider: "zai", model: "glm-5.2", thinking: "low",
|
||||
} })).statusCode).toBe(200);
|
||||
expect((await app.inject({ method: "GET", url: "/sessions" })).json()).toEqual([
|
||||
expect.objectContaining({ id: "session-b", active: true }),
|
||||
]);
|
||||
expect((await app.inject({ method: "GET", url: "/sessions/session-b" })).json()).toMatchObject(bManifest);
|
||||
expect((await app.inject({ method: "POST", url: "/sessions/session-b/response", payload: { ui_response: {} } })).statusCode)
|
||||
.toBe(204);
|
||||
|
||||
active = undefined;
|
||||
expect((await app.inject({ method: "POST", url: "/sessions/session-b/resume" })).json())
|
||||
.toEqual({ id: "session-b", alreadyActive: false });
|
||||
expect(calls).toContain(`new:${bPath}`);
|
||||
expect(calls).toContain(`list:${bPath}`);
|
||||
expect(calls).toContain(`show:${bPath}`);
|
||||
expect(calls).toContain(`reopen:${bPinnedPath}`);
|
||||
});
|
||||
|
||||
test("POST /sessions usa i settings (workspace/provider/model/thinking) e crea+avvia", async () => {
|
||||
const modelKey = path.join(os.tmpdir(), `thoth-model-key-${process.pid}`);
|
||||
writeFileSync(modelKey, "test-model-key", { mode: 0o600 });
|
||||
@@ -1707,8 +1798,9 @@ test("POST /sessions/:id/rename calls setName", async () => {
|
||||
expect(arg).toEqual({ id: "s1", name: "N" });
|
||||
});
|
||||
|
||||
test("rename authorizes and mutates through the same selected workspace", async () => {
|
||||
test("rename authorizes and mutates through the same registry snapshot", async () => {
|
||||
const workspaces: string[] = [];
|
||||
const tenantPath = "/registry/snapshots/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/tenant-a.yaml";
|
||||
const app = buildApp(loadConfig({ AUTH_MODE: "upstream", THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {
|
||||
withPrincipal: () => ({
|
||||
@@ -1716,7 +1808,11 @@ test("rename authorizes and mutates through the same selected workspace", async
|
||||
setName: async (_id: string, _name: string, workspace: string) => { workspaces.push(`set:${workspace}`); },
|
||||
}),
|
||||
} as any,
|
||||
getSettings: () => ({ workspace: "tenant-a" }) as any,
|
||||
workspaceRegistry: {
|
||||
list: async () => [{
|
||||
id: "tenant-a", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: tenantPath, state: "operational",
|
||||
}],
|
||||
} as any,
|
||||
});
|
||||
const res = await app.inject({
|
||||
method: "POST", url: "/sessions/s1/rename", payload: { name: "N" },
|
||||
@@ -1725,7 +1821,7 @@ test("rename authorizes and mutates through the same selected workspace", async
|
||||
},
|
||||
});
|
||||
expect(res.statusCode).toBe(204);
|
||||
expect(workspaces).toEqual(["show:tenant-a", "set:tenant-a"]);
|
||||
expect(workspaces).toEqual([`show:${tenantPath}`, `set:${tenantPath}`]);
|
||||
});
|
||||
|
||||
test("POST /sessions/:id/group calls setGroup", async () => {
|
||||
|
||||
Reference in New Issue
Block a user