refactor: stop persisting workspace state in the browser

This commit is contained in:
2026-08-14 17:43:10 +02:00
parent 3978008aed
commit fd3b62ce6f
11 changed files with 101 additions and 310 deletions
+14 -125
View File
@@ -1,129 +1,18 @@
import { beforeEach, expect, test } from "vitest";
import type { CanonicalWorkspace } from "../api/workspaces";
import {
sanitizeCanonicalWorkspace,
workspaceBootstrapDrafts,
workspacePreferences,
type WorkspaceBootstrapDraft,
} from "./drafts";
import { expect, test } from "vitest";
import { canonicalWorkspaceFixture } from "../test/workspace-fixtures";
import * as workspaceModule from "./drafts";
const workspace: CanonicalWorkspace = {
workspace: { schema_version: 3, id: "psd-clinical", name: "PSD Clinical", language: "en" },
dwh: {
engine: "postgres", database: "clinical", schema: "datawarehouse",
supported_transports: ["postgres_direct"],
},
semantic_index: {
vector_store: {
engine: "qdrant", collection: "clinical", dimensions: 1024, distance: "cosine",
},
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
},
llm_policy: { allowed: ["zai/glm-5.2"] },
};
const bootstrapDraft: WorkspaceBootstrapDraft = {
workspaceId: "psd-clinical",
baseCommit: "a".repeat(40),
workspace,
updatedAt: "2026-08-04T10:00:00.000Z",
};
const policy = { max_chunk_chars: 8_000, retain_published_generations: 5 };
const evidenceWorkspace: CanonicalWorkspace = {
...workspace,
evidence: {
source: {
type: "filesystem",
uri: "psd-clinical/evidence",
patterns: ["documents/**/*.pdf", "notes/*.md"],
max_bytes: 12_000_000,
},
policy,
},
};
beforeEach(() => localStorage.clear());
test("keeps an anonymous user's model selection in browser storage", () => {
workspacePreferences.save({
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
});
expect(workspacePreferences.load()).toMatchObject({ model: "glm-5.2" });
test("workspace module exposes validation and in-memory coordination but no draft storage", () => {
expect(workspaceModule.sanitizeCanonicalWorkspace(canonicalWorkspaceFixture("psd-clinical")))
.toEqual(canonicalWorkspaceFixture("psd-clinical"));
expect(workspaceModule).not.toHaveProperty("workspaceBootstrapDrafts");
expect(workspaceModule).not.toHaveProperty("workspaceDrafts");
expect(workspaceModule).not.toHaveProperty("workspaceDeletionDrafts");
});
test("reloads a bootstrap draft from the v2 browser-storage key and discards it by workspace ID", () => {
workspaceBootstrapDrafts.save(bootstrapDraft);
expect(workspaceBootstrapDrafts.load("psd-clinical")).toEqual(bootstrapDraft);
expect(localStorage.getItem("thothii.workspace-registry.v2.bootstrap.psd-clinical")).toContain('"baseCommit"');
expect(localStorage.getItem("thothii.workspace-registry.v2.bootstrap.psd-clinical")).not.toContain("baseBlob");
workspaceBootstrapDrafts.discard("psd-clinical");
expect(workspaceBootstrapDrafts.load("psd-clinical")).toBeUndefined();
});
test("purges known v1 update and deletion keys without touching unrelated localStorage", () => {
localStorage.setItem("thothii.workspace-registry.v1.draft.psd-clinical", JSON.stringify({
workspaceId: "psd-clinical",
baseCommit: "a".repeat(40),
baseBlob: "b".repeat(40),
workspace,
updatedAt: "2026-08-04T10:00:00.000Z",
}));
localStorage.setItem("thothii.workspace-registry.v1.delete.psd-clinical", JSON.stringify({
id: "psd-clinical",
baseCommit: "a".repeat(40),
baseBlob: "b".repeat(40),
updatedAt: "2026-08-04T10:00:00.000Z",
}));
localStorage.setItem("unrelated", "keep-me");
expect(workspaceBootstrapDrafts.load("psd-clinical")).toBeUndefined();
expect(localStorage.getItem("thothii.workspace-registry.v1.draft.psd-clinical")).toBeNull();
expect(localStorage.getItem("thothii.workspace-registry.v1.delete.psd-clinical")).toBeNull();
expect(localStorage.getItem("unrelated")).toBe("keep-me");
});
test("rejects draft payloads that still carry baseBlob from the removed update flow", () => {
localStorage.setItem("thothii.workspace-registry.v2.bootstrap.psd-clinical", JSON.stringify({
...bootstrapDraft,
baseBlob: "b".repeat(40),
}));
expect(workspaceBootstrapDrafts.load("psd-clinical")).toBeUndefined();
});
test("accepts only the workspace directory evidence root for filesystem sources", () => {
const sanitized = sanitizeCanonicalWorkspace(evidenceWorkspace);
expect(sanitized).toEqual(evidenceWorkspace);
expect(sanitized).not.toBe(evidenceWorkspace);
expect(sanitized?.evidence).not.toBe(evidenceWorkspace.evidence);
});
test("rejects filesystem Evidence that still points at workspace-content", () => {
const invalid: CanonicalWorkspace = {
...evidenceWorkspace,
evidence: {
source: {
type: "filesystem",
uri: "workspace-content/psd-clinical/evidence",
patterns: ["documents/**/*.pdf", "notes/*.md"],
max_bytes: 12_000_000,
},
policy,
},
};
expect(sanitizeCanonicalWorkspace(invalid)).toBeUndefined();
workspaceBootstrapDrafts.save({ ...bootstrapDraft, workspace: invalid });
expect(workspaceBootstrapDrafts.load("psd-clinical")).toBeUndefined();
});
test("continues to sanitize workspaces without Evidence", () => {
expect(sanitizeCanonicalWorkspace(workspace)).toEqual(workspace);
expect(sanitizeCanonicalWorkspace(workspace)).not.toHaveProperty("evidence");
test("canonical sanitizer rejects unknown fields rather than persisting them", () => {
expect(workspaceModule.sanitizeCanonicalWorkspace({
...canonicalWorkspaceFixture("psd-clinical"),
localDraft: { secret: "must-not-survive" },
})).toBeUndefined();
});
-123
View File
@@ -4,16 +4,6 @@ import type {
} from "../api/workspaces";
export { workspacePreferences, type WorkspacePreference } from "./preferences";
export interface WorkspaceBootstrapDraft {
workspaceId: string;
baseCommit: string;
workspace: CanonicalWorkspace;
updatedAt: string;
}
/** @deprecated Use WorkspaceBootstrapDraft. */
export type WorkspaceDraft = WorkspaceBootstrapDraft;
export const WORKSPACE_SUMMARY_ERROR = "Could not load workspace registry. Please retry.";
export const WORKSPACE_POLICY_ERROR = "Could not load selected workspace policy. Please retry.";
@@ -117,14 +107,6 @@ export const workspacePolicyGate = {
},
};
const LEGACY_PREFIX = "thothii.workspace-registry.v1";
const PREFIX = "thothii.workspace-registry.v2";
const DRAFT_PREFIX = `${PREFIX}.bootstrap.`;
function storage(): Storage | undefined {
try { return window.localStorage; } catch { return undefined; }
}
function record(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
@@ -473,108 +455,3 @@ export function sanitizeCanonicalWorkspace(value: unknown): CanonicalWorkspace |
...(evidence ? { evidence } : {}),
};
}
function repairBootstrapWorkspace(value: unknown, id: string): CanonicalWorkspace | undefined {
const direct = sanitizeCanonicalWorkspace(value);
if (direct) return direct;
const source = exactRecord(value, [
"workspace", "dwh", "semantic_index", "llm_policy", "diagnostics", "evidence",
]);
const evidence = exactRecord(source?.evidence, ["source", "policy"]);
const evidenceSource = exactRecord(evidence?.source, ["type", "uri", "patterns", "max_bytes"]);
if (
source
&& evidence
&& evidenceSource?.type === "filesystem"
&& typeof evidenceSource.uri === "string"
&& /^[a-z][a-z0-9-]{2,62}\/evidence$/.test(evidenceSource.uri)
) {
return sanitizeCanonicalWorkspace({
...source,
evidence: {
...evidence,
source: {
...evidenceSource,
uri: `${id}/evidence`,
},
},
});
}
return undefined;
}
function normalize(value: unknown): WorkspaceBootstrapDraft | undefined {
const source = exactRecord(value, ["workspaceId", "baseCommit", "workspace", "updatedAt"]);
const id = workspaceId(source?.workspaceId);
const workspace = id ? repairBootstrapWorkspace(source?.workspace, id) : undefined;
const baseCommit = typeof source?.baseCommit === "string" && /^[0-9a-f]{40}$/.test(source.baseCommit) ? source.baseCommit : undefined;
const updatedAt = typeof source?.updatedAt === "string" && Number.isFinite(Date.parse(source.updatedAt)) ? source.updatedAt : undefined;
if (!source || !workspace || !id || id !== workspace.workspace.id || !baseCommit || !updatedAt) {
return undefined;
}
return {
workspaceId: id,
baseCommit,
workspace,
updatedAt,
};
}
function key(id: string): string {
return `${DRAFT_PREFIX}${encodeURIComponent(id)}`;
}
function purgeLegacyRegistryDrafts(): void {
const store = storage();
if (!store) return;
const keys = Array.from({ length: store.length }, (_, index) => store.key(index)).filter((value): value is string => value !== null);
for (const entry of keys) {
if (entry.startsWith(`${LEGACY_PREFIX}.draft.`) || entry.startsWith(`${LEGACY_PREFIX}.delete.`)) {
store.removeItem(entry);
}
}
}
/** Browser-only bootstrap drafts. Saving or editing one never calls the server. */
export const workspaceBootstrapDrafts = {
load(id: string): WorkspaceBootstrapDraft | undefined {
purgeLegacyRegistryDrafts();
try {
const raw = storage()?.getItem(key(id));
return raw ? normalize(JSON.parse(raw)) : undefined;
} catch {
return undefined;
}
},
save(draft: WorkspaceBootstrapDraft): void {
purgeLegacyRegistryDrafts();
const safe = normalize(draft);
if (!safe) return;
try { storage()?.setItem(key(safe.workspaceId), JSON.stringify(safe)); } catch { /* storage is optional */ }
},
discard(id: string): void {
purgeLegacyRegistryDrafts();
try { storage()?.removeItem(key(id)); } catch { /* storage is optional */ }
},
};
/** @deprecated Use workspaceBootstrapDrafts. */
export const workspaceDrafts = workspaceBootstrapDrafts;
/** @deprecated Removed in P1.1; existing workspaces are curator-owned and deletions are not drafted in-browser. */
export interface WorkspaceDeletionDraft {
id: string;
baseCommit: string;
baseBlob: string;
updatedAt: string;
}
/** @deprecated Removed in P1.1; kept temporarily so legacy imports compile during the UI transition. */
export const workspaceDeletionDrafts = {
load(_id: string): WorkspaceDeletionDraft | undefined { return undefined; },
save(_draft: WorkspaceDeletionDraft): void {},
discard(_id: string): void {},
};
+28 -4
View File
@@ -1,15 +1,39 @@
import { expect, test } from "vitest";
import { beforeEach, expect, test } from "vitest";
import { workspacePreferences } from "./preferences";
test("migrates legacy selections once and keeps later browser choices", () => {
beforeEach(() => {
localStorage.clear();
workspacePreferences.reset();
});
test("keeps selections only in current application memory", () => {
localStorage.setItem("unrelated", "keep-me");
workspacePreferences.save({
workspaceId: "research",
provider: "deepseek",
model: "deepseek-v4-pro",
thinking: "high",
});
expect(workspacePreferences.load()).toEqual({
workspaceId: "research",
provider: "deepseek",
model: "deepseek-v4-pro",
thinking: "high",
});
expect(localStorage.length).toBe(1);
expect(localStorage.getItem("unrelated")).toBe("keep-me");
expect(localStorage.getItem("thothii.workspace-registry.v1.preferences")).toBeNull();
});
test("seeds memory from backend settings once and returns defensive copies", () => {
expect(workspacePreferences.migrate({
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
})).toMatchObject({ workspaceId: "psd-clinical", model: "glm-5.2" });
workspacePreferences.save({ workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "high" });
const snapshot = workspacePreferences.load();
snapshot.workspaceId = "mutated-copy";
expect(workspacePreferences.migrate({ workspaceId: "legacy" })).toMatchObject({
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "high",
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
});
});
+16 -21
View File
@@ -5,36 +5,31 @@ export interface WorkspacePreference {
thinking?: string;
}
const STORAGE_KEY = "thothii.workspace-registry.v1.preferences";
let current: WorkspacePreference | undefined;
function storage(): Storage | undefined {
try { return window.localStorage; } catch { return undefined; }
function snapshot(value: WorkspacePreference | undefined): WorkspacePreference {
return value === undefined ? {} : { ...value };
}
function parse(raw: string | null): WorkspacePreference | undefined {
if (raw === null) return undefined;
try {
const value = JSON.parse(raw);
return value && typeof value === "object" && !Array.isArray(value) ? value as WorkspacePreference : undefined;
} catch {
return undefined;
}
}
/** Anonymous, non-secret browser choices for the workspace session composer. */
/** Ephemeral workspace choices for the current application process. */
export const workspacePreferences = {
load(): WorkspacePreference {
return parse(storage()?.getItem(STORAGE_KEY) ?? null) ?? {};
return snapshot(current);
},
save(value: WorkspacePreference): WorkspacePreference {
try { storage()?.setItem(STORAGE_KEY, JSON.stringify(value)); } catch { /* storage is optional */ }
return value;
current = { ...value };
return snapshot(current);
},
/** Copy the pre-local-storage server values only when this browser has no saved choice. */
migrate(legacy: WorkspacePreference): WorkspacePreference {
const current = storage()?.getItem(STORAGE_KEY);
return current === null || current === undefined ? this.save(legacy) : this.load();
/** Seed current memory from backend installation settings only before the first local choice. */
migrate(settings: WorkspacePreference): WorkspacePreference {
if (current === undefined) current = { ...settings };
return snapshot(current);
},
/** Clear ephemeral state on application/test lifecycle reset. */
reset(): void {
current = undefined;
},
};