test: verify portable workspace registry end to end
This commit is contained in:
@@ -399,6 +399,16 @@ export function sessionRoutes(
|
||||
.filter((revision) => revision.state === "operational")
|
||||
.map((revision) => runner.sessionList(revision.snapshotPath) as Promise<SessionRow[]>));
|
||||
const list = lists.flat();
|
||||
// Only an administrator-visible complete list (or the single local principal) is safe
|
||||
// input for retention. A remote per-user view can never discard another principal's pin.
|
||||
const reconcileSnapshotRetention = (d.workspaceRegistry as Partial<WorkspaceRegistry>).reconcileSnapshotRetention;
|
||||
const hasCompleteRetentionView = (scope === "all" && principal.isAdmin) || principal.issuer === "local";
|
||||
if (hasCompleteRetentionView && typeof reconcileSnapshotRetention === "function") {
|
||||
const retained = [...new Set(list
|
||||
.filter((row) => row.status !== "finalized" && !row.archived && typeof row.workspace_revision === "string")
|
||||
.map((row) => row.workspace_revision!))];
|
||||
await reconcileSnapshotRetention.call(d.workspaceRegistry, retained);
|
||||
}
|
||||
// Annotate each row with whether a live Pi runtime is currently bound. The client
|
||||
// opens an `active` session straight into its live view (reconnecting to its pending
|
||||
// gate), while a cold session keeps its explicit Resume affordance — so a mere click
|
||||
|
||||
@@ -24,6 +24,9 @@ export interface SessionRow {
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
author: string | null;
|
||||
workspace_id?: string | null;
|
||||
workspace_revision?: string | null;
|
||||
archived?: boolean;
|
||||
}
|
||||
|
||||
export interface SessionDocument {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { lstatSync } from "node:fs";
|
||||
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { buildInstallationContract, renderWorkspaceDocs } from "./contracts.js";
|
||||
import {
|
||||
@@ -164,6 +164,30 @@ export class WorkspaceRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage-collect obsolete immutable snapshots without breaking cold Resume.
|
||||
* Callers must supply revisions collected from an administrator-visible complete session list;
|
||||
* a partial, per-user list could otherwise remove another user's resumable workspace pin.
|
||||
*/
|
||||
async reconcileSnapshotRetention(referencedCommits: readonly string[]): Promise<void> {
|
||||
const retained = new Set(referencedCommits.map(safeCommit));
|
||||
await this.repository.ensureLayout();
|
||||
await this.lock.run(async () => {
|
||||
retained.add((await this.activeState()).head);
|
||||
const entries = await readdir(this.repository.snapshotsPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
// Leave staging and unexpected entries untouched: this cleanup only owns finalized,
|
||||
// commit-addressed snapshot directories.
|
||||
if (!entry.isDirectory() || entry.isSymbolicLink() || !/^[0-9a-f]{40}$/.test(entry.name)) continue;
|
||||
if (retained.has(entry.name)) continue;
|
||||
const path = join(this.repository.snapshotsPath, entry.name);
|
||||
const current = lstatSync(path);
|
||||
if (!current.isDirectory() || current.isSymbolicLink()) continue;
|
||||
await rm(path, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish canonical YAML and derived public documentation as one optimistic Git revision.
|
||||
* The browser never provides paths or generated artifacts; those are derived server-side.
|
||||
|
||||
Reference in New Issue
Block a user