feat: pin sessions to workspace revisions
This commit is contained in:
@@ -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: () => {} };
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user