feat: pin sessions to workspace revisions

This commit is contained in:
2026-08-04 04:52:06 +02:00
parent 2573d87a0e
commit 90894176b6
13 changed files with 313 additions and 80 deletions
+112 -2
View File
@@ -1,4 +1,4 @@
import { test, expect } from "vitest";
import { test, expect, vi } from "vitest";
import { spawn as nodeSpawn } from "node:child_process";
import path from "node:path";
import os from "node:os";
@@ -148,6 +148,44 @@ test("new sessions are created through the authenticated principal, not a client
expect(principal).toMatchObject({ issuer: "portal", subject: "alice" });
});
test("creates a session from the active immutable workspace revision", async () => {
const sessionNew = vi.fn(async () => ({ id: "pinned" }));
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: { sessionNew, searchPack: async () => {} } as any,
readiness: { ensure: async () => ({ ok: true }) } as any,
mgr: {
get: () => undefined,
createFor: () => ({ bridge: { onClientEvent: () => {} } }),
configure: async () => {},
start: () => {},
} as any,
getSettings: () => ({ 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: vi.fn(async () => ({
workspace: {
workspace: { schema_version: 2, id: "psd-clinical", name: "PSD", language: "it" },
dwh: {}, semantic_index: {}, llm_policy: { allowed: ["zai/glm-5.2"] },
},
revision: {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: "/data/workspace-registry/snapshots/abc/psd-clinical.yaml", state: "operational",
},
})),
} as any,
});
await app.inject({
method: "POST", url: "/sessions",
payload: { question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low" },
});
expect(sessionNew).toHaveBeenCalledWith(expect.objectContaining({
workspaceConfigPath: "/data/workspace-registry/snapshots/abc/psd-clinical.yaml",
workspaceId: "psd-clinical", workspaceRevision: "a".repeat(40),
}));
});
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 });
@@ -171,7 +209,7 @@ test("POST /sessions usa i settings (workspace/provider/model/thinking) e crea+a
});
const created = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
expect(created.json()).toEqual({ id: "s1" });
expect(sessionNewArg.workspace).toBe("w");
expect(sessionNewArg.workspaceConfigPath).toBe("w");
expect(sessionNewArg.provider).toBe("zai");
expect(sessionNewArg.model).toBe("glm-5.2");
expect(sessionNewArg.thinking).toBe("high");
@@ -373,6 +411,78 @@ test("POST /sessions/:id/resume configura Pi con il thinking persistito", async
expect(configured.thinking).toBe("medium");
});
test("POST /sessions/:id/resume uses the manifest's retained workspace revision", async () => {
const reopenSession = vi.fn(async () => {});
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
mgr: {
get: () => undefined,
createFor: () => ({ bridge: { onClientEvent: () => {} } }),
configure: async () => {}, start: () => {},
} as any,
thtRunner: {
sessionShow: async () => ({
status: "open", archived: false, workspace_id: "psd-clinical", workspace_revision: "a".repeat(40),
}),
reopenSession,
} as any,
readiness: { ensure: async () => ({ ok: true }) } as any,
getSettings: () => ({ workspace: "legacy" }) as any,
workspaceRegistry: {
readPinned: vi.fn(async () => ({
workspace: { workspace: { id: "psd-clinical" } },
revision: {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: "/data/workspace-registry/snapshots/aaaaaaaa/psd-clinical.yaml", state: "operational",
},
})),
} as any,
});
const response = await app.inject({ method: "POST", url: "/sessions/pinned/resume" });
expect(response.statusCode).toBe(200);
expect(reopenSession).toHaveBeenCalledWith(
"pinned", "/data/workspace-registry/snapshots/aaaaaaaa/psd-clinical.yaml",
);
});
test("POST /sessions/:id/resume returns a sanitized error when its retained revision is unavailable", async () => {
const rawFailure = "cannot read /data/workspace-registry/snapshots/secret-revision";
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: {
sessionShow: async () => ({
status: "open", archived: false, workspace_id: "psd-clinical", workspace_revision: "a".repeat(40),
}),
} as any,
getSettings: () => ({ workspace: "legacy" }) as any,
workspaceRegistry: { readPinned: async () => { throw new Error(rawFailure); } } as any,
});
const response = await app.inject({ method: "POST", url: "/sessions/pinned/resume" });
expect(response.statusCode).toBe(409);
expect(response.body).not.toContain(rawFailure);
expect(response.json()).toMatchObject({ code: "workspace_revision_unavailable" });
});
test("GET /sessions/:id warns when a legacy manifest has no workspace revision", async () => {
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
mgr: {
get: () => undefined,
createFor: () => ({ bridge: { onClientEvent: () => {} } }),
configure: async () => {}, start: () => {},
} as any,
thtRunner: { sessionShow: async () => ({ status: "open", archived: false }), reopenSession: async () => {} } as any,
readiness: { ensure: async () => ({ ok: true }) } as any,
getSettings: () => ({ workspace: "legacy" }) as any,
});
const response = await app.inject({ method: "GET", url: "/sessions/legacy" });
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({ warning: expect.stringMatching(/legacy/i) });
});
test("POST /sessions/:id/resume usa il thinking globale se manca nel manifest", async () => {
let configured: any;
const bridge = { onClientEvent: () => {}, emitClientEvent: () => {} };
+13 -10
View File
@@ -31,8 +31,8 @@ test("GET /settings returns effective defaults (env provider/model/thinking, fir
}
});
test("PUT /settings persists and GET reads it back", async () => {
const { app, dir } = appWithTmpSettings({}, {
test("PUT /settings does not persist personal workspace or LLM choices", async () => {
const { app, dir } = appWithTmpSettings({ PI_PROVIDER: "zai", PI_MODEL: "glm-5.2", PI_THINKING: "medium" }, {
listModels: async () => [{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true }],
});
try {
@@ -42,13 +42,14 @@ test("PUT /settings persists and GET reads it back", async () => {
});
expect(put.statusCode).toBe(200);
const got = await app.inject({ method: "GET", url: "/settings" });
expect(got.json()).toMatchObject({ workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "high" });
expect(got.json()).toMatchObject({ provider: "zai", model: "glm-5.2", thinking: "medium" });
expect(got.json()).not.toMatchObject({ workspace: "psd", thinking: "high" });
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("settings are isolated by the authenticated repository principal", async () => {
test("settings no longer read or write principal-specific preferences", async () => {
const preferences = new Map<string, any>();
const runner = {
withPrincipal: (principal: any) => ({
@@ -73,11 +74,11 @@ test("settings are isolated by the authenticated repository principal", async ()
const alice = await app.inject({ method: "GET", url: "/settings", headers: headers("alice") });
const bob = await app.inject({ method: "GET", url: "/settings", headers: headers("bob") });
expect(alice.json()).toMatchObject({ workspace: "psd", thinking: "high" });
expect(bob.json()).not.toMatchObject({ workspace: "psd", thinking: "high" });
expect(alice.json()).toEqual(bob.json());
expect(preferences.size).toBe(0);
});
test("GET /settings seeds an empty private profile from complete legacy settings once", async () => {
test("GET /settings retains complete legacy installation defaults without seeding a private profile", async () => {
let preferences: Record<string, unknown> = {};
const writes: Record<string, unknown>[] = [];
const runner = {
@@ -104,13 +105,13 @@ test("GET /settings seeds an empty private profile from complete legacy settings
expect(first.statusCode).toBe(200);
expect(first.json()).toEqual(expected);
expect(second.json()).toEqual(expected);
expect(writes).toEqual([expected]);
expect(writes).toEqual([]);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("GET /settings does not overwrite an existing private profile with legacy settings", async () => {
test("GET /settings ignores stale private preferences in favor of installation defaults", async () => {
const privateSettings = {
workspace: "private", provider: "zai", model: "glm-5.2", thinking: "high",
};
@@ -130,7 +131,9 @@ test("GET /settings does not overwrite an existing private profile with legacy s
const response = await app.inject({ method: "GET", url: "/settings" });
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual(privateSettings);
expect(response.json()).toEqual({
workspace: "local", provider: "local-qwen", model: "qwen3.6-35b-a3b", thinking: "low",
});
} finally {
rmSync(dir, { recursive: true, force: true });
}