diff --git a/backend/src/workspaces/contracts.ts b/backend/src/workspaces/contracts.ts index 8b25998c..eec16656 100644 --- a/backend/src/workspaces/contracts.ts +++ b/backend/src/workspaces/contracts.ts @@ -230,14 +230,20 @@ function evidenceDocumentation( ]; } const evidenceVariables = variables.filter(({ role }) => role === "EVIDENCE"); + const requiredVariables = evidenceVariables.filter(({ suffix }) => suffix !== "SESSION_TOKEN_FILE"); + const optionalVariables = evidenceVariables.filter(({ suffix }) => suffix === "SESSION_TOKEN_FILE"); return [ ...common, ...details, `- Maximum chunk characters: \`${policy.max_chunk_chars}\``, `- Retained published generations: \`${policy.retain_published_generations}\``, - ...(evidenceVariables.length === 0 ? [] : [ + ...(requiredVariables.length === 0 ? [] : [ "- Required installation file variables:", - ...evidenceVariables.map(({ name }) => ` - \`${name}\``), + ...requiredVariables.map(({ name }) => ` - \`${name}\``), + ]), + ...(optionalVariables.length === 0 ? [] : [ + "- Optional installation file variables:", + ...optionalVariables.map(({ name }) => ` - \`${name}\``), ]), "", ]; diff --git a/backend/test/routes-workspaces.test.ts b/backend/test/routes-workspaces.test.ts index 41938a9b..f5849d45 100644 --- a/backend/test/routes-workspaces.test.ts +++ b/backend/test/routes-workspaces.test.ts @@ -615,6 +615,24 @@ test("real publish create/update, pull, list, and read preserve a complete Evide baseCommit: createdRevision.commit, baseBlob: createdRevision.blob, }, }); + expect(update.statusCode).toBe(200); + await realGit(fixture.author, ["pull", "--ff-only", "origin", "main"]); + const remotelyEdited = validateCanonicalWorkspace({ + ...updated, + evidence: { + ...updated.evidence, + policy: { max_chunk_chars: 9_001, retain_published_generations: 9 }, + }, + }); + writeFileSync( + join(fixture.author, "workspaces", "research-clinical.yaml"), + serializeWorkspaceYaml(remotelyEdited), + ); + await realGit(fixture.author, ["add", "workspaces/research-clinical.yaml"]); + await realGit(fixture.author, ["commit", "-m", "Remote Evidence-only descriptor edit"]); + await realGit(fixture.author, ["push", "origin", "main"]); + const remoteCommit = await realGit(fixture.author, ["rev-parse", "HEAD"]); + const pull = await fixture.app.inject({ method: "POST", url: "/workspace-registry/pull" }); const list = await fixture.app.inject({ method: "GET", url: "/workspaces" }); const read = await fixture.app.inject({ method: "GET", url: "/workspaces/research-clinical" }); @@ -623,10 +641,12 @@ test("real publish create/update, pull, list, and read preserve a complete Evide expect(create.statusCode).toBe(200); expect(update.statusCode).toBe(200); expect(pull.statusCode).toBe(200); + expect(pull.json().head).toBe(remoteCommit); expect(list.statusCode).toBe(200); - expect(list.json().find(({ id }: { id: string }) => id === "research-clinical").workspace).toEqual(updated); + expect(list.json().find(({ id }: { id: string }) => id === "research-clinical").workspace) + .toEqual(remotelyEdited); expect(read.statusCode).toBe(200); - expect(read.json().workspace).toEqual(updated); + expect(read.json().workspace).toEqual(remotelyEdited); }); test("real route reports a safe field for an Evidence-only concurrent edit", async () => { @@ -697,11 +717,18 @@ test.each([ label: "inline S3 credential field", source: { type: "s3", uri: "s3://clinical-evidence/published/", access_key: SECRET_CANARY }, }, -])("real validate rejects $label without echoing it", async ({ source }) => { +])("real publish rejects $label without echoing it or changing HEAD", async ({ source }) => { const fixture = await createRealRouteFixture(); + await fixture.registry.bootstrap(); + const base = await fixture.registry.read("psd-clinical"); + const invalid = structuredClone(base.workspace) as any; + invalid.evidence = { source }; const response = await fixture.app.inject({ - method: "POST", url: "/workspaces/validate", - payload: { workspace: { ...workspace, evidence: { source } } }, + method: "POST", url: "/workspaces/publish", + payload: { + action: "update", workspace: invalid, + baseCommit: base.revision.commit, baseBlob: base.revision.blob, + }, }); expect(response.statusCode).toBe(400); diff --git a/backend/test/workspace-registry.test.ts b/backend/test/workspace-registry.test.ts index f0f24136..6eb096b2 100644 --- a/backend/test/workspace-registry.test.ts +++ b/backend/test/workspace-registry.test.ts @@ -1013,13 +1013,17 @@ test("snapshots canonical Evidence artifacts at the active commit without copyin const committedDescriptor = parseWorkspaceYaml(await gitOutput(remote.source, [ "show", `${remote.initialCommit}:workspaces/psd-clinical.yaml`, ])) as CanonicalWorkspace; + const committedBlob = await gitOutput(remote.source, [ + "rev-parse", `${remote.initialCommit}:workspaces/psd-clinical.yaml`, + ]); + expect(active.revision.blob).toBe(committedBlob); expect(expectedFiles["psd-clinical.yaml"]).toBe(serializeWorkspaceYaml(committedDescriptor)); expect(readdirSync(snapshotDirectory).sort()).toEqual([ "psd-clinical.env.example", "psd-clinical.md", "psd-clinical.yaml", "snapshot.json", ]); expect(manifest.head).toBe(remote.initialCommit); expect(manifest.revisions[0]).toMatchObject({ - id: "psd-clinical", commit: remote.initialCommit, blob: active.revision.blob, + id: "psd-clinical", commit: remote.initialCommit, blob: committedBlob, }); expect(Object.keys(manifest.files).sort()).toEqual(Object.keys(expectedFiles).sort()); for (const [name, contents] of Object.entries(expectedFiles)) { diff --git a/backend/test/workspaces-contracts.test.ts b/backend/test/workspaces-contracts.test.ts index 2ac47f83..7cb11a07 100644 --- a/backend/test/workspaces-contracts.test.ts +++ b/backend/test/workspaces-contracts.test.ts @@ -444,6 +444,25 @@ test.each(evidenceSources)("renders deterministic public Evidence docs for $labe } }); +test("documents the S3 session token file as optional", () => { + const descriptor = parseWorkspaceYaml( + `${renderWorkspaceWithoutEvidence()}evidence: + source: { type: s3, uri: s3://clinical-evidence/published/, credentials: static_files } +`, + ); + const markdown = renderWorkspaceDocs(descriptor).markdown; + const required = markdown.slice( + markdown.indexOf("- Required installation file variables:"), + markdown.indexOf("- Optional installation file variables:"), + ); + const optional = markdown.slice(markdown.indexOf("- Optional installation file variables:")); + + expect(required).toContain("EVIDENCE_ACCESS_KEY_FILE"); + expect(required).toContain("EVIDENCE_SECRET_KEY_FILE"); + expect(required).not.toContain("EVIDENCE_SESSION_TOKEN_FILE"); + expect(optional).toContain("EVIDENCE_SESSION_TOKEN_FILE"); +}); + test("documents same-revision filesystem ownership without claiming P1 materialization", () => { const descriptor = parseWorkspaceYaml( `${renderWorkspaceWithoutEvidence()}evidence:\n source: { type: filesystem, uri: workspace-content/psd-clinical/evidence }\n`,