import { execFile } from "node:child_process"; import { chmodSync, existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { promisify } from "node:util"; import { afterEach, expect, test, vi } from "vitest"; import { parse } from "yaml"; import { buildApp } from "../src/app.js"; import { loadConfig } from "../src/config.js"; import { ThtRunner } from "../src/tht/tht-runner.js"; import { WorkspaceRegistry } from "../src/workspaces/registry.js"; import { WorkspaceSecretStore } from "../src/workspaces/secret-store.js"; import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js"; const runFile = promisify(execFile); const harnessDir = resolve("../harness"); const thtBin = join(harnessDir, ".venv", "bin", "tht"); const roots: string[] = []; const canonicalWorkspace = `workspace: schema_version: 4 id: psd-clinical name: Runtime handoff language: en dwh: engine: postgres database: analytics schema: mart supported_transports: [postgres_direct] `; const filesystemWorkspace = `${canonicalWorkspace}evidence: source: type: filesystem uri: psd-clinical/evidence `; function evidenceWorkspace(source: string, policy = ""): string { return `${canonicalWorkspace}evidence: source: ${source}${policy}`; } afterEach(() => { vi.unstubAllEnvs(); roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })); }); async function git(cwd: string, args: string[]): Promise { return (await runFile("git", args, { cwd })).stdout.trim(); } async function fixture(workspaceSource = filesystemWorkspace) { const root = mkdtempSync(join(tmpdir(), "tht-runtime-handoff-")); 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"); 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 Handoff Test"]); await git(source, ["config", "user.email", "runtime-handoff@example.invalid"]); writeFileSync(join(source, "thoth-workspaces.yaml"), "schema_version: 1\nworkspaces: [{id: psd-clinical, name: Runtime handoff}]\n"); mkdirSync(join(source, "psd-clinical"), { recursive: true }); writeFileSync(join(source, "psd-clinical", "workspace.yaml"), workspaceSource); const evidenceRoot = join(source, "psd-clinical", "evidence"); mkdirSync(evidenceRoot, { recursive: true }); writeFileSync(join(evidenceRoot, "guide.md"), "# Immutable revision evidence\n"); 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 secretContents: Record = { "dwh-password": "dwh-password-value", "evidence-signed-urls.json": JSON.stringify([ "https://evidence.example.test/guide.md?token=SIGNED-HANDOFF-CANARY", ]), "evidence-access": "ACCESS-HANDOFF-CANARY", "evidence-secret": "SECRET-HANDOFF-CANARY", "evidence-token": "TOKEN-HANDOFF-CANARY", }; for (const [name, contents] of Object.entries(secretContents)) { const path = join(secretRoot, name); writeFileSync(path, contents, { mode: 0o600 }); chmodSync(path, 0o600); } mkdirSync(dataRoot); const registryConfig: WorkspaceRegistryConfig = { root: registryRoot, remoteUrl: remote, branch: "main", gitAuthorName: "Runtime Handoff Test", gitAuthorEmail: "runtime-handoff@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]; const environment = { THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct", THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.invalid", THT_WS_PSD_CLINICAL_DWH_PORT: "5432", THT_WS_PSD_CLINICAL_DWH_USER: "reader", THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: join(secretRoot, "dwh-password"), THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE: join(secretRoot, "evidence-signed-urls.json"), THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE: join(secretRoot, "evidence-access"), THT_WS_PSD_CLINICAL_EVIDENCE_SECRET_KEY_FILE: join(secretRoot, "evidence-secret"), THT_WS_PSD_CLINICAL_EVIDENCE_SESSION_TOKEN_FILE: join(secretRoot, "evidence-token"), }; for (const [name, value] of Object.entries(environment)) vi.stubEnv(name, value); vi.stubEnv("THT_HOME", join(root, "home")); return { root, source, dataRoot, secretRoot, registry, registryConfig, revision }; } function runnerFor(f: Awaited>): ThtRunner { return new ThtRunner({ thtBin, harnessDir, configPath: "config/tht.yaml", dataRoot: f.dataRoot, runtimeSnapshotRoot: join(f.registryConfig.root, "snapshots", "runtime"), secretRoots: f.registryConfig.secretRoots, } as any); } test("real schema-v4 registry revision loads through ThtRunner and the harness contract", async () => { const f = await fixture(); const runner = runnerFor(f); expect(await runner.sessionList(f.revision.snapshotPath)).toEqual([]); const created = await runner.sessionNew({ question: "runtime handoff", workspaceConfigPath: f.revision.snapshotPath, workspaceId: f.revision.id, workspaceRevision: f.revision.commit, }); expect(await runner.sessionShow(created.id, f.revision.snapshotPath)).toMatchObject({ id: created.id, workspace_id: "psd-clinical", workspace_revision: f.revision.commit, }); expect(existsSync(join( f.dataRoot, "sessions", "psd-clinical", "sessions", created.id, "session_manifest.yaml", ))).toBe(true); expect(readdirSync(join(f.registryConfig.root, "snapshots", "runtime"))).toEqual([]); }, 15_000); test("ThtRunner uses a vault secret only for the lifetime of its runtime lease", async () => { const f = await fixture(); vi.stubEnv("THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE", ""); const vaultRoot = join(f.root, "workspace-secrets"); const runtimeRoot = join(f.root, "workspace-secret-runtime"); const secretStore = new WorkspaceSecretStore({ root: vaultRoot, runtimeRoot, installationId: "test", }); secretStore.put("psd-clinical", "dwh.password", "vault-runtime-password"); const runner = new ThtRunner({ thtBin, harnessDir, configPath: "config/tht.yaml", dataRoot: f.dataRoot, runtimeSnapshotRoot: join(f.registryConfig.root, "snapshots", "runtime"), secretRoots: f.registryConfig.secretRoots, workspaceSecretStore: secretStore, } as any); const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath); const rendered = parse(readFileSync(lease.path, "utf8")) as { database: { password_file: string }; }; expect(readFileSync(rendered.database.password_file, "utf8")).toBe("vault-runtime-password"); lease.release(); expect(existsSync(rendered.database.password_file)).toBe(false); }); test("ThtRunner binds and cleans the effective relationship snapshot with its runtime lease", async () => { const f = await fixture(); const runner = runnerFor(f); const relationships = JSON.stringify({ schemaVersion: 1, workspaceId: "psd-clinical", relationships: [], }); const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath, relationships); const rendered = parse(readFileSync(lease.path, "utf8")) as { paths: { effective_relationships: string }; }; expect(readFileSync(rendered.paths.effective_relationships, "utf8")).toBe(relationships); lease.release(); expect(existsSync(rendered.paths.effective_relationships)).toBe(false); }); test("separate runtime leases hand off byte-identical revision Evidence configs accepted by tht", async () => { const f = await fixture(); const runner = runnerFor(f); const first = runner.acquireWorkspaceRuntime(f.revision.snapshotPath); const second = runner.acquireWorkspaceRuntime(f.revision.snapshotPath); const expectedRoot = join( f.registryConfig.root, "snapshots", f.revision.commit, "psd-clinical", "evidence", ); try { expect(first.path).not.toBe(second.path); const firstYaml = readFileSync(first.path, "utf8"); const secondYaml = readFileSync(second.path, "utf8"); expect(secondYaml).toBe(firstYaml); expect(parse(firstYaml).runtime_identity).toEqual({ workspace_id: "psd-clinical", workspace_revision: f.revision.commit, source_identity: "workspace://psd-clinical", }); expect(parse(firstYaml).evidence).toEqual({ sources: [{ type: "filesystem", root: expectedRoot, patterns: ["**/*.md"], max_bytes: 10_485_760, }], }); expect(parse(firstYaml).vector).toEqual({ max_chunk_chars: 4_000, retain_published_generations: 3, }); expect(expectedRoot).not.toContain(join(f.registryConfig.root, "repo")); for (const lease of [first, second]) { const checked = await runFile(thtBin, ["config", "check", "-c", lease.path], { cwd: harnessDir, env: { ...process.env, THT_HOME: join(f.root, "home") }, }); expect(`${checked.stdout}${checked.stderr}`).not.toContain("HANDOFF-CANARY"); } first.release(); expect(existsSync(first.path)).toBe(false); expect(existsSync(second.path)).toBe(true); second.release(); expect(existsSync(second.path)).toBe(false); } finally { first.release(); second.release(); } }); test("real Evidence-content-only commit changes runtime identity and root with identical descriptor YAML", async () => { const f = await fixture(); const runner = runnerFor(f); const first = runner.acquireWorkspaceRuntime(f.revision.snapshotPath); const descriptorBefore = readFileSync(f.revision.snapshotPath, "utf8"); writeFileSync( join(f.source, "psd-clinical", "evidence", "guide.md"), "# Content-only revision two\n", ); await git(f.source, ["add", "psd-clinical/evidence/guide.md"]); await git(f.source, ["commit", "-m", "Update Evidence content only"]); await git(f.source, ["push", "origin", "main"]); await f.registry.pull(); const current = (await f.registry.list())[0]; const second = runner.acquireWorkspaceRuntime(current.snapshotPath); try { expect(current.commit).not.toBe(f.revision.commit); expect(current.blob).toBe(f.revision.blob); expect(readFileSync(current.snapshotPath, "utf8")).toBe(descriptorBefore); const firstRendered = parse(readFileSync(first.path, "utf8")); const secondRendered = parse(readFileSync(second.path, "utf8")); expect(firstRendered.runtime_identity.workspace_revision).toBe(f.revision.commit); expect(secondRendered.runtime_identity.workspace_revision).toBe(current.commit); expect(secondRendered.evidence.sources[0].root).toBe(join( f.registryConfig.root, "snapshots", current.commit, "psd-clinical", "evidence", )); expect(secondRendered.evidence.sources[0].root).not.toBe(firstRendered.evidence.sources[0].root); for (const lease of [first, second]) { await expect(runFile(thtBin, ["config", "check", "-c", lease.path], { cwd: harnessDir, env: { ...process.env, THT_HOME: join(f.root, "home") }, })).resolves.toBeDefined(); } } finally { first.release(); second.release(); } }); test("signed HTTP Evidence resolves its file binding and config check never captures its contents", async () => { const f = await fixture(evidenceWorkspace(` type: http uris: [https://evidence.example.test/guide.md] authentication: signed_urls_file connect_timeout_ms: 1250 read_timeout_ms: 30001 max_bytes: 12345 max_redirects: 2 allow_private_hosts: false max_cache_bytes: 67890 `)); const runner = runnerFor(f); const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath); try { const yaml = readFileSync(lease.path, "utf8"); expect(parse(yaml).evidence.sources).toEqual([{ type: "http", provenance_urls: ["https://evidence.example.test/guide.md"], signed_urls_file: realpathSync(join(f.secretRoot, "evidence-signed-urls.json")), connect_timeout: 1.25, read_timeout: 30.001, max_bytes: 12_345, max_redirects: 2, allow_private_hosts: false, max_cache_bytes: 67_890, }]); expect(yaml).not.toContain("SIGNED-HANDOFF-CANARY"); const checked = await runFile(thtBin, ["config", "check", "-c", lease.path], { cwd: harnessDir, env: { ...process.env, THT_HOME: join(f.root, "home") }, }); expect(`${checked.stdout}${checked.stderr}`).not.toContain("SIGNED-HANDOFF-CANARY"); } finally { lease.release(); } }); test("static S3 Evidence resolves only configured secret-root file paths", async () => { const f = await fixture(evidenceWorkspace(` type: s3 uri: s3://clinical-evidence/published/ endpoint_url: https://s3.example.test/ region: eu-west-1 credentials: static_files trusted_endpoint: true allow_private_endpoint: true allow_insecure_endpoint: false max_bytes: 222 max_objects: 33 max_pages: 4 page_size: 5 `, ` policy: max_chunk_chars: 2500 retain_published_generations: 7 `)); const runner = runnerFor(f); const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath); try { const yaml = readFileSync(lease.path, "utf8"); expect(parse(yaml).evidence.sources).toEqual([{ type: "s3", bucket: "clinical-evidence", prefix: "published/", endpoint_url: "https://s3.example.test/", region: "eu-west-1", access_key_file: realpathSync(join(f.secretRoot, "evidence-access")), secret_key_file: realpathSync(join(f.secretRoot, "evidence-secret")), session_token_file: realpathSync(join(f.secretRoot, "evidence-token")), trusted_endpoint: true, allow_private_endpoint: true, allow_insecure_endpoint: false, max_bytes: 222, max_objects: 33, max_pages: 4, page_size: 5, }]); expect(parse(yaml).vector).toEqual({ max_chunk_chars: 2_500, retain_published_generations: 7, }); for (const canary of ["ACCESS-HANDOFF-CANARY", "SECRET-HANDOFF-CANARY", "TOKEN-HANDOFF-CANARY"]) { expect(yaml).not.toContain(canary); } const checked = await runFile(thtBin, ["config", "check", "-c", lease.path], { cwd: harnessDir, env: { ...process.env, THT_HOME: join(f.root, "home") }, }); const output = `${checked.stdout}${checked.stderr}`; for (const canary of ["ACCESS-HANDOFF-CANARY", "SECRET-HANDOFF-CANARY", "TOKEN-HANDOFF-CANARY"]) { expect(output).not.toContain(canary); } } finally { lease.release(); } }); test("local GET sessions mine uses the real canonical handoff and returns an empty inventory", async () => { const f = await fixture(); const app = buildApp(loadConfig({ AUTH_MODE: "none", THT_HARNESS_DIR: harnessDir, THT_BIN: thtBin, THT_DATA_ROOT: f.dataRoot, THT_WORKSPACE_REGISTRY_ROOT: f.registryConfig.root, THT_WORKSPACE_GIT_REMOTE: f.registryConfig.remoteUrl, THT_WORKSPACE_SECRET_ROOTS: f.registryConfig.secretRoots.join(","), }), { thtRunner: runnerFor(f), workspaceRegistry: f.registry, mgr: { get: () => undefined } as any, }); try { const response = await app.inject({ method: "GET", url: "/sessions?scope=mine" }); expect(response.statusCode).toBe(200); expect(response.json()).toEqual([]); } finally { await app.close(); } });