refactor: remove legacy workspace UI state

This commit is contained in:
2026-08-10 22:56:00 +02:00
parent fc19b0d67c
commit 90684c3280
12 changed files with 283 additions and 130 deletions
+91 -4
View File
@@ -15,12 +15,12 @@ test("createSession migrates legacy selections and POSTs browser preferences", a
workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low", workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
})), })),
http.get("/api/workspaces", () => HttpResponse.json([{ http.get("/api/workspaces", () => HttpResponse.json([{
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en",
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" }, revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot" },
}])), }])),
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd-clinical"), workspace: canonicalWorkspaceFixture("psd-clinical"),
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" }, revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot" },
})), })),
http.post("/api/sessions", async ({ request }) => { http.post("/api/sessions", async ({ request }) => {
body = await request.json(); body = await request.json();
@@ -36,6 +36,93 @@ test("createSession migrates legacy selections and POSTs browser preferences", a
}); });
}); });
test("createSession does not POST when a selected summary aliases another workspace name", async () => {
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
}));
let posted = false;
server.use(
http.get("/api/workspaces", () => HttpResponse.json([{
id: "psd-clinical", name: "other-workspace", file: "psd-clinical.yaml",
displayName: "PSD Clinical", language: "en",
revision: {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
},
}])),
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd-clinical"),
revision: {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
},
})),
http.post("/api/sessions", () => {
posted = true;
return HttpResponse.json({ id: "s1" });
}),
);
await expect(createSession({ question: "q" })).rejects.toMatchObject({
message: "Could not load workspace registry. Please retry.",
});
expect(posted).toBe(false);
});
test.each([
["historical state", { state: "operational" }],
["an unknown revision field", { generation: 1 }],
["a malformed revision", { commit: "not-a-commit" }],
])("createSession does not POST when the selected summary revision has %s", async (_case, revisionPatch) => {
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
}));
const revision = {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: "/snapshot", ...revisionPatch,
};
let posted = false;
server.use(
http.get("/api/workspaces", () => HttpResponse.json([{
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml",
displayName: "PSD Clinical", language: "en", revision,
}])),
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd-clinical"),
revision: {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
},
})),
http.post("/api/sessions", () => {
posted = true;
return HttpResponse.json({ id: "s1" });
}),
);
await expect(createSession({ question: "q" })).rejects.toMatchObject({
message: "Could not load workspace registry. Please retry.",
});
expect(posted).toBe(false);
});
test("createSession preserves allowLegacy for a selected session workspace absent from registry summaries", async () => {
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
workspaceId: "retained-session-workspace", provider: "zai", model: "glm-5.2", thinking: "low",
}));
let body: unknown;
server.use(
http.get("/api/workspaces", () => HttpResponse.json([])),
http.post("/api/sessions", async ({ request }) => {
body = await request.json();
return HttpResponse.json({ id: "s1" });
}),
);
await expect(createSession({ question: "q" })).resolves.toEqual({ id: "s1" });
expect(body).toEqual({
question: "q", workspaceId: "retained-session-workspace",
provider: "zai", model: "glm-5.2", thinking: "low",
});
});
test("createSession rejects a workspace summary that omits the canonical revision", async () => { test("createSession rejects a workspace summary that omits the canonical revision", async () => {
localStorage.clear(); localStorage.clear();
let posted = false; let posted = false;
@@ -44,7 +131,7 @@ test("createSession rejects a workspace summary that omits the canonical revisio
workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low", workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
})), })),
http.get("/api/workspaces", () => HttpResponse.json([{ http.get("/api/workspaces", () => HttpResponse.json([{
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en",
}])), }])),
http.post("/api/sessions", async () => { http.post("/api/sessions", async () => {
posted = true; posted = true;
-4
View File
@@ -63,10 +63,6 @@ async function ensureWorkspaceSelectionPolicy(): Promise<WorkspacePreference> {
workspacePolicyGate.reject(workspaceId); workspacePolicyGate.reject(workspaceId);
throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR); throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR);
} }
if (workspace?.revision?.state === "migration_required") {
workspacePolicyGate.rejectSummary(workspaceId);
throw new WorkspaceSelectionError(WORKSPACE_POLICY_ERROR);
}
if (!workspace?.revision) { if (!workspace?.revision) {
workspacePolicyGate.allowLegacy(workspaceId); workspacePolicyGate.allowLegacy(workspaceId);
return workspacePreferences.load(); return workspacePreferences.load();
+96 -2
View File
@@ -2,7 +2,7 @@ import { expect, test } from "vitest";
import { http, HttpResponse } from "msw"; import { http, HttpResponse } from "msw";
import { server } from "../test/msw"; import { server } from "../test/msw";
import { import {
asWorkspaceConflict, getWorkspace, importWorkspace, publishWorkspace, validateWorkspace, asWorkspaceConflict, getWorkspace, importWorkspace, listWorkspaces, publishWorkspace, validateWorkspace,
type CanonicalWorkspace, type CanonicalWorkspace,
} from "./workspaces"; } from "./workspaces";
@@ -34,9 +34,74 @@ const revision = {
commit: "a".repeat(40), commit: "a".repeat(40),
blob: "b".repeat(40), blob: "b".repeat(40),
snapshotPath: "workspaces/psd-clinical.yaml", snapshotPath: "workspaces/psd-clinical.yaml",
state: "operational" as const,
}; };
const summary = {
id: "psd-clinical",
name: "psd-clinical",
file: "psd-clinical.yaml",
displayName: "PSD Clinical",
description: "Clinical workspace",
language: "en" as const,
revision,
};
test("decodes and normalizes state-free workspace summaries without passing through backend-only fields", async () => {
server.use(http.get("/api/workspaces", () => HttpResponse.json([{
...summary, workspace, backendOnly: "ignored",
}])));
await expect(listWorkspaces()).resolves.toEqual([summary]);
});
test("accepts internally multiline display names and descriptions using backend trim semantics", async () => {
const multiline = {
...summary,
displayName: "PSD\nClinical",
description: "First line\n\tSecond line",
};
server.use(http.get("/api/workspaces", () => HttpResponse.json([multiline])));
await expect(listWorkspaces()).resolves.toEqual([multiline]);
});
test.each([
["an id/name alias", { ...summary, name: "other-workspace" }],
["a non-canonical selector file", { ...summary, file: "workspaces/psd-clinical.yaml" }],
])("rejects workspace summaries with %s", async (_case, malformedSummary) => {
server.use(http.get("/api/workspaces", () => HttpResponse.json([malformedSummary])));
await expect(listWorkspaces()).rejects.toThrow("invalid workspace summary");
});
test.each([
["historical state", { ...revision, state: "operational" }],
["unknown revision field", { ...revision, generation: 1 }],
["malformed revision", { ...revision, commit: "not-a-commit" }],
])("rejects workspace summaries with %s", async (_case, malformedRevision) => {
server.use(http.get("/api/workspaces", () => HttpResponse.json([{
...summary, revision: malformedRevision,
}])));
await expect(listWorkspaces()).rejects.toThrow("invalid workspace summary");
});
test.each([
["a non-array response", { ...summary }],
["a malformed selector field", [{ ...summary, language: "fr" }]],
])("rejects %s from the workspace summary API", async (_case, response) => {
server.use(http.get("/api/workspaces", () => HttpResponse.json(response)));
await expect(listWorkspaces()).rejects.toThrow("invalid workspace summary");
});
test("preserves a present summary without revision so callers can distinguish it from an absent workspace", async () => {
const { revision: _revision, ...incomplete } = summary;
server.use(http.get("/api/workspaces", () => HttpResponse.json([incomplete])));
await expect(listWorkspaces()).resolves.toEqual([incomplete]);
});
test("uploads a workspace bundle without JSON content type", async () => { test("uploads a workspace bundle without JSON content type", async () => {
let contentType: string | null = null; let contentType: string | null = null;
server.use(http.post("/api/workspaces/import", ({ request }) => { server.use(http.post("/api/workspaces/import", ({ request }) => {
@@ -76,6 +141,35 @@ test("rejects imported Evidence with a secret-shaped field", async () => {
.rejects.toThrow("invalid imported workspace draft"); .rejects.toThrow("invalid imported workspace draft");
}); });
test("accepts the atomic schema-v3 workspace revision contract without historical state", async () => {
const stateFreeRevision = {
id: "psd-clinical",
commit: "a".repeat(40),
blob: "b".repeat(40),
snapshotPath: "workspaces/psd-clinical.yaml",
};
server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
workspace, revision: stateFreeRevision,
})));
await expect(getWorkspace("psd-clinical")).resolves.toEqual({ workspace, revision: stateFreeRevision });
});
test("rejects the removed historical workspace revision state as an extra API key", async () => {
server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
workspace,
revision: {
id: "psd-clinical",
commit: "a".repeat(40),
blob: "b".repeat(40),
snapshotPath: "workspaces/psd-clinical.yaml",
state: "operational",
},
})));
await expect(getWorkspace("psd-clinical")).rejects.toThrow("invalid workspace revision");
});
test("rejects read responses with a missing or inconsistent revision", async () => { test("rejects read responses with a missing or inconsistent revision", async () => {
server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
workspace: evidenceWorkspace, workspace: evidenceWorkspace,
+45 -7
View File
@@ -99,7 +99,6 @@ export interface WorkspaceRevision {
commit: string; commit: string;
blob: string; blob: string;
snapshotPath: string; snapshotPath: string;
state: "operational" | "migration_required";
} }
export interface WorkspaceSummary { export interface WorkspaceSummary {
@@ -110,7 +109,7 @@ export interface WorkspaceSummary {
displayName: string; displayName: string;
description?: string; description?: string;
language: "en" | "it"; language: "en" | "it";
revision: WorkspaceRevision; revision?: WorkspaceRevision;
} }
export interface WorkspaceRecord { export interface WorkspaceRecord {
@@ -206,9 +205,10 @@ function exactObject(value: unknown, keys: readonly string[]): Record<string, un
} }
function workspaceRevision(value: unknown, expectedId: string): WorkspaceRevision | undefined { function workspaceRevision(value: unknown, expectedId: string): WorkspaceRevision | undefined {
const source = exactObject(value, ["id", "commit", "blob", "snapshotPath", "state"]); // Frontend and core deploy atomically: accept only the schema-v3, state-free contract.
const source = exactObject(value, ["id", "commit", "blob", "snapshotPath"]);
if (!source) return undefined; if (!source) return undefined;
const { id, commit, blob, snapshotPath, state } = source; const { id, commit, blob, snapshotPath } = source;
if ( if (
id !== expectedId id !== expectedId
|| typeof id !== "string" || !/^[a-z][a-z0-9-]{2,62}$/.test(id) || typeof id !== "string" || !/^[a-z][a-z0-9-]{2,62}$/.test(id)
@@ -216,9 +216,39 @@ function workspaceRevision(value: unknown, expectedId: string): WorkspaceRevisio
|| typeof blob !== "string" || !/^[0-9a-f]{40}$/.test(blob) || typeof blob !== "string" || !/^[0-9a-f]{40}$/.test(blob)
|| typeof snapshotPath !== "string" || snapshotPath.length === 0 || typeof snapshotPath !== "string" || snapshotPath.length === 0
|| snapshotPath.trim() !== snapshotPath || /[\u0000-\u001f\u007f]/u.test(snapshotPath) || snapshotPath.trim() !== snapshotPath || /[\u0000-\u001f\u007f]/u.test(snapshotPath)
|| (state !== "operational" && state !== "migration_required")
) return undefined; ) return undefined;
return { id, commit, blob, snapshotPath, state }; return { id, commit, blob, snapshotPath };
}
function workspaceSummary(value: unknown): WorkspaceSummary | undefined {
const source = object(value);
if (!source) return undefined;
const { id, name, file, displayName, description, language } = source;
// Summary prose follows the producer's z.string().trim().min(1) contract;
// path-like revision fields retain their stricter control-character checks above.
const validText = (candidate: unknown) => typeof candidate === "string"
&& candidate.length > 0
&& candidate.trim() === candidate;
if (
typeof id !== "string" || !/^[a-z][a-z0-9-]{2,62}$/.test(id)
|| name !== id || file !== `${id}.yaml`
|| !validText(displayName)
|| (description !== undefined && !validText(description))
|| (language !== "en" && language !== "it")
) return undefined;
const revision = source.revision === undefined
? undefined
: workspaceRevision(source.revision, id);
if (source.revision !== undefined && !revision) return undefined;
return {
id,
name: name as string,
file: file as string,
displayName: displayName as string,
...(description === undefined ? {} : { description: description as string }),
language,
...(revision ? { revision } : {}),
};
} }
function requireWorkspaceRevision(value: unknown, expectedId: string): WorkspaceRevision { function requireWorkspaceRevision(value: unknown, expectedId: string): WorkspaceRevision {
@@ -280,7 +310,15 @@ function requireCanonicalWorkspace(value: unknown): CanonicalWorkspace {
return workspace; return workspace;
} }
export const listWorkspaces = () => apiFetch<WorkspaceSummary[]>("/workspaces"); export const listWorkspaces = async (): Promise<WorkspaceSummary[]> => {
const response = await apiFetch<unknown>("/workspaces");
if (!Array.isArray(response)) throw new Error("Workspace API returned an invalid workspace summary");
const summaries = response.map(workspaceSummary);
if (summaries.some((summary) => !summary)) {
throw new Error("Workspace API returned an invalid workspace summary");
}
return summaries as WorkspaceSummary[];
};
export const getWorkspace = async (id: string): Promise<WorkspaceRecord> => { export const getWorkspace = async (id: string): Promise<WorkspaceRecord> => {
const response = await apiFetch<unknown>(`/workspaces/${encodeURIComponent(id)}`); const response = await apiFetch<unknown>(`/workspaces/${encodeURIComponent(id)}`);
const source = object(response); const source = object(response);
+4 -4
View File
@@ -36,8 +36,8 @@ test("submitting includes browser-local migrated preferences and calls onCreated
workspace: "default", provider: "zai", model: "glm-5.2", thinking: "low", workspace: "default", provider: "zai", model: "glm-5.2", thinking: "low",
})), })),
http.get("/api/workspaces", () => HttpResponse.json([{ http.get("/api/workspaces", () => HttpResponse.json([{
id: "default", name: "default", file: "default.yaml", displayName: "Default", id: "default", name: "default", file: "default.yaml", displayName: "Default", language: "en",
revision: { state: "operational" }, revision: workspaceRevisionFixture("default"),
}])), }])),
http.get("/api/workspaces/default", () => HttpResponse.json({ http.get("/api/workspaces/default", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("default", ["zai/glm-5.2"], "zai/glm-5.2"), workspace: canonicalWorkspaceFixture("default", ["zai/glm-5.2"], "zai/glm-5.2"),
@@ -67,7 +67,7 @@ test("first-run direct dialog creation waits for registry policy without a mount
let policyRequestStarted = false; let policyRequestStarted = false;
const policyMayFinish = new Promise<void>((resolve) => { releasePolicy = resolve; }); const policyMayFinish = new Promise<void>((resolve) => { releasePolicy = resolve; });
const revision = { const revision = {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const, id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
}; };
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({ localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
workspaceId: "psd-clinical", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium", workspaceId: "psd-clinical", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
@@ -76,7 +76,7 @@ test("first-run direct dialog creation waits for registry policy without a mount
http.get("/api/workspaces", () => { http.get("/api/workspaces", () => {
summaryRequestStarted = true; summaryRequestStarted = true;
return HttpResponse.json([{ return HttpResponse.json([{
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en",
revision, revision,
}]); }]);
}), }),
+24 -64
View File
@@ -24,12 +24,12 @@ test("new sessions send the browser-selected workspace, model, provider, and thi
})); }));
server.use( server.use(
http.get("/api/workspaces", () => HttpResponse.json([{ http.get("/api/workspaces", () => HttpResponse.json([{
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en",
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" }, revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot" },
}])), }])),
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd-clinical"), workspace: canonicalWorkspaceFixture("psd-clinical"),
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" }, revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot" },
})), })),
http.post("/api/sessions", async ({ request }) => { http.post("/api/sessions", async ({ request }) => {
body = await request.json(); body = await request.json();
@@ -135,10 +135,13 @@ test("footer shows cumulative k-token counters after workspace and context gauge
http.get("/api/settings", () => HttpResponse.json({ http.get("/api/settings", () => HttpResponse.json({
workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "medium", workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "medium",
})), })),
http.get("/api/workspaces", () => HttpResponse.json([{ name: "psd" }])), http.get("/api/workspaces", () => HttpResponse.json([{
id: "psd", name: "psd", file: "psd.yaml", displayName: "PSD", language: "en",
revision: { id: "psd", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot" },
}])),
http.get("/api/workspaces/psd", () => HttpResponse.json({ http.get("/api/workspaces/psd", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd"), workspace: canonicalWorkspaceFixture("psd"),
revision: { id: "psd", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" }, revision: { id: "psd", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot" },
})), })),
http.get("/api/models", () => HttpResponse.json({ http.get("/api/models", () => HttpResponse.json({
models: [{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true }], models: [{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true }],
@@ -172,12 +175,12 @@ test("footer limits model choices to the selected workspace policy", async () =>
workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium", workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
})), })),
http.get("/api/workspaces", () => HttpResponse.json([{ http.get("/api/workspaces", () => HttpResponse.json([{
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en",
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" }, revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot" },
}])), }])),
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd-clinical"), workspace: canonicalWorkspaceFixture("psd-clinical"),
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" }, revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot" },
})), })),
http.get("/api/models", () => HttpResponse.json({ models: [ http.get("/api/models", () => HttpResponse.json({ models: [
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true }, { provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
@@ -198,15 +201,15 @@ test("switching workspaces replaces an out-of-policy model before session creati
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium", workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
})); }));
const revision = (id: string) => ({ const revision = (id: string) => ({
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const, id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
}); });
server.use( server.use(
http.get("/api/settings", () => HttpResponse.json({ http.get("/api/settings", () => HttpResponse.json({
workspace: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium", workspace: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
})), })),
http.get("/api/workspaces", () => HttpResponse.json([ http.get("/api/workspaces", () => HttpResponse.json([
{ id: "research", name: "research", file: "research.yaml", displayName: "Research", revision: revision("research") }, { id: "research", name: "research", file: "research.yaml", displayName: "Research", language: "en", revision: revision("research") },
{ id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", revision: revision("psd-clinical") }, { id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en", revision: revision("psd-clinical") },
])), ])),
http.get("/api/workspaces/research", () => HttpResponse.json({ http.get("/api/workspaces/research", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("research", ["deepseek/deepseek-v4-pro"]), workspace: canonicalWorkspaceFixture("research", ["deepseek/deepseek-v4-pro"]),
@@ -251,13 +254,13 @@ test("immediate submit waits for a switched workspace policy before creating a s
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium", workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
})); }));
const revision = (id: string) => ({ const revision = (id: string) => ({
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const, id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
}); });
server.use( server.use(
http.get("/api/settings", () => HttpResponse.json({ workspace: "research" })), http.get("/api/settings", () => HttpResponse.json({ workspace: "research" })),
http.get("/api/workspaces", () => HttpResponse.json([ http.get("/api/workspaces", () => HttpResponse.json([
{ id: "research", name: "research", file: "research.yaml", displayName: "Research", revision: revision("research") }, { id: "research", name: "research", file: "research.yaml", displayName: "Research", language: "en", revision: revision("research") },
{ id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", revision: revision("psd-clinical") }, { id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en", revision: revision("psd-clinical") },
])), ])),
http.get("/api/workspaces/research", () => HttpResponse.json({ http.get("/api/workspaces/research", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("research", ["deepseek/deepseek-v4-pro"]), revision: revision("research"), workspace: canonicalWorkspaceFixture("research", ["deepseek/deepseek-v4-pro"]), revision: revision("research"),
@@ -307,12 +310,12 @@ test("initial restored workspace waits for its delayed policy before creating a
workspaceId: "psd-clinical", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium", workspaceId: "psd-clinical", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
})); }));
const revision = (id: string) => ({ const revision = (id: string) => ({
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const, id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
}); });
server.use( server.use(
http.get("/api/settings", () => HttpResponse.json({ workspace: "psd-clinical" })), http.get("/api/settings", () => HttpResponse.json({ workspace: "psd-clinical" })),
http.get("/api/workspaces", () => HttpResponse.json([ http.get("/api/workspaces", () => HttpResponse.json([
{ id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", revision: revision("psd-clinical") }, { id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en", revision: revision("psd-clinical") },
])), ])),
http.get("/api/workspaces/psd-clinical", async () => { http.get("/api/workspaces/psd-clinical", async () => {
policyRequestStarted = true; policyRequestStarted = true;
@@ -347,49 +350,6 @@ test("initial restored workspace waits for its delayed policy before creating a
})); }));
}); });
test("initial submit rejects a migration-required workspace after summaries load", async () => {
let body: unknown;
let failure: string | undefined;
let releaseSummaries!: () => void;
let summaryRequestStarted = false;
const summariesMayFinish = new Promise<void>((resolve) => { releaseSummaries = resolve; });
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
workspaceId: "legacy-workspace", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
}));
server.use(
http.get("/api/settings", () => HttpResponse.json({ workspace: "legacy-workspace" })),
http.get("/api/workspaces", async () => {
summaryRequestStarted = true;
await summariesMayFinish;
return HttpResponse.json([{
id: "legacy-workspace", name: "legacy-workspace", file: "legacy-workspace.yaml", displayName: "Legacy workspace",
revision: { id: "legacy-workspace", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "migration_required" },
}]);
}),
http.get("/api/workspaces/legacy-workspace", () => new HttpResponse(null, { status: 409 })),
http.get("/api/models", () => HttpResponse.json({ models: [
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
] })),
http.post("/api/sessions", async ({ request }) => {
body = await request.json();
return HttpResponse.json({ id: "s1" });
}),
);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} onSessionCreateFailed={(message) => { failure = message; }} /></QueryClientProvider>);
await waitFor(() => expect(summaryRequestStarted).toBe(true));
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
expect(body).toBeUndefined();
expect(screen.getByRole("button", { name: /send/i })).toBeDisabled();
releaseSummaries();
await waitFor(() => expect(failure).toBe("Could not load selected workspace policy. Please retry."));
expect(body).toBeUndefined();
});
test("initial submit rejects a workspace summary that omits the canonical revision", async () => { test("initial submit rejects a workspace summary that omits the canonical revision", async () => {
let body: unknown; let body: unknown;
let failure: string | undefined; let failure: string | undefined;
@@ -399,7 +359,7 @@ test("initial submit rejects a workspace summary that omits the canonical revisi
server.use( server.use(
http.get("/api/settings", () => HttpResponse.json({ workspace: "broken-workspace" })), http.get("/api/settings", () => HttpResponse.json({ workspace: "broken-workspace" })),
http.get("/api/workspaces", () => HttpResponse.json([{ http.get("/api/workspaces", () => HttpResponse.json([{
id: "broken-workspace", name: "broken-workspace", file: "broken-workspace.yaml", displayName: "Broken workspace", id: "broken-workspace", name: "broken-workspace", file: "broken-workspace.yaml", displayName: "Broken workspace", language: "en",
}])), }])),
http.get("/api/models", () => HttpResponse.json({ models: [ http.get("/api/models", () => HttpResponse.json({ models: [
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true }, { provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
@@ -467,14 +427,14 @@ test("submit follows a rapid workspace switch instead of waiting for an abandone
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium", workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
})); }));
const revision = (id: string) => ({ const revision = (id: string) => ({
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const, id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
}); });
server.use( server.use(
http.get("/api/settings", () => HttpResponse.json({ workspace: "research" })), http.get("/api/settings", () => HttpResponse.json({ workspace: "research" })),
http.get("/api/workspaces", () => HttpResponse.json([ http.get("/api/workspaces", () => HttpResponse.json([
{ id: "research", name: "research", file: "research.yaml", displayName: "Research", revision: revision("research") }, { id: "research", name: "research", file: "research.yaml", displayName: "Research", language: "en", revision: revision("research") },
{ id: "workspace-b", name: "workspace-b", file: "workspace-b.yaml", displayName: "Workspace B", revision: revision("workspace-b") }, { id: "workspace-b", name: "workspace-b", file: "workspace-b.yaml", displayName: "Workspace B", language: "en", revision: revision("workspace-b") },
{ id: "workspace-c", name: "workspace-c", file: "workspace-c.yaml", displayName: "Workspace C", revision: revision("workspace-c") }, { id: "workspace-c", name: "workspace-c", file: "workspace-c.yaml", displayName: "Workspace C", language: "en", revision: revision("workspace-c") },
])), ])),
http.get("/api/workspaces/research", () => HttpResponse.json({ http.get("/api/workspaces/research", () => HttpResponse.json({
workspace: canonicalWorkspaceFixture("research", ["deepseek/deepseek-v4-pro"]), revision: revision("research"), workspace: canonicalWorkspaceFixture("research", ["deepseek/deepseek-v4-pro"]), revision: revision("research"),
+2 -5
View File
@@ -203,8 +203,6 @@ export function ComposerFooter() {
workspacePolicyGate.rejectSummary(workspace); workspacePolicyGate.rejectSummary(workspace);
} else if (selectedWorkspace && !selectedWorkspaceHasRevision) { } else if (selectedWorkspace && !selectedWorkspaceHasRevision) {
workspacePolicyGate.rejectSummary(workspace); workspacePolicyGate.rejectSummary(workspace);
} else if (selectedWorkspace?.revision?.state === "migration_required") {
workspacePolicyGate.rejectSummary(workspace);
} else if (selectedWorkspace?.revision) { } else if (selectedWorkspace?.revision) {
workspacePolicyGate.select(workspace); workspacePolicyGate.select(workspace);
} else { } else {
@@ -243,7 +241,6 @@ export function ComposerFooter() {
if (patch.workspaceId && patch.workspaceId !== workspace) { if (patch.workspaceId && patch.workspaceId !== workspace) {
const selected = workspaces.find((candidate) => candidate.id === patch.workspaceId); const selected = workspaces.find((candidate) => candidate.id === patch.workspaceId);
if (selected && !selected.revision) workspacePolicyGate.rejectSummary(patch.workspaceId); if (selected && !selected.revision) workspacePolicyGate.rejectSummary(patch.workspaceId);
else if (selected?.revision?.state === "migration_required") workspacePolicyGate.rejectSummary(patch.workspaceId);
else if (selected?.revision) workspacePolicyGate.select(patch.workspaceId); else if (selected?.revision) workspacePolicyGate.select(patch.workspaceId);
else workspacePolicyGate.allowLegacy(patch.workspaceId); else workspacePolicyGate.allowLegacy(patch.workspaceId);
} }
@@ -269,8 +266,8 @@ export function ComposerFooter() {
{workspaces.length === 0 ? ( {workspaces.length === 0 ? (
<option value="">— workspace —</option> <option value="">— workspace —</option>
) : ( ) : (
workspaces.filter((w) => w.revision && w.revision.state !== "migration_required").map((w) => ( workspaces.filter((w) => w.revision).map((w) => (
<option key={w.name} value={w.name}> <option key={w.id} value={w.id}>
{w.name} {w.name}
</option> </option>
)) ))
+14 -22
View File
@@ -27,13 +27,13 @@ beforeEach(() => {
http.get("/api/workspace-registry/status", () => http.get("/api/workspace-registry/status", () =>
HttpResponse.json({ branch: "main", head: "a".repeat(40), ahead: 0, behind: 0, degraded: false })), HttpResponse.json({ branch: "main", head: "a".repeat(40), ahead: 0, behind: 0, degraded: false })),
http.get("/api/workspaces", () => HttpResponse.json([{ http.get("/api/workspaces", () => HttpResponse.json([{
id: "psd-clinical", name: "PSD Clinical", displayName: "PSD Clinical", description: "Clinical data", id: "psd-clinical", name: "psd-clinical", displayName: "PSD Clinical", description: "Clinical data",
language: "en", file: "workspaces/psd-clinical.yaml", language: "en", file: "psd-clinical.yaml",
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd", state: "operational" }, revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd" },
}])), }])),
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
workspace, workspace,
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd", state: "operational" }, revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd" },
})), })),
); );
}); });
@@ -163,12 +163,12 @@ test("proposes a different valid ID when duplicating a 63-character workspace ID
const maxWorkspace = { ...workspace, workspace: { ...workspace.workspace, id: maxId } }; const maxWorkspace = { ...workspace, workspace: { ...workspace.workspace, id: maxId } };
server.use( server.use(
http.get("/api/workspaces", () => HttpResponse.json([{ http.get("/api/workspaces", () => HttpResponse.json([{
id: maxId, name: "Maximum", displayName: "Maximum", language: "en", file: `workspaces/${maxId}.yaml`, id: maxId, name: maxId, displayName: "Maximum", language: "en", file: `${maxId}.yaml`,
revision: { id: maxId, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/maximum", state: "operational" }, revision: { id: maxId, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/maximum" },
}])), }])),
http.get(`/api/workspaces/${maxId}`, () => HttpResponse.json({ http.get(`/api/workspaces/${maxId}`, () => HttpResponse.json({
workspace: maxWorkspace, workspace: maxWorkspace,
revision: { id: maxId, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/maximum", state: "operational" }, revision: { id: maxId, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/maximum" },
})), })),
); );
renderManager(); renderManager();
@@ -213,23 +213,15 @@ test("runs validation and installation test with only sanitized messages", async
expect(within(screen.getByTestId("workspace-diagnostics")).queryByText(/password|token|secret/i)).not.toBeInTheDocument(); expect(within(screen.getByTestId("workspace-diagnostics")).queryByText(/password|token|secret/i)).not.toBeInTheDocument();
}); });
test("shows a migration banner for legacy descriptors and does not load editor details for them", async () => { test("loads a state-free registry revision and displays only its commit", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
server.use(
http.get("/api/workspaces", () => HttpResponse.json([
{
id: "psd-clinical", name: "PSD Clinical", displayName: "PSD Clinical", description: "Clinical data",
language: "en", file: "workspaces/psd-clinical.yaml",
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd", state: "migration_required" },
},
])),
);
renderManager(); renderManager();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" })); await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
expect(await screen.findByText("This workspace uses a legacy descriptor and must be migrated to schema v3 before new sessions or publication.")).toBeVisible(); expect(await screen.findByLabelText("Vector collection")).toBeVisible();
expect(screen.queryByLabelText("Vector collection")).not.toBeInTheDocument(); expect(screen.getByText(`Revision ${"a".repeat(12)}`)).toBeVisible();
expect(screen.queryByText(/migration required/i)).not.toBeInTheDocument();
}); });
test("shows an actionable unavailable message when a workspace summary omits its canonical revision", async () => { test("shows an actionable unavailable message when a workspace summary omits its canonical revision", async () => {
@@ -237,8 +229,8 @@ test("shows an actionable unavailable message when a workspace summary omits its
server.use( server.use(
http.get("/api/workspaces", () => HttpResponse.json([ http.get("/api/workspaces", () => HttpResponse.json([
{ {
id: "broken-workspace", name: "Broken workspace", displayName: "Broken workspace", description: "Broken data", id: "broken-workspace", name: "broken-workspace", displayName: "Broken workspace", description: "Broken data",
language: "en", file: "workspaces/broken-workspace.yaml", language: "en", file: "broken-workspace.yaml",
}, },
])), ])),
); );
@@ -288,7 +280,7 @@ test("shows an accessible retry when the selected workspace detail query fails",
calls += 1; calls += 1;
return calls === 1 ? new HttpResponse(null, { status: 503 }) : HttpResponse.json({ return calls === 1 ? new HttpResponse(null, { status: 503 }) : HttpResponse.json({
workspace, workspace,
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd", state: "operational" }, revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd" },
}); });
})); }));
renderManager(); renderManager();
+3 -9
View File
@@ -64,11 +64,10 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
const workspaces = workspacesQuery.data ?? []; const workspaces = workspacesQuery.data ?? [];
const selectedSummary = useMemo(() => workspaces.find((workspace) => workspace.id === selectedId), [selectedId, workspaces]); const selectedSummary = useMemo(() => workspaces.find((workspace) => workspace.id === selectedId), [selectedId, workspaces]);
const selectedSummaryIncomplete = Boolean(selectedSummary && !selectedSummary.revision); const selectedSummaryIncomplete = Boolean(selectedSummary && !selectedSummary.revision);
const selectedIsLegacy = selectedSummary?.revision?.state === "migration_required";
const detailQuery = useQuery({ const detailQuery = useQuery({
queryKey: ["workspace", selectedId], queryKey: ["workspace", selectedId],
queryFn: () => getWorkspace(selectedId!), queryFn: () => getWorkspace(selectedId!),
enabled: Boolean(open && selectedId && !localDraft && !selectedSummaryIncomplete && !selectedIsLegacy), enabled: Boolean(open && selectedId && !localDraft && !selectedSummaryIncomplete),
}); });
const status = statusQuery.data; const status = statusQuery.data;
const record = detailQuery.data; const record = detailQuery.data;
@@ -317,13 +316,8 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
<p className="font-semibold">Workspace summary unavailable</p> <p className="font-semibold">Workspace summary unavailable</p>
<p className="mt-1">This workspace summary is incomplete. Refresh the registry or choose another workspace before creating sessions or editing drafts.</p> <p className="mt-1">This workspace summary is incomplete. Refresh the registry or choose another workspace before creating sessions or editing drafts.</p>
</div> </div>
) : selectedIsLegacy ? (
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm">
<p className="font-semibold">Migration required</p>
<p className="mt-1">This workspace uses a legacy descriptor and must be migrated to schema v3 before new sessions or publication.</p>
</div>
) : detailQuery.isError && selectedId && !localDraft ? <QueryError name="Workspace details failed" message="Could not load workspace details." retryLabel="Retry workspace details" onRetry={() => { void detailQuery.refetch(); }} /> : !currentDraft && !detailQuery.isLoading && <div className="grid min-h-64 place-items-center text-center"><div><h3 className="font-heading font-semibold">Select a workspace</h3><p className="mt-1 text-sm text-muted-foreground">Review an existing definition or start a browser-only draft.</p></div></div>} ) : detailQuery.isError && selectedId && !localDraft ? <QueryError name="Workspace details failed" message="Could not load workspace details." retryLabel="Retry workspace details" onRetry={() => { void detailQuery.refetch(); }} /> : !currentDraft && !detailQuery.isLoading && <div className="grid min-h-64 place-items-center text-center"><div><h3 className="font-heading font-semibold">Select a workspace</h3><p className="mt-1 text-sm text-muted-foreground">Review an existing definition or start a browser-only draft.</p></div></div>}
{!selectedSummaryIncomplete && !selectedIsLegacy && !detailQuery.isError && (currentDraft || detailQuery.isLoading) && ( {!selectedSummaryIncomplete && !detailQuery.isError && (currentDraft || detailQuery.isLoading) && (
<> <>
{detailQuery.isLoading && !currentDraft ? <p className="text-sm text-muted-foreground">Loading workspace definition…</p> : currentDraft && <> {detailQuery.isLoading && !currentDraft ? <p className="text-sm text-muted-foreground">Loading workspace definition…</p> : currentDraft && <>
<div className="mb-5 flex flex-wrap items-start justify-between gap-3 border-b border-border/70 pb-4"> <div className="mb-5 flex flex-wrap items-start justify-between gap-3 border-b border-border/70 pb-4">
@@ -347,7 +341,7 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
<section className="mt-6 border-t border-border/70 pt-5" aria-label="Git status and history"> <section className="mt-6 border-t border-border/70 pt-5" aria-label="Git status and history">
<h3 className="font-heading text-sm font-semibold">Git status &amp; history</h3> <h3 className="font-heading text-sm font-semibold">Git status &amp; history</h3>
<p className="mt-2 text-sm text-muted-foreground">{status?.degraded ? "Using the last valid local snapshot." : "Registry checkout is current."}</p> <p className="mt-2 text-sm text-muted-foreground">{status?.degraded ? "Using the last valid local snapshot." : "Registry checkout is current."}</p>
{record && <p className="mt-1 font-mono text-xs text-muted-foreground">Revision {record.revision.commit.slice(0, 12)} · {record.revision.state}</p>} {record && <p className="mt-1 font-mono text-xs text-muted-foreground">Revision {record.revision.commit.slice(0, 12)}</p>}
</section> </section>
</>} </>}
</> </>
@@ -52,7 +52,7 @@ test("validates a draft and requires a separate confirmation before publishing",
let publishCalls = 0; let publishCalls = 0;
server.use(http.post("/api/workspaces/publish", () => { server.use(http.post("/api/workspaces/publish", () => {
publishCalls += 1; publishCalls += 1;
return HttpResponse.json({ revision: { id: "psd-clinical", commit: "c".repeat(40), blob: "d".repeat(40), snapshotPath: "/safe", state: "operational" } }); return HttpResponse.json({ revision: { id: "psd-clinical", commit: "c".repeat(40), blob: "d".repeat(40), snapshotPath: "/safe" } });
})); }));
render(<WorkspacePublishDialog open request={request} onOpenChange={vi.fn()} onPublished={published} onResolved={vi.fn()} onPull={vi.fn()} onReload={vi.fn()} />); render(<WorkspacePublishDialog open request={request} onOpenChange={vi.fn()} onPublished={published} onResolved={vi.fn()} onPull={vi.fn()} onReload={vi.fn()} />);
+1 -3
View File
@@ -23,9 +23,7 @@ test("F1: create session -> widget via SSE -> respond -> POST /response", async
http.get("/api/settings", () => HttpResponse.json({ http.get("/api/settings", () => HttpResponse.json({
workspace: "default", provider: "zai", model: "glm-5.2", thinking: "low", workspace: "default", provider: "zai", model: "glm-5.2", thinking: "low",
})), })),
http.get("/api/workspaces", () => http.get("/api/workspaces", () => HttpResponse.json([])),
HttpResponse.json([{ name: "default", file: "default.db" }]),
),
http.get("/api/models", () => HttpResponse.json({ models: [] })), http.get("/api/models", () => HttpResponse.json({ models: [] })),
http.post("/api/sessions/s1/response", async ({ request }) => { http.post("/api/sessions/s1/response", async ({ request }) => {
responded = await request.json(); responded = await request.json();
+2 -5
View File
@@ -23,12 +23,9 @@ export function canonicalWorkspaceFixture(
}; };
} }
export function workspaceRevisionFixture( export function workspaceRevisionFixture(id: string): WorkspaceRevision {
id: string,
state: WorkspaceRevision["state"] = "operational",
): WorkspaceRevision {
return { return {
id, commit: "a".repeat(40), blob: "b".repeat(40), id, commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: `/snapshots/${"a".repeat(40)}/${id}.yaml`, state, snapshotPath: `/snapshots/${"a".repeat(40)}/${id}.yaml`,
}; };
} }