refactor: remove workspace revision state

This commit is contained in:
2026-08-10 20:54:40 +02:00
parent 512b0261b1
commit f102629cee
8 changed files with 292 additions and 314 deletions
-8
View File
@@ -154,7 +154,6 @@ export function sessionRoutes(
throw registryError; throw registryError;
} }
for (const revision of revisions) { for (const revision of revisions) {
if (revision.state !== "operational") continue;
try { try {
const manifest = await runner.sessionShow(id, revision.snapshotPath); const manifest = await runner.sessionShow(id, revision.snapshotPath);
if (manifest) return { manifest, workspaceConfigPath: revision.snapshotPath }; if (manifest) return { manifest, workspaceConfigPath: revision.snapshotPath };
@@ -339,12 +338,6 @@ export function sessionRoutes(
if ("markPersisted" in resolved && "abort" in resolved) { if ("markPersisted" in resolved && "abort" in resolved) {
revisionLease = resolved as Awaited<ReturnType<WorkspaceRegistry["acquireSessionRevision"]>>; revisionLease = resolved as Awaited<ReturnType<WorkspaceRegistry["acquireSessionRevision"]>>;
} }
if (resolved.revision.state !== "operational") {
return reply.code(409).send({
error: WORKSPACE_REVISION_UNAVAILABLE_MESSAGE,
code: "workspace_revision_unavailable",
});
}
if (!d.workspaceRuntimeSupport(resolved.workspace)) { if (!d.workspaceRuntimeSupport(resolved.workspace)) {
return reply.code(409).send({ return reply.code(409).send({
error: "This workspace transport is not available to runtime sessions.", error: "This workspace transport is not available to runtime sessions.",
@@ -481,7 +474,6 @@ export function sessionRoutes(
const runner = runnerFor(scopedPrincipal); const runner = runnerFor(scopedPrincipal);
const revisions = await sessionRevisions(); const revisions = await sessionRevisions();
const lists = await Promise.all(revisions const lists = await Promise.all(revisions
.filter((revision) => revision.state === "operational")
.map((revision) => runner.sessionList(revision.snapshotPath) as Promise<SessionRow[]>)); .map((revision) => runner.sessionList(revision.snapshotPath) as Promise<SessionRow[]>));
const sessions = new Map<string, SessionRow>(); const sessions = new Map<string, SessionRow>();
for (const row of lists.flat()) { for (const row of lists.flat()) {
-1
View File
@@ -24,7 +24,6 @@ export function sqlRoutes(app: FastifyInstance, deps: {
? await registry.listRetainedSnapshots.call(deps.workspaceRegistry) ? await registry.listRetainedSnapshots.call(deps.workspaceRegistry)
: await deps.workspaceRegistry.list(); : await deps.workspaceRegistry.list();
for (const revision of revisions) { for (const revision of revisions) {
if (revision.state !== "operational") continue;
try { try {
const manifest = await runner.sessionShow(id, revision.snapshotPath); const manifest = await runner.sessionShow(id, revision.snapshotPath);
if (!manifest) continue; if (!manifest) continue;
+1 -6
View File
@@ -339,12 +339,7 @@ export function workspaceRoutes(app: FastifyInstance, deps: WorkspaceRoutesDeps)
app.post("/workspaces/:id/test", async (request, reply) => { app.post("/workspaces/:id/test", async (request, reply) => {
try { try {
const { id } = z.object({ id: workspaceId }).parse(request.params); const { id } = z.object({ id: workspaceId }).parse(request.params);
const { workspace, revision } = await deps.registry.read(id); const { workspace } = await deps.registry.read(id);
if (revision.state !== "operational") {
throw new WorkspaceRegistryError(
"workspace_not_activatable", "Workspace requires explicit migration",
);
}
let operational: CanonicalWorkspace; let operational: CanonicalWorkspace;
try { try {
operational = validateOperationalWorkspace(workspace); operational = validateOperationalWorkspace(workspace);
+94 -187
View File
@@ -10,7 +10,6 @@ import {
type GitStatus, type GitStatus,
} from "./git-repository.js"; } from "./git-repository.js";
import { import {
isCanonicalWorkspace,
parseWorkspaceYaml, parseWorkspaceYaml,
serializeWorkspaceYaml, serializeWorkspaceYaml,
validateOperationalWorkspace, validateOperationalWorkspace,
@@ -26,7 +25,6 @@ export interface WorkspaceRevision {
commit: string; commit: string;
blob: string; blob: string;
snapshotPath: string; snapshotPath: string;
state: "operational" | "migration_required";
} }
export interface SessionRevisionLease { export interface SessionRevisionLease {
@@ -74,17 +72,6 @@ interface RevisionLeaseRecord {
state: "creating" | "persisted"; state: "creating" | "persisted";
} }
type LegacyWorkspaceRevision = Omit<WorkspaceRevision, "state">;
interface LegacyActiveState {
head: string;
revisions: LegacyWorkspaceRevision[];
}
interface LegacySnapshotManifest extends LegacyActiveState {
files: Record<string, string>;
}
function workspacePath(id: string): string { function workspacePath(id: string): string {
if (!/^[a-z][a-z0-9-]{2,62}$/.test(id)) { if (!/^[a-z][a-z0-9-]{2,62}$/.test(id)) {
throw new WorkspaceRegistryError("workspace_invalid", "Workspace ID is invalid"); throw new WorkspaceRegistryError("workspace_invalid", "Workspace ID is invalid");
@@ -181,7 +168,7 @@ export class WorkspaceRegistry {
if (!entry.isDirectory() || entry.isSymbolicLink() || !/^[0-9a-f]{40}$/.test(entry.name)) continue; if (!entry.isDirectory() || entry.isSymbolicLink() || !/^[0-9a-f]{40}$/.test(entry.name)) continue;
if (entry.name === active.head) continue; if (entry.name === active.head) continue;
const state = await this.snapshotState(entry.name); const state = await this.snapshotState(entry.name);
revisions.push(...state.revisions.filter((revision) => revision.state === "operational")); revisions.push(...state.revisions);
} }
return revisions; return revisions;
} catch (error) { } catch (error) {
@@ -212,9 +199,6 @@ export class WorkspaceRegistry {
const state = await this.activeState(); const state = await this.activeState();
const revision = state.revisions.find((candidate) => candidate.id === id); const revision = state.revisions.find((candidate) => candidate.id === id);
if (!revision) throw new WorkspaceRegistryError("workspace_invalid", "Workspace is unavailable"); if (!revision) throw new WorkspaceRegistryError("workspace_invalid", "Workspace is unavailable");
if (revision.state !== "operational") {
throw new WorkspaceRegistryError("workspace_invalid", "Workspace is unavailable");
}
let workspace: WorkspaceDescriptor; let workspace: WorkspaceDescriptor;
try { try {
workspace = validateOperationalWorkspace( workspace = validateOperationalWorkspace(
@@ -437,8 +421,7 @@ export class WorkspaceRegistry {
const base = await this.readSnapshotCanonical(request.baseCommit, id); const base = await this.readSnapshotCanonical(request.baseCommit, id);
let remote: CanonicalWorkspace | undefined; let remote: CanonicalWorkspace | undefined;
if (existing) { if (existing) {
const read = await this.read(id); remote = (await this.read(id)).workspace;
remote = isCanonicalWorkspace(read.workspace) ? read.workspace : undefined;
} }
return new WorkspaceConflictError( return new WorkspaceConflictError(
this.changedFields(base, remote), this.changedFields(base, remote),
@@ -453,8 +436,7 @@ export class WorkspaceRegistry {
private async readSnapshotCanonical(commit: string, id: string): Promise<CanonicalWorkspace | undefined> { private async readSnapshotCanonical(commit: string, id: string): Promise<CanonicalWorkspace | undefined> {
try { try {
const source = await readFile(this.snapshotPath(commit, id), "utf8"); const source = await readFile(this.snapshotPath(commit, id), "utf8");
const workspace = parseWorkspaceYaml(source); return parseWorkspaceYaml(source);
return isCanonicalWorkspace(workspace) ? workspace : undefined;
} catch { } catch {
return undefined; return undefined;
} }
@@ -482,7 +464,6 @@ export class WorkspaceRegistry {
} }
private async assertEvidenceContext(workspace: WorkspaceDescriptor, revision: string): Promise<void> { private async assertEvidenceContext(workspace: WorkspaceDescriptor, revision: string): Promise<void> {
if (!isCanonicalWorkspace(workspace)) return;
if (workspace.evidence?.source.type !== "filesystem") return; if (workspace.evidence?.source.type !== "filesystem") return;
// P6 owns recursive containment. Here we deliberately validate only the declared root object. // P6 owns recursive containment. Here we deliberately validate only the declared root object.
await this.repository.assertTreeAtRevision(revision, workspace.evidence.source.uri); await this.repository.assertTreeAtRevision(revision, workspace.evidence.source.uri);
@@ -504,7 +485,6 @@ export class WorkspaceRegistry {
source: string; source: string;
workspace: WorkspaceDescriptor; workspace: WorkspaceDescriptor;
blob: string; blob: string;
state: WorkspaceRevision["state"];
}> = []; }> = [];
const collectionOwners = new Map<string, string>(); const collectionOwners = new Map<string, string>();
try { try {
@@ -516,27 +496,19 @@ export class WorkspaceRegistry {
throw new WorkspaceRegistryError("workspace_invalid", "Workspace ID does not match its repository path"); throw new WorkspaceRegistryError("workspace_invalid", "Workspace ID does not match its repository path");
} }
await this.assertEvidenceContext(workspace, safeHead); await this.assertEvidenceContext(workspace, safeHead);
let snapshotSource = source; const collection = workspace.semantic_index.vector_store.collection;
const state: WorkspaceRevision["state"] = isCanonicalWorkspace(workspace) const owner = collectionOwners.get(collection);
? "operational" if (owner !== undefined) {
: "migration_required"; throw new Error(`duplicate qdrant collection ownership: ${collection} (${owner}, ${id})`);
if (isCanonicalWorkspace(workspace)) {
const collection = workspace.semantic_index.vector_store.collection;
const owner = collectionOwners.get(collection);
if (owner !== undefined) {
throw new Error(`duplicate qdrant collection ownership: ${collection} (${owner}, ${id})`);
}
collectionOwners.set(collection, id);
buildInstallationContract(workspace);
renderWorkspaceDocs(workspace);
snapshotSource = serializeWorkspaceYaml(workspace);
} }
collectionOwners.set(collection, id);
buildInstallationContract(workspace);
renderWorkspaceDocs(workspace);
snapshots.push({ snapshots.push({
id, id,
source: snapshotSource, source: serializeWorkspaceYaml(workspace),
workspace, workspace,
blob: await this.repository.blob(path), blob: await this.repository.blob(path),
state,
}); });
} }
} catch (error) { } catch (error) {
@@ -549,10 +521,9 @@ export class WorkspaceRegistry {
commit: safeHead, commit: safeHead,
blob: snapshot.blob, blob: snapshot.blob,
snapshotPath: this.snapshotPath(safeHead, snapshot.id), snapshotPath: this.snapshotPath(safeHead, snapshot.id),
state: snapshot.state,
})); }));
if (this.pathExists(snapshotDirectory)) { if (this.pathExists(snapshotDirectory)) {
await this.assertOrMigrateSnapshotIntegrity({ head: safeHead, revisions }); await this.assertSnapshotIntegrity({ head: safeHead, revisions });
} else { } else {
const staging = join(this.repository.snapshotsPath, `.staging-${randomUUID()}`); const staging = join(this.repository.snapshotsPath, `.staging-${randomUUID()}`);
await mkdir(staging, { mode: 0o700 }); await mkdir(staging, { mode: 0o700 });
@@ -564,13 +535,11 @@ export class WorkspaceRegistry {
const docsName = `${snapshot.id}.md`; const docsName = `${snapshot.id}.md`;
await writeFile(join(staging, yamlName), snapshot.source, { encoding: "utf8", mode: 0o400 }); await writeFile(join(staging, yamlName), snapshot.source, { encoding: "utf8", mode: 0o400 });
files[yamlName] = digest(snapshot.source); files[yamlName] = digest(snapshot.source);
if (snapshot.state === "operational") { const docs = renderWorkspaceDocs(snapshot.workspace);
const docs = renderWorkspaceDocs(snapshot.workspace); await writeFile(join(staging, envName), docs.envExample, { encoding: "utf8", mode: 0o400 });
await writeFile(join(staging, envName), docs.envExample, { encoding: "utf8", mode: 0o400 }); await writeFile(join(staging, docsName), docs.markdown, { encoding: "utf8", mode: 0o400 });
await writeFile(join(staging, docsName), docs.markdown, { encoding: "utf8", mode: 0o400 }); files[envName] = digest(docs.envExample);
files[envName] = digest(docs.envExample); files[docsName] = digest(docs.markdown);
files[docsName] = digest(docs.markdown);
}
} }
await writeFile(join(staging, "snapshot.json"), JSON.stringify({ head: safeHead, revisions, files }), { await writeFile(join(staging, "snapshot.json"), JSON.stringify({ head: safeHead, revisions, files }), {
encoding: "utf8", mode: 0o400, encoding: "utf8", mode: 0o400,
@@ -609,12 +578,7 @@ export class WorkspaceRegistry {
private async tryActiveState(): Promise<ActiveState | undefined> { private async tryActiveState(): Promise<ActiveState | undefined> {
const file = join(this.repository.statePath, "active.json"); const file = join(this.repository.statePath, "active.json");
try { try {
const parsed: unknown = JSON.parse(await readFile(file, "utf8")); const state = this.decodeActiveState(JSON.parse(await readFile(file, "utf8")));
if (this.isLegacyActiveState(parsed)) {
return await this.migrateLegacyActiveState(parsed);
}
const state = parsed as ActiveState;
this.assertActiveState(state);
await this.assertSnapshotIntegrity(state); await this.assertSnapshotIntegrity(state);
return state; return state;
} catch (error) { } catch (error) {
@@ -631,125 +595,86 @@ export class WorkspaceRegistry {
await rename(staging, target); await rename(staging, target);
} }
private async writeSnapshotManifest(directory: string, manifest: SnapshotManifest): Promise<void> { private decodeActiveState(value: unknown): ActiveState {
const target = join(directory, "snapshot.json"); const state = this.strictObject(value, ["head", "revisions"]);
const staging = join(directory, `.snapshot-${randomUUID()}.json`); return this.decodeStateRevisions(state.head, state.revisions);
await writeFile(staging, JSON.stringify(manifest), { encoding: "utf8", mode: 0o400 });
await rename(staging, target);
} }
private assertActiveState(state: ActiveState): void { private decodeSnapshotManifest(value: unknown): SnapshotManifest {
safeCommit(state.head); const manifest = this.strictObject(value, ["head", "revisions", "files"]);
if (!Array.isArray(state.revisions)) throw new Error("bad state"); const state = this.decodeStateRevisions(manifest.head, manifest.revisions);
if (!manifest.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) {
throw new Error("bad manifest files");
}
const entries = Object.entries(manifest.files as Record<string, unknown>);
if (entries.some(([, contentsDigest]) => typeof contentsDigest !== "string")) {
throw new Error("bad manifest files");
}
return { ...state, files: Object.fromEntries(entries) as Record<string, string> };
}
private decodeStateRevisions(headValue: unknown, revisionsValue: unknown): ActiveState {
if (typeof headValue !== "string" || !Array.isArray(revisionsValue)) throw new Error("bad state");
const head = safeCommit(headValue);
const ids = new Set<string>(); const ids = new Set<string>();
for (const revision of state.revisions) { const revisions = revisionsValue.map((value) => {
safeCommit(revision.commit); const revision = this.decodeRevision(value, head);
safeBlob(revision.blob); if (ids.has(revision.id)) throw new Error("duplicate revision");
if (revision.commit !== state.head || ids.has(revision.id)) throw new Error("bad revision");
if (revision.state !== "operational" && revision.state !== "migration_required") throw new Error("bad revision");
ids.add(revision.id); ids.add(revision.id);
workspacePath(revision.id); return revision;
if (!isAbsolute(revision.snapshotPath) || revision.snapshotPath !== this.snapshotPath(revision.commit, revision.id)) { });
throw new Error("bad snapshot path"); return { head, revisions };
} }
private decodeRevision(value: unknown, head: string): WorkspaceRevision {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("bad revision");
const revision = value as Record<string, unknown>;
const keys = Object.keys(revision);
const required = ["id", "commit", "blob", "snapshotPath"];
const hasHistoricalState = Object.prototype.hasOwnProperty.call(revision, "state");
if (
keys.length !== required.length + (hasHistoricalState ? 1 : 0)
|| !required.every((key) => Object.prototype.hasOwnProperty.call(revision, key))
|| (hasHistoricalState && revision.state !== "operational")
) {
throw new Error("bad revision");
} }
} if (
typeof revision.id !== "string"
private isLegacyActiveState(value: unknown): value is LegacyActiveState { || typeof revision.commit !== "string"
if (!value || typeof value !== "object") return false; || typeof revision.blob !== "string"
const revisions = (value as { revisions?: unknown }).revisions; || typeof revision.snapshotPath !== "string"
return Array.isArray(revisions) && revisions.length > 0 && revisions.every((revision) => ( ) {
revision && typeof revision === "object" && !("state" in revision) throw new Error("bad revision");
));
}
private isLegacySnapshotManifest(value: unknown): value is LegacySnapshotManifest {
return this.isLegacyActiveState(value)
&& !!(value as { files?: unknown }).files
&& typeof (value as { files?: unknown }).files === "object"
&& !Array.isArray((value as { files?: unknown }).files);
}
private assertLegacyActiveState(state: LegacyActiveState): void {
safeCommit(state.head);
if (!Array.isArray(state.revisions) || state.revisions.length === 0) throw new Error("bad legacy state");
const ids = new Set<string>();
for (const revision of state.revisions) {
safeCommit(revision.commit);
safeBlob(revision.blob);
if (revision.commit !== state.head || ids.has(revision.id)) throw new Error("bad legacy revision");
ids.add(revision.id);
workspacePath(revision.id);
if (!isAbsolute(revision.snapshotPath) || revision.snapshotPath !== this.snapshotPath(revision.commit, revision.id)) {
throw new Error("bad legacy snapshot path");
}
} }
const id = revision.id;
const commit = safeCommit(revision.commit);
const blob = safeBlob(revision.blob);
const snapshotPath = revision.snapshotPath;
workspacePath(id);
if (
commit !== head
|| !isAbsolute(snapshotPath)
|| snapshotPath !== this.snapshotPath(commit, id)
) {
throw new Error("bad revision");
}
// Always reconstruct a fresh public revision. The sole accepted historical state field is
// compatibility input and must never cross the registry boundary.
return { id, commit, blob, snapshotPath };
} }
private async migrateLegacyActiveState(legacy: LegacyActiveState): Promise<ActiveState> { private strictObject(value: unknown, expectedKeys: readonly string[]): Record<string, unknown> {
this.assertLegacyActiveState(legacy); if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("bad state");
const state = await this.deriveStateFromLegacyRevisions(legacy); const record = value as Record<string, unknown>;
const manifest = await this.readSnapshotManifest(state.head); const keys = Object.keys(record);
if (this.isLegacySnapshotManifest(manifest)) { if (
await this.migrateLegacySnapshotManifest(state, manifest); keys.length !== expectedKeys.length
} else { || !expectedKeys.every((key) => Object.prototype.hasOwnProperty.call(record, key))
await this.assertSnapshotIntegrity(state); ) {
} throw new Error("bad state");
await this.writeActiveState(state);
return state;
}
private async deriveStateFromLegacyRevisions(legacy: LegacyActiveState): Promise<ActiveState> {
const revisions: WorkspaceRevision[] = [];
for (const revision of legacy.revisions) {
const source = await readFile(revision.snapshotPath, "utf8");
const workspace = parseWorkspaceYaml(source);
if (workspace.workspace.id !== revision.id) throw new Error("legacy snapshot workspace is invalid");
revisions.push({
...revision,
state: isCanonicalWorkspace(workspace) ? "operational" : "migration_required",
});
}
return { head: legacy.head, revisions };
}
private async assertOrMigrateSnapshotIntegrity(state: ActiveState): Promise<void> {
const manifest = await this.readSnapshotManifest(state.head);
if (this.isLegacySnapshotManifest(manifest)) {
await this.migrateLegacySnapshotManifest(state, manifest);
return;
}
await this.assertSnapshotIntegrity(state);
}
private async migrateLegacySnapshotManifest(
state: ActiveState,
suppliedManifest?: LegacySnapshotManifest,
): Promise<void> {
const manifest = suppliedManifest ?? await this.readSnapshotManifest(state.head);
try {
if (!this.isLegacySnapshotManifest(manifest)) throw new Error("snapshot is not pre-state");
this.assertLegacyActiveState(manifest);
if (manifest.head !== state.head || !this.sameLegacyRevisions(manifest.revisions, state.revisions)) {
throw new Error("legacy manifest revisions do not match active state");
}
const derived = await this.deriveStateFromLegacyRevisions(manifest);
if (!this.sameRevisions(derived.revisions, state.revisions)) {
throw new Error("legacy manifest state does not match workspace snapshots");
}
const directory = join(this.repository.snapshotsPath, state.head);
const legacyExpected = state.revisions.flatMap((revision) => [
`${revision.id}.yaml`, `${revision.id}.env.example`, `${revision.id}.md`,
]);
await this.assertManifestFiles(directory, manifest.files, legacyExpected);
await this.assertSnapshotEvidenceContexts(state);
const expected = this.expectedSnapshotFiles(state);
const files = Object.fromEntries(expected.map((name) => [name, manifest.files[name]]));
await this.writeSnapshotManifest(directory, { ...state, files });
} catch (error) {
if (error instanceof WorkspaceRegistryError) throw error;
throw new WorkspaceRegistryError("workspace_invalid", "Workspace snapshot integrity check failed");
} }
return record;
} }
private async readSnapshotManifest(head: string): Promise<unknown> { private async readSnapshotManifest(head: string): Promise<unknown> {
@@ -758,14 +683,7 @@ export class WorkspaceRegistry {
} }
private async snapshotState(head: string): Promise<ActiveState> { private async snapshotState(head: string): Promise<ActiveState> {
const manifest = await this.readSnapshotManifest(safeCommit(head)); const state = this.decodeSnapshotManifest(await this.readSnapshotManifest(safeCommit(head)));
if (this.isLegacySnapshotManifest(manifest)) {
const state = await this.deriveStateFromLegacyRevisions(manifest);
await this.migrateLegacySnapshotManifest(state, manifest);
return state;
}
const state = manifest as ActiveState;
this.assertActiveState(state);
await this.assertSnapshotIntegrity(state); await this.assertSnapshotIntegrity(state);
return state; return state;
} }
@@ -773,8 +691,7 @@ export class WorkspaceRegistry {
private async assertSnapshotIntegrity(state: ActiveState): Promise<void> { private async assertSnapshotIntegrity(state: ActiveState): Promise<void> {
const directory = join(this.repository.snapshotsPath, state.head); const directory = join(this.repository.snapshotsPath, state.head);
try { try {
const manifest = await this.readSnapshotManifest(state.head) as SnapshotManifest; const manifest = this.decodeSnapshotManifest(await this.readSnapshotManifest(state.head));
this.assertActiveState(manifest);
if (manifest.head !== state.head || !this.sameRevisions(manifest.revisions, state.revisions)) { if (manifest.head !== state.head || !this.sameRevisions(manifest.revisions, state.revisions)) {
throw new Error("manifest revisions do not match active state"); throw new Error("manifest revisions do not match active state");
} }
@@ -789,9 +706,9 @@ export class WorkspaceRegistry {
private expectedSnapshotFiles(state: ActiveState): string[] { private expectedSnapshotFiles(state: ActiveState): string[] {
// P1 snapshots only descriptors and derived public docs. P6 owns revision-pinned // P1 snapshots only descriptors and derived public docs. P6 owns revision-pinned
// workspace-content materialization and its recursive containment checks. // workspace-content materialization and its recursive containment checks.
return state.revisions.flatMap((revision) => revision.state === "operational" return state.revisions.flatMap((revision) => [
? [`${revision.id}.yaml`, `${revision.id}.env.example`, `${revision.id}.md`] `${revision.id}.yaml`, `${revision.id}.env.example`, `${revision.id}.md`,
: [`${revision.id}.yaml`]); ]);
} }
private async assertManifestFiles( private async assertManifestFiles(
@@ -818,16 +735,6 @@ export class WorkspaceRegistry {
} }
private sameRevisions(left: WorkspaceRevision[], right: WorkspaceRevision[]): boolean { private sameRevisions(left: WorkspaceRevision[], right: WorkspaceRevision[]): boolean {
return left.length === right.length && left.every((revision, index) => {
const candidate = right[index];
return candidate !== undefined
&& candidate.id === revision.id && candidate.commit === revision.commit
&& candidate.blob === revision.blob && candidate.snapshotPath === revision.snapshotPath
&& candidate.state === revision.state;
});
}
private sameLegacyRevisions(left: LegacyWorkspaceRevision[], right: WorkspaceRevision[]): boolean {
return left.length === right.length && left.every((revision, index) => { return left.length === right.length && left.every((revision, index) => {
const candidate = right[index]; const candidate = right[index];
return candidate !== undefined return candidate !== undefined
+13 -16
View File
@@ -38,13 +38,13 @@ function operationalWorkspace(id = "default") {
const defaultWorkspaceRegistry = { const defaultWorkspaceRegistry = {
list: vi.fn(async () => [{ list: vi.fn(async () => [{
id: "default", commit: "e".repeat(40), blob: "f".repeat(40), id: "default", commit: "e".repeat(40), blob: "f".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"e".repeat(40)}/default.yaml`, state: "operational", snapshotPath: `/data/workspace-registry/snapshots/${"e".repeat(40)}/default.yaml`,
}]), }]),
read: vi.fn(async (id: string) => ({ read: vi.fn(async (id: string) => ({
workspace: operationalWorkspace(id), workspace: operationalWorkspace(id),
revision: { revision: {
id, commit: "e".repeat(40), blob: "f".repeat(40), id, commit: "e".repeat(40), blob: "f".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"e".repeat(40)}/${id}.yaml`, state: "operational", snapshotPath: `/data/workspace-registry/snapshots/${"e".repeat(40)}/${id}.yaml`,
}, },
})), })),
}; };
@@ -306,8 +306,8 @@ test("retention scans a removed workspace's retained snapshot", async () => {
const activeSnapshot = "/registry/snapshots/a/other.yaml"; const activeSnapshot = "/registry/snapshots/a/other.yaml";
const removedSnapshot = "/registry/snapshots/e/removed.yaml"; const removedSnapshot = "/registry/snapshots/e/removed.yaml";
const listRetainedSnapshots = vi.fn(async () => [ const listRetainedSnapshots = vi.fn(async () => [
{ id: "other", commit: "a".repeat(40), state: "operational", snapshotPath: activeSnapshot }, { id: "other", commit: "a".repeat(40), snapshotPath: activeSnapshot },
{ id: "removed", commit: removedRevision, state: "operational", snapshotPath: removedSnapshot }, { id: "removed", commit: removedRevision, snapshotPath: removedSnapshot },
]); ]);
const app = buildApp(loadConfig({ AUTH_MODE: "upstream", THT_HARNESS_DIR: "../harness" }), { const app = buildApp(loadConfig({ AUTH_MODE: "upstream", THT_HARNESS_DIR: "../harness" }), {
thtRunner: { thtRunner: {
@@ -318,7 +318,7 @@ test("retention scans a removed workspace's retained snapshot", async () => {
}), }),
} as any, } as any,
workspaceRegistry: { workspaceRegistry: {
list: async () => [{ id: "other", commit: "a".repeat(40), state: "operational", snapshotPath: activeSnapshot }], list: async () => [{ id: "other", commit: "a".repeat(40), snapshotPath: activeSnapshot }],
listRetainedSnapshots, listRetainedSnapshots,
reconcileSnapshotRetention: retained, reconcileSnapshotRetention: retained,
} as any, } as any,
@@ -472,7 +472,7 @@ test("creates a session from the active immutable workspace revision", async ()
}, },
revision: { revision: {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: "/data/workspace-registry/snapshots/abc/psd-clinical.yaml", state: "operational", snapshotPath: "/data/workspace-registry/snapshots/abc/psd-clinical.yaml",
}, },
})), })),
} as any, } as any,
@@ -524,7 +524,6 @@ test("rejects an SSH-only workspace before persisting or starting a session", as
revision: { revision: {
id: "ssh-workspace", commit: "a".repeat(40), blob: "b".repeat(40), id: "ssh-workspace", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/ssh-workspace.yaml`, snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/ssh-workspace.yaml`,
state: "operational",
}, },
abort, abort,
markPersisted, markPersisted,
@@ -552,7 +551,6 @@ test("hands a revision lease to retention only after the session manifest is dur
revision: { revision: {
id: "leased", commit: "a".repeat(40), blob: "b".repeat(40), id: "leased", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/leased.yaml`, snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/leased.yaml`,
state: "operational",
}, },
markPersisted, markPersisted,
abort, abort,
@@ -590,7 +588,7 @@ test("creates a session from the configured default workspace revision when work
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } }, workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
revision: { revision: {
id, commit: "c".repeat(40), blob: "d".repeat(40), id, commit: "c".repeat(40), blob: "d".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"c".repeat(40)}/${id}.yaml`, state: "operational", snapshotPath: `/data/workspace-registry/snapshots/${"c".repeat(40)}/${id}.yaml`,
}, },
})), })),
}; };
@@ -676,11 +674,11 @@ test("session lifecycle locates a B session when installation default is A", asy
workspaceRegistry: { workspaceRegistry: {
read: async (id: string) => ({ read: async (id: string) => ({
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } }, workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
revision: { id, commit: "b".repeat(40), blob: "d".repeat(40), snapshotPath: bPath, state: "operational" }, revision: { id, commit: "b".repeat(40), blob: "d".repeat(40), snapshotPath: bPath },
}), }),
list: async () => [ list: async () => [
{ id: "a-workspace", commit: "a".repeat(40), blob: "a".repeat(40), snapshotPath: aPath, state: "operational" }, { id: "a-workspace", commit: "a".repeat(40), blob: "a".repeat(40), snapshotPath: aPath },
{ id: "b-workspace", commit: "b".repeat(40), blob: "b".repeat(40), snapshotPath: bPath, state: "operational" }, { id: "b-workspace", commit: "b".repeat(40), blob: "b".repeat(40), snapshotPath: bPath },
], ],
readPinned: vi.fn(async (id: string, revision: string) => { readPinned: vi.fn(async (id: string, revision: string) => {
expect([id, revision]).toEqual(["b-workspace", "c".repeat(40)]); expect([id, revision]).toEqual(["b-workspace", "c".repeat(40)]);
@@ -956,7 +954,7 @@ test("POST /sessions/:id/resume uses the manifest's retained workspace revision"
workspace: operationalWorkspace("psd-clinical"), workspace: operationalWorkspace("psd-clinical"),
revision: { revision: {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: "/data/workspace-registry/snapshots/aaaaaaaa/psd-clinical.yaml", state: "operational", snapshotPath: "/data/workspace-registry/snapshots/aaaaaaaa/psd-clinical.yaml",
}, },
})), })),
} as any, } as any,
@@ -1072,7 +1070,7 @@ test("a pruned pin blocks Resume but not active or mutation lifecycle routes", a
} as any, } as any,
workspaceRegistry: { workspaceRegistry: {
list: async () => [{ list: async () => [{
id: "b-workspace", commit: "a".repeat(40), blob: "a".repeat(40), snapshotPath: activePath, state: "operational", id: "b-workspace", commit: "a".repeat(40), blob: "a".repeat(40), snapshotPath: activePath,
}], }],
readPinned, readPinned,
} as any, } as any,
@@ -2299,7 +2297,7 @@ test("rename authorizes and mutates through the same registry snapshot", async (
} as any, } as any,
workspaceRegistry: { workspaceRegistry: {
list: async () => [{ list: async () => [{
id: "tenant-a", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: tenantPath, state: "operational", id: "tenant-a", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: tenantPath,
}], }],
} as any, } as any,
}); });
@@ -2861,7 +2859,6 @@ test.each([
const revision = { const revision = {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/psd-clinical.yaml`, snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/psd-clinical.yaml`,
state: "operational" as const,
}; };
try { try {
+1 -1
View File
@@ -108,7 +108,7 @@ test("registry-backed SQL preview resolves and uses the session's pinned runtime
workspaceRegistry: { workspaceRegistry: {
list: async () => [{ list: async () => [{
id: "psd-clinical", commit: "a".repeat(40), blob: "c".repeat(40), id: "psd-clinical", commit: "a".repeat(40), blob: "c".repeat(40),
snapshotPath: activePath, state: "operational", snapshotPath: activePath,
}], }],
readPinned: async () => ({ workspace: {}, workspaceConfigPath: pinnedPath }), readPinned: async () => ({ workspace: {}, workspaceConfigPath: pinnedPath }),
} as any, } as any,
+2 -31
View File
@@ -114,7 +114,6 @@ const revision: WorkspaceRevision = {
commit: "a".repeat(40), commit: "a".repeat(40),
blob: "b".repeat(40), blob: "b".repeat(40),
snapshotPath: "/registry/snapshots/psd-clinical.yaml", snapshotPath: "/registry/snapshots/psd-clinical.yaml",
state: "operational",
}; };
type RegistryFake = Pick<WorkspaceRegistry, "bootstrap" | "pull" | "list" | "read" | "publish">; type RegistryFake = Pick<WorkspaceRegistry, "bootstrap" | "pull" | "list" | "read" | "publish">;
@@ -237,6 +236,8 @@ test("lists compatible workspace summaries and reads a validated workspace", asy
})]); })]);
expect(detail.statusCode).toBe(200); expect(detail.statusCode).toBe(200);
expect(detail.json()).toMatchObject({ workspace, revision }); expect(detail.json()).toMatchObject({ workspace, revision });
expect(list.json()[0].revision).not.toHaveProperty("state");
expect(detail.json().revision).not.toHaveProperty("state");
}); });
test("validates a canonical workspace and runs the injected installation diagnostic", async () => { test("validates a canonical workspace and runs the injected installation diagnostic", async () => {
@@ -278,36 +279,6 @@ test.each([
expect(registry.publish).not.toHaveBeenCalled(); expect(registry.publish).not.toHaveBeenCalled();
}); });
test("rejects a migration-required v2 workspace before resolving semantic diagnostics", async () => {
const diagnose = vi.fn(async () => ({
activatable: false,
diagnostics: [{ level: "error" as const, code: "binding_missing" as const, field: "THT_WS_PSD_CLINICAL_VECTOR_BASE_URL", message: "Installation binding is missing or invalid." }],
}));
const registry = registryFake({
read: vi.fn(async () => ({ workspace: workspaceV2, revision: { ...revision, state: "migration_required" as const } })),
});
const app = appFor(registry, diagnose);
const originalEnv = { ...process.env };
process.env.THT_WS_PSD_CLINICAL_DWH_TRANSPORT = "rest_api";
process.env.THT_WS_PSD_CLINICAL_DWH_BASE_URL = "https://dwh.example.test";
process.env.THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT = "rest_api";
process.env.THT_WS_PSD_CLINICAL_VECTOR_BASE_URL = "https://vector.example.test";
process.env.THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL = "https://embedding.example.test";
try {
const testResult = await app.inject({ method: "POST", url: "/workspaces/psd-clinical/test", payload: {} });
expect(testResult.statusCode).toBe(400);
expect(testResult.json()).toEqual({
code: "workspace_not_activatable",
message: "Workspace cannot be activated on this installation.",
});
expect(diagnose).not.toHaveBeenCalled();
} finally {
process.env = originalEnv;
}
});
test("runs diagnostics for a schema v3 workspace without external semantic bindings", async () => { test("runs diagnostics for a schema v3 workspace without external semantic bindings", async () => {
const diagnose = vi.fn(async () => ({ activatable: true, diagnostics: [] })); const diagnose = vi.fn(async () => ({ activatable: true, diagnostics: [] }));
const app = appFor(registryFake(), diagnose); const app = appFor(registryFake(), diagnose);
+181 -64
View File
@@ -312,37 +312,47 @@ async function pushInvalidWorkspace(source: string): Promise<void> {
await git(source, ["push", "origin", "main"]); await git(source, ["push", "origin", "main"]);
} }
function legacyDigest(contents: string): string { type HistoricalRevisionState = "absent" | "operational" | "migration_required" | "unknown";
return createHash("sha256").update(contents).digest("hex");
function revisionWithHistoricalState(
revision: Record<string, unknown>,
encoding: HistoricalRevisionState,
): Record<string, unknown> {
const { state: _state, ...stateFree } = revision;
return encoding === "absent" ? stateFree : {
...stateFree,
state: encoding === "unknown" ? "retired" : encoding,
};
} }
function persistPreStateManifest(root: string, commit: string): void { function rewritePersistedRevisionStates(
const snapshotDirectory = join(root, "snapshots", commit); root: string,
commit: string,
activeEncoding: HistoricalRevisionState,
manifestEncoding: HistoricalRevisionState,
): void {
const activePath = join(root, "state", "active.json"); const activePath = join(root, "state", "active.json");
const snapshotPath = join(snapshotDirectory, "snapshot.json"); const snapshotPath = join(root, "snapshots", commit, "snapshot.json");
const active = JSON.parse(readFileSync(activePath, "utf8")); const active = JSON.parse(readFileSync(activePath, "utf8"));
const manifest = JSON.parse(readFileSync(snapshotPath, "utf8")); const manifest = JSON.parse(readFileSync(snapshotPath, "utf8"));
const envName = "psd-clinical.env.example"; active.revisions = active.revisions.map((revision: Record<string, unknown>) => (
const docsName = "psd-clinical.md"; revisionWithHistoricalState(revision, activeEncoding)
const envExample = "# Legacy registry artifact\n"; ));
const markdown = "# Legacy registry artifact\n"; manifest.revisions = manifest.revisions.map((revision: Record<string, unknown>) => (
revisionWithHistoricalState(revision, manifestEncoding)
chmodSync(join(snapshotDirectory, envName), 0o600); ));
chmodSync(join(snapshotDirectory, docsName), 0o600);
writeFileSync(join(snapshotDirectory, envName), envExample);
writeFileSync(join(snapshotDirectory, docsName), markdown);
active.revisions = active.revisions.map(({ state: _state, ...revision }: Record<string, unknown>) => revision);
manifest.revisions = manifest.revisions.map(({ state: _state, ...revision }: Record<string, unknown>) => revision);
manifest.files = {
"psd-clinical.yaml": manifest.files["psd-clinical.yaml"],
[envName]: legacyDigest(envExample),
[docsName]: legacyDigest(markdown),
};
writeFileSync(activePath, JSON.stringify(active)); writeFileSync(activePath, JSON.stringify(active));
chmodSync(snapshotPath, 0o600); chmodSync(snapshotPath, 0o600);
writeFileSync(snapshotPath, JSON.stringify(manifest)); writeFileSync(snapshotPath, JSON.stringify(manifest));
} }
function persistedState(root: string, commit: string): { active: any; manifest: any } {
return {
active: JSON.parse(readFileSync(join(root, "state", "active.json"), "utf8")),
manifest: JSON.parse(readFileSync(join(root, "snapshots", commit, "snapshot.json"), "utf8")),
};
}
test("allows first API publication and delete-last from a content-only registry base", async () => { test("allows first API publication and delete-last from a content-only registry base", async () => {
const remote = await contentOnlyFixture(); const remote = await contentOnlyFixture();
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote)); const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
@@ -390,7 +400,6 @@ test("concurrent first lists lazily bootstrap a clean registry once safely", asy
expect.objectContaining({ expect.objectContaining({
id: "psd-clinical", id: "psd-clinical",
commit: remote.initialCommit, commit: remote.initialCommit,
state: "operational",
}), }),
]); ]);
} }
@@ -706,63 +715,171 @@ test.each([
expect(existsSync(join(remote.root, "registry", "state", "active.json"))).toBe(false); expect(existsSync(join(remote.root, "registry", "state", "active.json"))).toBe(false);
}); });
test("migrates a validated pre-state manifest while preserving its v3 operational state", async () => { test("writes only state-free revisions and never exposes revision state", async () => {
const remote = await fixture(); const remote = await fixture();
const root = join(remote.root, "registry"); const root = join(remote.root, "registry");
const firstRegistry = new WorkspaceRegistry(config(root, remote.remote)); const registry = new WorkspaceRegistry(config(root, remote.remote));
await firstRegistry.bootstrap(); await registry.bootstrap();
persistPreStateManifest(root, remote.initialCommit);
const restoredRegistry = new WorkspaceRegistry(config(root, remote.remote)); const initial = persistedState(root, remote.initialCommit);
await expect(restoredRegistry.bootstrap()).resolves.toMatchObject({ expect(Object.keys(initial.active).sort()).toEqual(["head", "revisions"]);
head: remote.initialCommit, expect(Object.keys(initial.manifest).sort()).toEqual(["files", "head", "revisions"]);
degraded: false, expect(Object.keys(initial.active.revisions[0]).sort()).toEqual(["blob", "commit", "id", "snapshotPath"]);
expect(Object.keys(initial.manifest.revisions[0]).sort()).toEqual(["blob", "commit", "id", "snapshotPath"]);
const listed = await registry.list();
const read = await registry.read("psd-clinical");
expect(listed[0]).not.toHaveProperty("state");
expect(read.revision).not.toHaveProperty("state");
const published = await registry.publish({
action: "update",
workspace: workspaceWith("psd-clinical", { name: "State-free revision" }),
baseCommit: remote.initialCommit,
baseBlob: listed[0]!.blob,
}); });
await expect(restoredRegistry.list()).resolves.toMatchObject([ const updated = persistedState(root, published!.commit);
{ id: "psd-clinical", state: "operational" }, expect(updated.active.revisions[0]).not.toHaveProperty("state");
]); expect(updated.manifest.revisions[0]).not.toHaveProperty("state");
expect(published).not.toHaveProperty("state");
const active = JSON.parse(readFileSync(join(root, "state", "active.json"), "utf8"));
const manifest = JSON.parse(readFileSync(join(root, "snapshots", remote.initialCommit, "snapshot.json"), "utf8"));
expect(active.revisions[0].state).toBe("operational");
expect(manifest.revisions[0].state).toBe("operational");
expect(Object.keys(manifest.files).sort()).toEqual([
"psd-clinical.env.example", "psd-clinical.md", "psd-clinical.yaml",
]);
}); });
test("finishes a pre-state active manifest migration after its v3 snapshot was atomically updated", async () => { test("accepts historical operational state without leaking it or rewriting the immutable snapshot", async () => {
const remote = await fixture();
const root = join(remote.root, "registry");
await new WorkspaceRegistry(config(root, remote.remote)).bootstrap();
rewritePersistedRevisionStates(root, remote.initialCommit, "operational", "operational");
const snapshotPath = join(root, "snapshots", remote.initialCommit, "snapshot.json");
const historicalManifest = readFileSync(snapshotPath, "utf8");
const restored = new WorkspaceRegistry(config(root, remote.remote));
const listed = await restored.list();
const read = await restored.read("psd-clinical");
expect(listed[0]).not.toHaveProperty("state");
expect(read.revision).not.toHaveProperty("state");
expect(readFileSync(snapshotPath, "utf8")).toBe(historicalManifest);
await restored.bootstrap();
const rewrittenActive = persistedState(root, remote.initialCommit).active;
expect(rewrittenActive.revisions[0]).not.toHaveProperty("state");
expect(readFileSync(snapshotPath, "utf8")).toBe(historicalManifest);
});
test.each([
["historical active and state-free snapshot", "operational", "absent"],
["state-free active and historical snapshot", "absent", "operational"],
] as const)("normalizes mixed persisted revision encodings: %s", async (_name, activeState, manifestState) => {
const remote = await fixture();
const root = join(remote.root, "registry");
await new WorkspaceRegistry(config(root, remote.remote)).bootstrap();
rewritePersistedRevisionStates(root, remote.initialCommit, activeState, manifestState);
const snapshotPath = join(root, "snapshots", remote.initialCommit, "snapshot.json");
const historicalManifest = readFileSync(snapshotPath, "utf8");
const revisions = await new WorkspaceRegistry(config(root, remote.remote)).list();
expect(revisions[0]).not.toHaveProperty("state");
expect(readFileSync(snapshotPath, "utf8")).toBe(historicalManifest);
});
test.each([
["migration_required in active state", "migration_required", "absent"],
["migration_required in snapshot manifest", "absent", "migration_required"],
["unknown state in active state", "unknown", "operational"],
["unknown state in snapshot manifest", "operational", "unknown"],
] as const)("rejects %s", async (_name, activeState, manifestState) => {
const remote = await fixture();
const root = join(remote.root, "registry");
await new WorkspaceRegistry(config(root, remote.remote)).bootstrap();
rewritePersistedRevisionStates(root, remote.initialCommit, activeState, manifestState);
await expect(new WorkspaceRegistry(config(root, remote.remote)).list()).rejects.toMatchObject({
code: "workspace_invalid",
});
});
test.each([
["active top level", "active", "top"],
["active revision", "active", "revision"],
["snapshot top level", "manifest", "top"],
["snapshot revision", "manifest", "revision"],
] as const)("rejects unknown fields in %s", async (_name, component, location) => {
const remote = await fixture();
const root = join(remote.root, "registry");
await new WorkspaceRegistry(config(root, remote.remote)).bootstrap();
const path = component === "active"
? join(root, "state", "active.json")
: join(root, "snapshots", remote.initialCommit, "snapshot.json");
const persisted = JSON.parse(readFileSync(path, "utf8"));
if (location === "top") persisted.unexpected = true;
else persisted.revisions[0].unexpected = true;
if (component === "manifest") chmodSync(path, 0o600);
writeFileSync(path, JSON.stringify(persisted));
await expect(new WorkspaceRegistry(config(root, remote.remote)).list()).rejects.toMatchObject({
code: "workspace_invalid",
});
});
test("normalizes operational state in retained historical snapshots without rewriting them", async () => {
const remote = await fixture(); const remote = await fixture();
const root = join(remote.root, "registry"); const root = join(remote.root, "registry");
const registry = new WorkspaceRegistry(config(root, remote.remote)); const registry = new WorkspaceRegistry(config(root, remote.remote));
await registry.bootstrap(); await registry.bootstrap();
persistPreStateManifest(root, remote.initialCommit); writeFileSync(join(remote.source, "workspaces", "psd-clinical.yaml"), validYaml.replace(
"name: Policlinico San Donato", "name: Current workspace",
));
await git(remote.source, ["add", "workspaces/psd-clinical.yaml"]);
await git(remote.source, ["commit", "-m", "Update active workspace"]);
await git(remote.source, ["push", "origin", "main"]);
await registry.pull();
rewritePersistedRevisionStates(root, remote.initialCommit, "absent", "operational");
const snapshotPath = join(root, "snapshots", remote.initialCommit, "snapshot.json"); const snapshotPath = join(root, "snapshots", remote.initialCommit, "snapshot.json");
const manifest = JSON.parse(readFileSync(snapshotPath, "utf8")); const historicalManifest = readFileSync(snapshotPath, "utf8");
manifest.revisions[0].state = "operational";
writeFileSync(snapshotPath, JSON.stringify(manifest));
const restoredRegistry = new WorkspaceRegistry(config(root, remote.remote)); const retained = await new WorkspaceRegistry(config(root, remote.remote)).listRetainedSnapshots();
await expect(restoredRegistry.list()).resolves.toMatchObject([
{ id: "psd-clinical", state: "operational" }, expect(retained).toEqual(expect.arrayContaining([
]); expect.objectContaining({ id: "psd-clinical", commit: remote.initialCommit }),
]));
expect(retained.every((revision) => !("state" in revision))).toBe(true);
expect(readFileSync(snapshotPath, "utf8")).toBe(historicalManifest);
}); });
test("rejects a corrupt pre-state manifest rather than accepting it during migration", async () => { test("normalizes historical operational state during offline fallback after restart", async () => {
const remote = await fixture(); const remote = await fixture();
const root = join(remote.root, "registry"); const root = join(remote.root, "registry");
const registry = new WorkspaceRegistry(config(root, remote.remote)); await new WorkspaceRegistry(config(root, remote.remote)).bootstrap();
await registry.bootstrap(); rewritePersistedRevisionStates(root, remote.initialCommit, "operational", "operational");
persistPreStateManifest(root, remote.initialCommit); rmSync(remote.remote, { recursive: true, force: true });
const snapshotPath = join(root, "snapshots", remote.initialCommit, "snapshot.json");
const manifest = JSON.parse(readFileSync(snapshotPath, "utf8"));
manifest.files["psd-clinical.yaml"] = "0".repeat(64);
writeFileSync(snapshotPath, JSON.stringify(manifest));
const restoredRegistry = new WorkspaceRegistry(config(root, remote.remote)); const restored = new WorkspaceRegistry(config(root, remote.remote));
await expect(restoredRegistry.bootstrap()).rejects.toMatchObject({ code: "workspace_invalid" }); await expect(restored.pull()).resolves.toMatchObject({ degraded: true, head: remote.initialCommit });
await expect(restoredRegistry.list()).rejects.toMatchObject({ code: "workspace_invalid" }); const listed = await restored.list();
const read = await restored.read("psd-clinical");
expect(listed[0]).not.toHaveProperty("state");
expect(read.revision).not.toHaveProperty("state");
});
test("fails closed when a retained snapshot descriptor is not schema v3", async () => {
const remote = await fixture();
const root = join(remote.root, "registry");
await new WorkspaceRegistry(config(root, remote.remote)).bootstrap();
const snapshotDirectory = join(root, "snapshots", remote.initialCommit);
const yamlPath = join(snapshotDirectory, "psd-clinical.yaml");
chmodSync(yamlPath, 0o600);
const legacy = legacyV2Yaml();
writeFileSync(yamlPath, legacy);
const manifestPath = join(snapshotDirectory, "snapshot.json");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
manifest.files["psd-clinical.yaml"] = createHash("sha256").update(legacy).digest("hex");
chmodSync(manifestPath, 0o600);
writeFileSync(manifestPath, JSON.stringify(manifest));
await expect(new WorkspaceRegistry(config(root, remote.remote)).list()).rejects.toMatchObject({
code: "workspace_invalid",
});
}); });
test("keeps the last valid snapshot when a pulled commit has invalid YAML", async () => { test("keeps the last valid snapshot when a pulled commit has invalid YAML", async () => {
@@ -889,8 +1006,8 @@ test("lists operational descriptors retained after their workspace was removed f
const retained = await registry.listRetainedSnapshots(); const retained = await registry.listRetainedSnapshots();
expect(retained).toEqual(expect.arrayContaining([ expect(retained).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "psd-clinical", commit: remote.initialCommit, state: "operational" }), expect.objectContaining({ id: "psd-clinical", commit: remote.initialCommit }),
expect.objectContaining({ id: "archive-only", state: "operational" }), expect.objectContaining({ id: "archive-only" }),
])); ]));
}); });