fix: harden workspace activation and snapshot retention
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user