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: () => {} };
|
||||
|
||||
Reference in New Issue
Block a user