feat: P2 operator, preprocessing state/service, and runtime config lease
This commit is contained in:
@@ -10,18 +10,22 @@ import { ThtRunner } from "../src/tht/tht-runner.js";
|
||||
|
||||
// Spy on child_process.spawn so we can capture the resolved argv (incl. -c config)
|
||||
// that ThtRunner.run() builds, without launching a real process.
|
||||
vi.mock("node:child_process", () => ({
|
||||
spawn: vi.fn(() => {
|
||||
const ch: any = new EventEmitter();
|
||||
ch.stdout = new EventEmitter();
|
||||
ch.stderr = new EventEmitter();
|
||||
queueMicrotask(() => {
|
||||
ch.stdout.emit("data", Buffer.from('{"id":"x"}'));
|
||||
ch.emit("close", 0);
|
||||
});
|
||||
return ch;
|
||||
}),
|
||||
}));
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
||||
return {
|
||||
...actual,
|
||||
spawn: vi.fn(() => {
|
||||
const ch: any = new EventEmitter();
|
||||
ch.stdout = new EventEmitter();
|
||||
ch.stderr = new EventEmitter();
|
||||
queueMicrotask(() => {
|
||||
ch.stdout.emit("data", Buffer.from('{"id":"x"}'));
|
||||
ch.emit("close", 0);
|
||||
});
|
||||
return ch;
|
||||
}),
|
||||
};
|
||||
});
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
test("sessionNew parses id from JSON", async () => {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { expect, test, vi } from "vitest";
|
||||
import {
|
||||
runWorkspaceMaintenanceCli,
|
||||
type WorkspaceMaintenanceIo,
|
||||
} from "../src/workspace-maintenance.js";
|
||||
import type { WorkspaceOperationResult } from "../src/workspaces/preprocessing-service.js";
|
||||
|
||||
function io(stdin: string): WorkspaceMaintenanceIo & { stdout: string[]; stderr: string[] } {
|
||||
const stdout: string[] = [];
|
||||
const stderr: string[] = [];
|
||||
return {
|
||||
stdin,
|
||||
stdout,
|
||||
stderr,
|
||||
writeStdout: (value) => void stdout.push(value),
|
||||
writeStderr: (value) => void stderr.push(value),
|
||||
};
|
||||
}
|
||||
|
||||
function ok(operation: string): WorkspaceOperationResult {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
status: "succeeded",
|
||||
code: "ok",
|
||||
workspaceId: "psd-clinical",
|
||||
workspaceRevision: "a".repeat(40),
|
||||
descriptorBlob: "b".repeat(40),
|
||||
operation,
|
||||
completedStages: [],
|
||||
};
|
||||
}
|
||||
|
||||
test("entrypoint emits exactly one pristine JSON document and maps success/block/failure exits", async () => {
|
||||
const service = {
|
||||
inspect: vi.fn(async () => ok("inspect")),
|
||||
preprocessDwh: vi.fn(async () => ({ ...ok("preprocess dwh"), status: "blocked", code: "manual_review_required" as const })),
|
||||
run: vi.fn(async () => ({ ...ok("preprocess run"), status: "failed", code: "semantic_index_incompatible" as const })),
|
||||
} as any;
|
||||
|
||||
const inspectIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
||||
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "inspect"], service, inspectIo)).toBe(0);
|
||||
expect(JSON.parse(inspectIo.stdout.join(""))).toMatchObject({ operation: "inspect", code: "ok" });
|
||||
expect(inspectIo.stderr.join("")).toBe("");
|
||||
|
||||
const blockedIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
||||
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "preprocess-dwh"], service, blockedIo)).toBe(3);
|
||||
expect(JSON.parse(blockedIo.stdout.join(""))).toMatchObject({ code: "manual_review_required" });
|
||||
|
||||
const failedIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
||||
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "preprocess-run"], service, failedIo)).toBe(1);
|
||||
expect(JSON.parse(failedIo.stdout.join(""))).toMatchObject({ code: "semantic_index_incompatible" });
|
||||
});
|
||||
|
||||
test("malformed stdin, unknown commands, and extra fields fail with exit 2 but still return bounded JSON", async () => {
|
||||
const service = {} as any;
|
||||
|
||||
const malformedIo = io("not-json");
|
||||
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "inspect"], service, malformedIo)).toBe(2);
|
||||
expect(JSON.parse(malformedIo.stdout.join(""))).toMatchObject({ status: "failed", operation: "inspect" });
|
||||
|
||||
const extraFieldIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical", unexpected: true }));
|
||||
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "inspect"], service, extraFieldIo)).toBe(2);
|
||||
expect(JSON.parse(extraFieldIo.stdout.join(""))).toMatchObject({ status: "failed", operation: "inspect" });
|
||||
|
||||
const unknownIo = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
||||
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "explode"], service, unknownIo)).toBe(2);
|
||||
expect(JSON.parse(unknownIo.stdout.join(""))).toMatchObject({ status: "failed", operation: "explode" });
|
||||
});
|
||||
|
||||
test("raw exception text is redacted from stderr and stdout remains within the public result contract", async () => {
|
||||
const service = {
|
||||
inspect: vi.fn(async () => {
|
||||
throw new Error("https://secret.example.invalid?q=token SELECT * FROM sensitive_table");
|
||||
}),
|
||||
} as any;
|
||||
const captured = io(JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical" }));
|
||||
|
||||
expect(await runWorkspaceMaintenanceCli(["node", "workspace-maintenance", "inspect"], service, captured)).toBe(1);
|
||||
expect(JSON.parse(captured.stdout.join(""))).toMatchObject({
|
||||
status: "failed",
|
||||
operation: "inspect",
|
||||
});
|
||||
expect(captured.stderr.join("")).not.toContain("secret.example.invalid");
|
||||
expect(captured.stderr.join("")).not.toContain("SELECT *");
|
||||
});
|
||||
@@ -0,0 +1,333 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { parseWorkspaceYaml } from "../src/workspaces/schema.js";
|
||||
import { PreprocessingStateStore } from "../src/workspaces/preprocessing-state.js";
|
||||
import {
|
||||
WorkspacePreprocessingService,
|
||||
type ChildProcessRequest,
|
||||
} from "../src/workspaces/preprocessing-service.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
|
||||
});
|
||||
|
||||
const semanticRuntime = {
|
||||
internalQdrantUrl: "http://qdrant:6333",
|
||||
internalEmbeddingUrl: "http://embedding:11434",
|
||||
internalEmbeddingModel: "qwen3-embedding:0.6b",
|
||||
internalEmbeddingDimensions: 1024,
|
||||
};
|
||||
|
||||
const baseWorkspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
id: psd-clinical
|
||||
name: Runtime Lease
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
const filesystemWorkspace = parseWorkspaceYaml(`${baseWorkspace ? '' : ''}workspace:
|
||||
schema_version: 3
|
||||
id: fs-workspace
|
||||
name: Filesystem
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: fs-workspace
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
uri: fs-workspace/evidence
|
||||
`);
|
||||
const privateHttpWorkspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
id: http-workspace
|
||||
name: Http
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: http-workspace
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
evidence:
|
||||
source:
|
||||
type: http
|
||||
uris: [http://127.0.0.1/private.md]
|
||||
authentication: none
|
||||
connect_timeout_ms: 1000
|
||||
read_timeout_ms: 2000
|
||||
max_bytes: 100
|
||||
max_redirects: 0
|
||||
allow_private_hosts: true
|
||||
max_cache_bytes: 100
|
||||
`);
|
||||
|
||||
function runtime(workspace = baseWorkspace, workspaceId = workspace.workspace.id) {
|
||||
return {
|
||||
workspace,
|
||||
workspaceId,
|
||||
workspaceRevision: "a".repeat(40),
|
||||
descriptorBlob: "b".repeat(40),
|
||||
catalogBlob: "c".repeat(40),
|
||||
configLease: {
|
||||
path: `/data/sessions/${workspaceId}/preprocessing/runtime-config/${"a".repeat(40)}.yaml`,
|
||||
workspaceId,
|
||||
workspaceRevision: "a".repeat(40),
|
||||
descriptorBlob: "b".repeat(40),
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
release: () => undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(workspace = baseWorkspace) {
|
||||
const dataRoot = mkdtempSync(join(tmpdir(), "tht-preprocessing-service-"));
|
||||
roots.push(dataRoot);
|
||||
const requests: ChildProcessRequest[] = [];
|
||||
const runChild = vi.fn(async (request: ChildProcessRequest) => {
|
||||
requests.push(request);
|
||||
return { exitCode: 0, stdout: JSON.stringify({ status: "succeeded" }), stderr: "" };
|
||||
});
|
||||
const service = new WorkspacePreprocessingService({
|
||||
dataRoot,
|
||||
acquireActiveRuntime: async () => runtime(workspace),
|
||||
runChild,
|
||||
listSessions: async () => [],
|
||||
semanticPreflight: async () => ({ ok: true }),
|
||||
});
|
||||
return { dataRoot, runChild, requests, service };
|
||||
}
|
||||
|
||||
test("preprocess dwh uses fixed argv and resumes outer state without rerunning a completed stage", async () => {
|
||||
const f = fixture();
|
||||
f.runChild.mockResolvedValueOnce({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({ status: "succeeded", run_id: "d".repeat(32) }),
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const first = await f.service.preprocessDwh({ workspaceId: "psd-clinical" });
|
||||
expect(first).toMatchObject({
|
||||
status: "succeeded",
|
||||
code: "ok",
|
||||
operation: "preprocess dwh",
|
||||
completedStages: ["dwh"],
|
||||
childRuns: { dwh: "d".repeat(32) },
|
||||
});
|
||||
expect((f.runChild.mock.calls[0]![0] as ChildProcessRequest).argv).toEqual([
|
||||
"preprocess", "dwh", "--steps", "introspect,lsh", "--json", "-c", "/dev/fd/3",
|
||||
]);
|
||||
|
||||
const second = await f.service.preprocessDwh({
|
||||
workspaceId: "psd-clinical",
|
||||
resumeRunId: first.runId!,
|
||||
});
|
||||
expect(second.status).toBe("unchanged");
|
||||
expect(f.runChild).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("schema suggest-fks publishes a candidate artifact and blocks full runs for manual review", async () => {
|
||||
const f = fixture();
|
||||
f.runChild
|
||||
.mockResolvedValueOnce({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({ status: "succeeded", run_id: "d".repeat(32) }),
|
||||
stderr: "",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({
|
||||
status: "succeeded",
|
||||
candidate_count: 1,
|
||||
candidate_digest: "sha256:" + "e".repeat(64),
|
||||
candidate_yaml: "tables: []\n",
|
||||
}),
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const result = await f.service.run({ workspaceId: "psd-clinical" });
|
||||
expect(result).toMatchObject({
|
||||
status: "blocked",
|
||||
code: "manual_review_required",
|
||||
completedStages: ["dwh", "fk_suggest"],
|
||||
});
|
||||
expect(f.runChild.mock.calls.map(([request]) => (request as ChildProcessRequest).argv[0])).toEqual(["preprocess", "schema"]);
|
||||
});
|
||||
|
||||
test("schema check requires the exact candidate digest, stages annotations via temp file, and persists the review", async () => {
|
||||
const f = fixture();
|
||||
f.runChild.mockResolvedValueOnce({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({
|
||||
status: "succeeded",
|
||||
candidate_count: 1,
|
||||
candidate_digest: "sha256:" + "e".repeat(64),
|
||||
candidate_yaml: "tables: []\n",
|
||||
}),
|
||||
stderr: "",
|
||||
});
|
||||
const suggest = await f.service.suggestFks({ workspaceId: "psd-clinical" });
|
||||
await expect(f.service.checkSchema({
|
||||
workspaceId: "psd-clinical",
|
||||
annotationsYaml: "tables: {}\n",
|
||||
reviewedCandidatesDigest: "sha256:" + "f".repeat(64),
|
||||
})).resolves.toMatchObject({ status: "failed", code: "annotation_invalid" });
|
||||
|
||||
const reviewedDigest = suggest.artifactIdentities![0]!.digest;
|
||||
let stagedPath = "";
|
||||
f.runChild.mockImplementationOnce(async (request: ChildProcessRequest) => {
|
||||
stagedPath = request.argv[request.argv.indexOf("--annotations") + 1]!;
|
||||
expect(readFileSync(stagedPath, "utf8")).toBe("tables: {}\n");
|
||||
expect(request.argv).toEqual([
|
||||
"schema", "check", "--annotations", stagedPath,
|
||||
"--reviewed-candidates", reviewedDigest,
|
||||
"--json", "-c", "/dev/fd/3",
|
||||
]);
|
||||
return {
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({
|
||||
status: "succeeded",
|
||||
orphan_count: 0,
|
||||
annotations_digest: "sha256:annotations",
|
||||
reviewed_candidates_digest: reviewedDigest,
|
||||
}),
|
||||
stderr: "",
|
||||
};
|
||||
});
|
||||
|
||||
const checked = await f.service.checkSchema({
|
||||
workspaceId: "psd-clinical",
|
||||
annotationsYaml: "tables: {}\n",
|
||||
reviewedCandidatesDigest: reviewedDigest,
|
||||
});
|
||||
expect(checked).toMatchObject({ status: "succeeded", code: "ok" });
|
||||
expect(() => readFileSync(stagedPath, "utf8")).toThrow();
|
||||
const state = new PreprocessingStateStore({ dataRoot: f.dataRoot, workspaceId: "psd-clinical" });
|
||||
expect(state.readFkReview(suggest.runId!)?.reviewedCandidatesDigest).toBe(reviewedDigest);
|
||||
});
|
||||
|
||||
test("index schema fails closed when semantic preflight refuses the collection", async () => {
|
||||
const dataRoot = mkdtempSync(join(tmpdir(), "tht-preprocessing-service-"));
|
||||
roots.push(dataRoot);
|
||||
const runChild = vi.fn();
|
||||
const service = new WorkspacePreprocessingService({
|
||||
dataRoot,
|
||||
acquireActiveRuntime: async () => runtime(baseWorkspace),
|
||||
runChild,
|
||||
listSessions: async () => [],
|
||||
semanticPreflight: async () => ({ ok: false, code: "semantic_index_incompatible" }),
|
||||
});
|
||||
|
||||
const result = await service.indexSchema({ workspaceId: "psd-clinical" });
|
||||
expect(result).toMatchObject({ status: "failed", code: "semantic_index_incompatible" });
|
||||
expect(runChild).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("evidence stops before child execution for filesystem sources and refuses private HTTP hosts outside the allowlist", async () => {
|
||||
const filesystem = fixture(filesystemWorkspace);
|
||||
const blocked = await filesystem.service.preprocessEvidence({ workspaceId: "fs-workspace" });
|
||||
expect(blocked).toMatchObject({ status: "blocked", code: "evidence_materialization_required" });
|
||||
expect(filesystem.runChild).not.toHaveBeenCalled();
|
||||
|
||||
const httpDataRoot = mkdtempSync(join(tmpdir(), "tht-preprocessing-service-"));
|
||||
roots.push(httpDataRoot);
|
||||
const httpService = new WorkspacePreprocessingService({
|
||||
dataRoot: httpDataRoot,
|
||||
acquireActiveRuntime: async () => runtime(privateHttpWorkspace, "http-workspace"),
|
||||
runChild: vi.fn(),
|
||||
listSessions: async () => [],
|
||||
semanticPreflight: async () => ({ ok: true }),
|
||||
httpPrivateHostAllowlist: ["metadata.internal"],
|
||||
});
|
||||
|
||||
const refused = await httpService.preprocessEvidence({ workspaceId: "http-workspace" });
|
||||
expect(refused).toMatchObject({ status: "failed", code: "egress_policy_refused" });
|
||||
});
|
||||
|
||||
test("full runs follow the explicit order and finish unchanged when no Evidence source exists", async () => {
|
||||
const f = fixture();
|
||||
f.runChild
|
||||
.mockResolvedValueOnce({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({ status: "succeeded", run_id: "d".repeat(32) }),
|
||||
stderr: "",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({
|
||||
status: "succeeded",
|
||||
candidate_count: 0,
|
||||
candidate_digest: "sha256:" + "0".repeat(64),
|
||||
candidate_yaml: "tables: []\n",
|
||||
}),
|
||||
stderr: "",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify({
|
||||
status: "succeeded",
|
||||
counts: { added: 1, updated: 0, deleted: 0, unchanged: 0 },
|
||||
}),
|
||||
stderr: "",
|
||||
});
|
||||
|
||||
const result = await f.service.run({ workspaceId: "psd-clinical" });
|
||||
expect(result).toMatchObject({
|
||||
status: "succeeded",
|
||||
code: "ok",
|
||||
completedStages: ["dwh", "fk_suggest", "schema_index"],
|
||||
warnings: ["workspace has no Evidence source"],
|
||||
});
|
||||
expect(f.runChild.mock.calls.map(([request]) => (request as ChildProcessRequest).argv.slice(0, 2).join(" "))).toEqual([
|
||||
"preprocess dwh",
|
||||
"schema suggest-fks",
|
||||
"vector index-schema",
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { PreprocessingStateStore } from "../src/workspaces/preprocessing-state.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
|
||||
});
|
||||
|
||||
function fixture() {
|
||||
const dataRoot = mkdtempSync(join(tmpdir(), "tht-preprocessing-state-"));
|
||||
roots.push(dataRoot);
|
||||
return {
|
||||
dataRoot,
|
||||
store: new PreprocessingStateStore({ dataRoot, workspaceId: "psd-clinical" }),
|
||||
};
|
||||
}
|
||||
|
||||
test("job state creates durable 0600 JSON and enforces same-revision resume", async () => {
|
||||
const { store } = fixture();
|
||||
const job = await store.beginJob({
|
||||
operation: "preprocess dwh",
|
||||
workspaceRevision: "a".repeat(40),
|
||||
descriptorBlob: "b".repeat(40),
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
});
|
||||
|
||||
const path = store.jobPath(job.runId);
|
||||
expect(existsSync(path)).toBe(true);
|
||||
expect(statSync(path).mode & 0o777).toBe(0o600);
|
||||
expect(JSON.parse(readFileSync(path, "utf8"))).toMatchObject({
|
||||
schemaVersion: 1,
|
||||
operation: "preprocess dwh",
|
||||
workspaceId: "psd-clinical",
|
||||
workspaceRevision: "a".repeat(40),
|
||||
descriptorBlob: "b".repeat(40),
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
});
|
||||
|
||||
await expect(store.beginJob({
|
||||
operation: "preprocess dwh",
|
||||
runId: job.runId,
|
||||
workspaceRevision: "d".repeat(40),
|
||||
descriptorBlob: "b".repeat(40),
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
})).rejects.toMatchObject({ code: "preprocessing_resume_mismatch" });
|
||||
|
||||
const resumed = await store.beginJob({
|
||||
operation: "preprocess dwh",
|
||||
runId: job.runId,
|
||||
workspaceRevision: "a".repeat(40),
|
||||
descriptorBlob: "b".repeat(40),
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
});
|
||||
expect(resumed.runId).toBe(job.runId);
|
||||
});
|
||||
|
||||
test("writer lock rejects a concurrent contender and the kernel releases it after holder death", async () => {
|
||||
const f = fixture();
|
||||
const other = new PreprocessingStateStore({ dataRoot: f.dataRoot, workspaceId: "psd-clinical" });
|
||||
const first = await f.store.acquireWriterLock();
|
||||
await expect(other.acquireWriterLock()).rejects.toMatchObject({ code: "preprocessing_conflict" });
|
||||
|
||||
process.kill(first.holderPid, "SIGKILL");
|
||||
const deadline = Date.now() + 5_000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const recovered = await other.acquireWriterLock();
|
||||
await recovered.release();
|
||||
return;
|
||||
} catch (error) {
|
||||
if ((error as { code?: string }).code !== "preprocessing_conflict") throw error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
throw new Error("writer lock was not released after holder death");
|
||||
});
|
||||
|
||||
test("session inventory guard blocks resumable sessions pinned to a different revision", async () => {
|
||||
const { store } = fixture();
|
||||
|
||||
await expect(store.assertSessionInventoryCompatible("a".repeat(40), [
|
||||
{ id: "open-other", status: "closed", archived: false, workspaceRevision: "b".repeat(40) },
|
||||
])).rejects.toMatchObject({ code: "preprocessing_conflict" });
|
||||
|
||||
await expect(store.assertSessionInventoryCompatible("a".repeat(40), [
|
||||
{ id: "current", status: "open", archived: false, workspaceRevision: "a".repeat(40) },
|
||||
{ id: "finalized", status: "finalized", archived: false, workspaceRevision: "b".repeat(40) },
|
||||
{ id: "archived", status: "closed", archived: true, workspaceRevision: "c".repeat(40) },
|
||||
])).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("candidate and review artifacts are digest-bound durable files", async () => {
|
||||
const { store } = fixture();
|
||||
const runId = "1".repeat(32);
|
||||
const candidate = await store.writeFkCandidates(runId, `tables: []
|
||||
`);
|
||||
const review = await store.writeFkReview(runId, {
|
||||
reviewedCandidatesDigest: candidate.digest,
|
||||
annotationsDigest: "sha256:annotations",
|
||||
workspaceRevision: "a".repeat(40),
|
||||
});
|
||||
|
||||
expect(candidate.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
|
||||
expect(review.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
|
||||
expect(store.readFkCandidates(runId)?.digest).toBe(candidate.digest);
|
||||
expect(store.readFkReview(runId)?.reviewedCandidatesDigest).toBe(candidate.digest);
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { WorkspaceRegistry } from "../src/workspaces/registry.js";
|
||||
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
||||
import {
|
||||
publishDeterministicRuntimeConfigLease,
|
||||
renderActiveWorkspaceRuntime,
|
||||
renderWorkspaceRuntimeFromSnapshotPath,
|
||||
} from "../src/workspaces/runtime-config-lease.js";
|
||||
|
||||
const runFile = promisify(execFile);
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
|
||||
});
|
||||
|
||||
async function git(cwd: string, args: string[]): Promise<string> {
|
||||
return (await runFile("git", args, { cwd })).stdout.trim();
|
||||
}
|
||||
|
||||
async function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), "tht-runtime-lease-"));
|
||||
roots.push(root);
|
||||
const remote = join(root, "remote.git");
|
||||
const source = join(root, "source");
|
||||
const registryRoot = join(root, "registry");
|
||||
const secretRoot = join(root, "secrets");
|
||||
const dataRoot = join(root, "data");
|
||||
const harnessDir = join(root, "harness");
|
||||
mkdirSync(harnessDir, { recursive: true });
|
||||
mkdirSync(join(harnessDir, "config"), { recursive: true });
|
||||
writeFileSync(join(harnessDir, "config", "tht.yaml"), `session_storage:
|
||||
mode: local
|
||||
profile: server
|
||||
`);
|
||||
|
||||
await git(root, ["init", "--bare", "--initial-branch=main", remote]);
|
||||
mkdirSync(source);
|
||||
await git(source, ["init", "--initial-branch=main"]);
|
||||
await git(source, ["config", "user.name", "Runtime Lease Test"]);
|
||||
await git(source, ["config", "user.email", "runtime-lease@example.invalid"]);
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), `schema_version: 1
|
||||
workspaces: [{id: psd-clinical, name: Runtime Lease}]
|
||||
`);
|
||||
mkdirSync(join(source, "psd-clinical", "evidence"), { recursive: true });
|
||||
writeFileSync(join(source, "psd-clinical", "workspace.yaml"), `workspace:
|
||||
schema_version: 3
|
||||
id: psd-clinical
|
||||
name: Runtime Lease
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
uri: psd-clinical/evidence
|
||||
`);
|
||||
writeFileSync(join(source, "psd-clinical", "evidence", "guide.md"), `# hello
|
||||
`);
|
||||
await git(source, ["add", "."]);
|
||||
await git(source, ["commit", "-m", "Canonical workspace"]);
|
||||
await git(source, ["remote", "add", "origin", remote]);
|
||||
await git(source, ["push", "origin", "main"]);
|
||||
|
||||
mkdirSync(secretRoot);
|
||||
const passwordFile = join(secretRoot, "dwh-password");
|
||||
writeFileSync(passwordFile, "secret", { mode: 0o600 });
|
||||
chmodSync(passwordFile, 0o600);
|
||||
mkdirSync(dataRoot);
|
||||
|
||||
const registryConfig: WorkspaceRegistryConfig = {
|
||||
root: registryRoot,
|
||||
remoteUrl: remote,
|
||||
branch: "main",
|
||||
gitAuthorName: "Runtime Lease Test",
|
||||
gitAuthorEmail: "runtime-lease@example.invalid",
|
||||
installationId: "test",
|
||||
secretRoots: [secretRoot],
|
||||
maxImportBytes: 1024 * 1024,
|
||||
maxImportEntries: 16,
|
||||
};
|
||||
const registry = new WorkspaceRegistry(registryConfig);
|
||||
await registry.bootstrap();
|
||||
const revision = (await registry.list())[0];
|
||||
|
||||
vi.stubEnv("THT_WS_PSD_CLINICAL_DWH_TRANSPORT", "postgres_direct");
|
||||
vi.stubEnv("THT_WS_PSD_CLINICAL_DWH_HOST", "warehouse.internal");
|
||||
vi.stubEnv("THT_WS_PSD_CLINICAL_DWH_PORT", "5432");
|
||||
vi.stubEnv("THT_WS_PSD_CLINICAL_DWH_USER", "reader");
|
||||
vi.stubEnv("THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE", passwordFile);
|
||||
|
||||
return {
|
||||
dataRoot,
|
||||
harnessDir,
|
||||
registry,
|
||||
registryConfig,
|
||||
revision,
|
||||
};
|
||||
}
|
||||
|
||||
const semanticRuntime = {
|
||||
internalQdrantUrl: "http://qdrant:6333",
|
||||
internalEmbeddingUrl: "http://embedding:11434",
|
||||
internalEmbeddingModel: "qwen3-embedding:0.6b",
|
||||
internalEmbeddingDimensions: 1024,
|
||||
};
|
||||
|
||||
test("active workspace rendering is byte-identical to direct snapshot rendering", async () => {
|
||||
const f = await fixture();
|
||||
const direct = renderWorkspaceRuntimeFromSnapshotPath({
|
||||
snapshotPath: f.revision.snapshotPath,
|
||||
harnessDir: f.harnessDir,
|
||||
configPath: "config/tht.yaml",
|
||||
dataRoot: f.dataRoot,
|
||||
secretRoots: f.registryConfig.secretRoots,
|
||||
semanticRuntime,
|
||||
});
|
||||
const active = await renderActiveWorkspaceRuntime({
|
||||
workspaceId: "psd-clinical",
|
||||
registry: f.registry,
|
||||
registryConfig: f.registryConfig,
|
||||
harnessDir: f.harnessDir,
|
||||
configPath: "config/tht.yaml",
|
||||
dataRoot: f.dataRoot,
|
||||
secretRoots: f.registryConfig.secretRoots,
|
||||
semanticRuntime,
|
||||
});
|
||||
|
||||
expect(active.renderedConfig).toBe(direct.renderedConfig);
|
||||
expect(active.workspaceRevision).toBe(f.revision.commit);
|
||||
expect(active.descriptorBlob).toBe(f.revision.blob);
|
||||
expect(active.catalogBlob).toMatch(/^[0-9a-f]{40}$/);
|
||||
});
|
||||
|
||||
test("deterministic operator leases publish one revision-bound protected config and refuse changed same-revision bytes", async () => {
|
||||
const f = await fixture();
|
||||
const first = await publishDeterministicRuntimeConfigLease({
|
||||
workspaceId: "psd-clinical",
|
||||
registry: f.registry,
|
||||
registryConfig: f.registryConfig,
|
||||
harnessDir: f.harnessDir,
|
||||
configPath: "config/tht.yaml",
|
||||
dataRoot: f.dataRoot,
|
||||
secretRoots: f.registryConfig.secretRoots,
|
||||
semanticRuntime,
|
||||
});
|
||||
const second = await publishDeterministicRuntimeConfigLease({
|
||||
workspaceId: "psd-clinical",
|
||||
registry: f.registry,
|
||||
registryConfig: f.registryConfig,
|
||||
harnessDir: f.harnessDir,
|
||||
configPath: "config/tht.yaml",
|
||||
dataRoot: f.dataRoot,
|
||||
secretRoots: f.registryConfig.secretRoots,
|
||||
semanticRuntime,
|
||||
});
|
||||
|
||||
expect(second.path).toBe(first.path);
|
||||
expect(first.path).toBe(join(
|
||||
f.dataRoot,
|
||||
"sessions",
|
||||
"psd-clinical",
|
||||
"preprocessing",
|
||||
"runtime-config",
|
||||
`${f.revision.commit}.yaml`,
|
||||
));
|
||||
expect(statSync(first.path).mode & 0o777).toBe(0o400);
|
||||
expect(statSync(first.manifestPath).mode & 0o777).toBe(0o600);
|
||||
expect(readFileSync(first.path, "utf8")).toContain("collection_lifecycle: require_existing");
|
||||
expect(existsSync(first.manifestPath)).toBe(true);
|
||||
|
||||
vi.stubEnv("THT_WS_PSD_CLINICAL_DWH_HOST", "warehouse-two.internal");
|
||||
await expect(publishDeterministicRuntimeConfigLease({
|
||||
workspaceId: "psd-clinical",
|
||||
registry: f.registry,
|
||||
registryConfig: f.registryConfig,
|
||||
harnessDir: f.harnessDir,
|
||||
configPath: "config/tht.yaml",
|
||||
dataRoot: f.dataRoot,
|
||||
secretRoots: f.registryConfig.secretRoots,
|
||||
semanticRuntime,
|
||||
})).rejects.toMatchObject({ code: "effective_config_mismatch" });
|
||||
});
|
||||
|
||||
test("runtime rendering rejects untrusted snapshot paths and symlinks", async () => {
|
||||
const f = await fixture();
|
||||
const outside = join(f.dataRoot, "outside.yaml");
|
||||
writeFileSync(outside, readFileSync(f.revision.snapshotPath, "utf8"));
|
||||
const symlink = join(f.dataRoot, "alias.yaml");
|
||||
symlinkSync(f.revision.snapshotPath, symlink);
|
||||
|
||||
expect(() => renderWorkspaceRuntimeFromSnapshotPath({
|
||||
snapshotPath: outside,
|
||||
harnessDir: f.harnessDir,
|
||||
configPath: "config/tht.yaml",
|
||||
dataRoot: f.dataRoot,
|
||||
secretRoots: f.registryConfig.secretRoots,
|
||||
semanticRuntime,
|
||||
})).toThrow(/trusted runtime snapshot/i);
|
||||
expect(() => renderWorkspaceRuntimeFromSnapshotPath({
|
||||
snapshotPath: symlink,
|
||||
harnessDir: f.harnessDir,
|
||||
configPath: "config/tht.yaml",
|
||||
dataRoot: f.dataRoot,
|
||||
secretRoots: f.registryConfig.secretRoots,
|
||||
semanticRuntime,
|
||||
})).toThrow(/trusted runtime snapshot/i);
|
||||
});
|
||||
Reference in New Issue
Block a user