diff --git a/backend/test/workspace-runtime-handoff.test.ts b/backend/test/workspace-runtime-handoff.test.ts index 94ef0684..517a5160 100644 --- a/backend/test/workspace-runtime-handoff.test.ts +++ b/backend/test/workspace-runtime-handoff.test.ts @@ -141,7 +141,7 @@ async function fixture(workspaceSource = filesystemWorkspace) { }; for (const [name, value] of Object.entries(environment)) vi.stubEnv(name, value); vi.stubEnv("THT_HOME", join(root, "home")); - return { root, dataRoot, secretRoot, registry, registryConfig, revision }; + return { root, source, dataRoot, secretRoot, registry, registryConfig, revision }; } function runnerFor(f: Awaited>): ThtRunner { @@ -234,6 +234,52 @@ test("separate runtime leases hand off byte-identical revision Evidence configs } }); +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, "workspace-content", "psd-clinical", "evidence", "guide.md"), + "# Content-only revision two\n", + ); + await git(f.source, ["add", "workspace-content/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, + "workspace-content", + "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] diff --git a/backend/test/workspace-runtime-renderer.test.ts b/backend/test/workspace-runtime-renderer.test.ts index 9f4f551a..9945d725 100644 --- a/backend/test/workspace-runtime-renderer.test.ts +++ b/backend/test/workspace-runtime-renderer.test.ts @@ -1,4 +1,7 @@ -import { expect, test } from "vitest"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; import { parse } from "yaml"; import { renderRuntimeConfig, @@ -300,6 +303,20 @@ test("renders REST bindings through the legacy rest sections without secret valu expect(yaml).not.toContain("\n api_key: "); }); +const evidenceSecretRoots: string[] = []; + +afterEach(() => { + evidenceSecretRoots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })); +}); + +function evidenceSecretFile(name: string, contents: string): string { + const root = mkdtempSync(join(tmpdir(), "tht-renderer-evidence-secret-")); + evidenceSecretRoots.push(root); + const path = join(root, name); + writeFileSync(path, contents, { mode: 0o600 }); + return path; +} + function evidenceWorkspace(source: Record, policy?: Record) { return parseWorkspaceYaml(`${canonicalEvidenceWorkspace}\nevidence:\n source: ${JSON.stringify(source)}${ policy === undefined ? "" : `\n policy: ${JSON.stringify(policy)}` @@ -410,7 +427,8 @@ test("renders public HTTP Evidence with exact fractional-second timeouts and eve }); test("renders signed HTTP Evidence as provenance plus a validated file path only", () => { - const signedFile = "/run/secrets/evidence-signed-urls.json"; + const canary = "SIGNED-URL-CANARY-CONTENT"; + const signedFile = evidenceSecretFile("evidence-signed-urls.json", canary); const yaml = evidenceRender({ type: "http", uris: [ @@ -437,7 +455,7 @@ test("renders signed HTTP Evidence as provenance plus a validated file path only allow_private_hosts: false, max_cache_bytes: 67_108_864, }]); - expect(yaml).not.toContain("SIGNED-URL-CANARY-CONTENT"); + expect(yaml).not.toContain(canary); }); test("renders ambient S3 Evidence without credential keys", () => { @@ -465,7 +483,13 @@ test("renders ambient S3 Evidence without credential keys", () => { }); test("renders static S3 Evidence with endpoint policy, limits, and file paths but no contents", () => { - const yaml = evidenceRender({ + const accessCanary = "ACCESS-CANARY-CONTENT"; + const secretCanary = "SECRET-CANARY-CONTENT"; + const tokenCanary = "TOKEN-CANARY-CONTENT"; + const accessFile = evidenceSecretFile("evidence-access", accessCanary); + const secretFile = evidenceSecretFile("evidence-secret", secretCanary); + const tokenFile = evidenceSecretFile("evidence-token", tokenCanary); + const source = { type: "s3", uri: "s3://clinical-evidence/published/", credentials: "static_files", @@ -478,14 +502,13 @@ test("renders static S3 Evidence with endpoint policy, limits, and file paths bu max_objects: 33, max_pages: 4, page_size: 5, - }, { - missing: [], - values: { - THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE: "/run/secrets/evidence-access", - THT_WS_PSD_CLINICAL_EVIDENCE_SECRET_KEY_FILE: "/run/secrets/evidence-secret", - THT_WS_PSD_CLINICAL_EVIDENCE_SESSION_TOKEN_FILE: "/run/secrets/evidence-token", - }, - }); + }; + const values = { + THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE: accessFile, + THT_WS_PSD_CLINICAL_EVIDENCE_SECRET_KEY_FILE: secretFile, + THT_WS_PSD_CLINICAL_EVIDENCE_SESSION_TOKEN_FILE: tokenFile, + }; + const yaml = evidenceRender(source, { missing: [], values }); expect(parse(yaml).evidence.sources).toEqual([{ type: "s3", @@ -493,9 +516,9 @@ test("renders static S3 Evidence with endpoint policy, limits, and file paths bu prefix: "published/", endpoint_url: "http://minio.internal:9000/", region: "eu-central-1", - access_key_file: "/run/secrets/evidence-access", - secret_key_file: "/run/secrets/evidence-secret", - session_token_file: "/run/secrets/evidence-token", + access_key_file: accessFile, + secret_key_file: secretFile, + session_token_file: tokenFile, trusted_endpoint: true, allow_private_endpoint: true, allow_insecure_endpoint: true, @@ -504,9 +527,19 @@ test("renders static S3 Evidence with endpoint policy, limits, and file paths bu max_pages: 4, page_size: 5, }]); - expect(yaml).not.toContain("ACCESS-CANARY-CONTENT"); - expect(yaml).not.toContain("SECRET-CANARY-CONTENT"); - expect(yaml).not.toContain("TOKEN-CANARY-CONTENT"); + expect(yaml).not.toContain(accessCanary); + expect(yaml).not.toContain(secretCanary); + expect(yaml).not.toContain(tokenCanary); + + const withoutToken = parse(evidenceRender(source, { + missing: [], + values: { + THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE: accessFile, + THT_WS_PSD_CLINICAL_EVIDENCE_SECRET_KEY_FILE: secretFile, + }, + })).evidence.sources[0]; + expect(withoutToken).toMatchObject({ access_key_file: accessFile, secret_key_file: secretFile }); + expect(withoutToken).not.toHaveProperty("session_token_file"); }); test("omits Evidence configuration and policy when the descriptor has no Evidence", () => {