fix: harden workspace activation and snapshot retention

This commit is contained in:
2026-08-04 09:25:06 +02:00
parent 3b23cf3714
commit e4fdbed864
14 changed files with 474 additions and 103 deletions
+95
View File
@@ -30,6 +30,7 @@ const defaultWorkspaceRegistry = {
function buildApp(config: Parameters<typeof buildRealApp>[0], deps: Record<string, unknown> = {}) {
return buildRealApp(config, {
workspaceRuntimeSupport: () => true,
...deps,
workspaceRegistry: { ...defaultWorkspaceRegistry, ...(deps.workspaceRegistry as object | undefined) },
} as any);
@@ -324,6 +325,100 @@ test("creates a session from the active immutable workspace revision", async ()
}));
});
test("rejects an SSH-only workspace before persisting or starting a session", async () => {
const sessionNew = vi.fn(async () => ({ id: "must-not-exist" }));
const ensure = vi.fn(async () => ({ ok: true }));
const createFor = vi.fn();
const abort = vi.fn(async () => {});
const markPersisted = vi.fn(async () => {});
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: { sessionNew, searchPack: async () => {} } as any,
readiness: { ensure } as any,
mgr: { get: () => undefined, createFor } as any,
getSettings: () => ({ workspace: "ssh-workspace" }) as any,
workspaceRuntimeSupport: vi.fn(() => false),
workspaceRegistry: {
acquireSessionRevision: vi.fn(async () => ({
workspace: {
workspace: { schema_version: 2, id: "ssh-workspace", name: "SSH", language: "en" },
dwh: {
engine: "postgres", database: "postgres", schema: "public",
supported_transports: ["ssh_tunnel"],
},
semantic_index: {
vector_store: {
engine: "pgvector", database: "postgres", schema: "vectors",
collection: "documents", dimensions: 768, distance: "cosine",
supported_transports: ["ssh_tunnel"],
},
embedding: {
provider: "ollama_compatible", model: "nomic-embed-text", dimensions: 768,
},
},
llm_policy: { allowed: ["zai/glm-5.2"] },
},
revision: {
id: "ssh-workspace", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/ssh-workspace.yaml`,
state: "operational",
},
abort,
markPersisted,
})),
} as any,
});
const response = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
expect(response.statusCode).toBe(409);
expect(response.json()).toMatchObject({ code: "workspace_not_activatable" });
expect(ensure).not.toHaveBeenCalled();
expect(sessionNew).not.toHaveBeenCalled();
expect(createFor).not.toHaveBeenCalled();
expect(abort).toHaveBeenCalledOnce();
expect(markPersisted).not.toHaveBeenCalled();
});
test("hands a revision lease to retention only after the session manifest is durable", async () => {
const persisted = deferred<{ id: string }>();
const markPersisted = vi.fn(async () => {});
const abort = vi.fn(async () => {});
const acquireSessionRevision = vi.fn(async () => ({
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
revision: {
id: "leased", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/leased.yaml`,
state: "operational",
},
markPersisted,
abort,
}));
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: { sessionNew: () => persisted.promise, searchPack: async () => {} } as any,
readiness: { ensure: async () => ({ ok: true }) } as any,
mgr: {
get: () => undefined,
createFor: () => ({ bridge: { onClientEvent: () => {} } }),
configure: async () => {},
start: () => {},
} as any,
getSettings: () => ({ workspace: "leased", provider: "zai", model: "glm-5.2" }) as any,
listModels: async () => [{ provider: "zai", id: "glm-5.2", name: "GLM", reasoning: true }],
workspaceRuntimeSupport: () => true,
workspaceRegistry: { acquireSessionRevision } as any,
});
const request = app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
await new Promise((resolve) => setImmediate(resolve));
expect(markPersisted).not.toHaveBeenCalled();
expect(abort).not.toHaveBeenCalled();
persisted.resolve({ id: "leased-session" });
expect((await request).statusCode).toBe(200);
expect(markPersisted).toHaveBeenCalledOnce();
expect(abort).not.toHaveBeenCalled();
});
test("creates a session from the configured default workspace revision when workspaceId is omitted", async () => {
const sessionNew = vi.fn(async () => ({ id: "default-pinned" }));
const registry = {
+27
View File
@@ -522,6 +522,33 @@ test("retains a historical snapshot while a resumable manifest still references
expect(existsSync(registry.snapshotPath(currentCommit, "psd-clinical"))).toBe(true);
});
test("a session revision lease survives stale retention scans until its manifest is observed", async () => {
const remote = await fixture();
const root = join(remote.root, "registry");
const registry = new WorkspaceRegistry(config(root, remote.remote));
await registry.bootstrap();
const lease = await registry.acquireSessionRevision("psd-clinical");
writeFileSync(join(remote.source, "workspaces", "psd-clinical.yaml"), validYaml.replace(
"name: Policlinico San Donato", "name: Concurrent revision",
));
await git(remote.source, ["add", "workspaces/psd-clinical.yaml"]);
await git(remote.source, ["commit", "-m", "Publish while session is starting"]);
await git(remote.source, ["push", "origin", "main"]);
await registry.pull();
await registry.reconcileSnapshotRetention([]);
expect(existsSync(registry.snapshotPath(remote.initialCommit, "psd-clinical"))).toBe(true);
await lease.markPersisted();
await registry.reconcileSnapshotRetention([]);
expect(existsSync(registry.snapshotPath(remote.initialCommit, "psd-clinical"))).toBe(true);
await registry.reconcileSnapshotRetention([remote.initialCommit]);
await registry.reconcileSnapshotRetention([]);
expect(existsSync(registry.snapshotPath(remote.initialCommit, "psd-clinical"))).toBe(false);
});
test("lists operational descriptors retained after their workspace was removed from the active revision", async () => {
const remote = await fixture();
const root = join(remote.root, "registry");
@@ -1,6 +1,7 @@
import { expect, test } from "vitest";
import { parse } from "yaml";
import { renderRuntimeConfig, type RuntimeBindings, type RuntimePaths } from "../src/workspaces/runtime-renderer.js";
import { supportsSessionRuntime } from "../src/workspaces/bindings.js";
import { parseWorkspaceYaml } from "../src/workspaces/schema.js";
const workspace = parseWorkspaceYaml(`workspace:
@@ -90,6 +91,18 @@ const directBindings: RuntimeBindings = {
},
};
test("runtime support stays fail-closed for either SSH connector", () => {
expect(supportsSessionRuntime(directBindings)).toBe(true);
expect(supportsSessionRuntime({
...directBindings,
dwh: { ...directBindings.dwh, transport: "ssh_tunnel" },
})).toBe(false);
expect(supportsSessionRuntime({
...directBindings,
vector: { ...directBindings.vector, transport: "ssh_tunnel" },
})).toBe(false);
});
test("renders a direct PostgreSQL binding to the legacy harness shape", () => {
const yaml = renderRuntimeConfig(workspace, directBindings, paths);
const rendered = parse(yaml);
+6 -2
View File
@@ -336,7 +336,7 @@ llm_policy:
}));
});
test("uses a loopback-only SSH tunnel for the bounded connector probe", async () => {
test("does not advertise a probe-only SSH tunnel as usable by runtime sessions", async () => {
const adapters = successfulAdapters();
const sshBindings: RuntimeBindings = {
...bindings,
@@ -360,7 +360,11 @@ test("uses a loopback-only SSH tunnel for the bounded connector probe", async ()
const result = await diagnose(adapters)(workspace, sshBindings, { writeProbe: false });
expect(result.activatable).toBe(true);
expect(result.activatable).toBe(false);
expect(result.diagnostics).toContainEqual(expect.objectContaining({
level: "error",
code: "workspace_not_activatable",
}));
expect(adapters.withSshTunnel).toHaveBeenCalledWith(expect.objectContaining({
localHost: "127.0.0.1",
localPort: 0,