feat: preserve evidence in workspace artifacts
This commit is contained in:
@@ -56,6 +56,8 @@ const bundleManifest = z.object({
|
|||||||
}).strict(),
|
}).strict(),
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
|
// Public P1 bundles contain only the descriptor and derived docs. Evidence file bytes remain
|
||||||
|
// revision-owned Git content for the later P6 materialization boundary.
|
||||||
const BUNDLE_FILES = ["manifest.json", "workspace.yaml", "contract.env.example", "README.md"] as const;
|
const BUNDLE_FILES = ["manifest.json", "workspace.yaml", "contract.env.example", "README.md"] as const;
|
||||||
type BundleFile = (typeof BUNDLE_FILES)[number];
|
type BundleFile = (typeof BUNDLE_FILES)[number];
|
||||||
|
|
||||||
@@ -298,6 +300,7 @@ export function workspaceRoutes(app: FastifyInstance, deps: WorkspaceRoutesDeps)
|
|||||||
displayName: workspace.workspace.name,
|
displayName: workspace.workspace.name,
|
||||||
description: workspace.workspace.description,
|
description: workspace.workspace.description,
|
||||||
language: workspace.workspace.language,
|
language: workspace.workspace.language,
|
||||||
|
workspace,
|
||||||
revision,
|
revision,
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -177,6 +177,72 @@ function localizedIntroduction(workspace: WorkspaceDescriptor): string {
|
|||||||
: `Installation setup for ${workspace.workspace.name}. Configure only the bindings supported by this installation.`;
|
: `Installation setup for ${workspace.workspace.name}. Configure only the bindings supported by this installation.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function evidenceDocumentation(
|
||||||
|
workspace: WorkspaceDescriptor,
|
||||||
|
variables: readonly InstallationVariable[],
|
||||||
|
): string[] {
|
||||||
|
if (!("evidence" in workspace) || workspace.evidence === undefined) return [];
|
||||||
|
const { source, policy } = workspace.evidence;
|
||||||
|
const common = [
|
||||||
|
"## Evidence source",
|
||||||
|
"",
|
||||||
|
`- Type: \`${source.type}\``,
|
||||||
|
];
|
||||||
|
let details: string[];
|
||||||
|
if (source.type === "filesystem") {
|
||||||
|
details = [
|
||||||
|
`- URI: \`${source.uri}\``,
|
||||||
|
`- Patterns: ${source.patterns.map((pattern) => `\`${pattern}\``).join(", ")}`,
|
||||||
|
`- Maximum source bytes: \`${source.max_bytes}\``,
|
||||||
|
"- Ownership: the descriptor and its Evidence tree are owned by the same Git revision.",
|
||||||
|
"- Materialization: P6 materializes that revision-pinned tree and verifies real containment, including symlink safety.",
|
||||||
|
"- Export boundary: the browser/API ZIP does not include Evidence file bytes.",
|
||||||
|
];
|
||||||
|
} else if (source.type === "http") {
|
||||||
|
details = [
|
||||||
|
"- URIs:",
|
||||||
|
...source.uris.map((uri) => ` - \`${uri}\``),
|
||||||
|
`- Authentication: \`${source.authentication}\`. ${source.authentication === "none"
|
||||||
|
? "No credential file is required."
|
||||||
|
: "Provide the signed URL file through the installation file variable listed below."}`,
|
||||||
|
`- Connect timeout (ms): \`${source.connect_timeout_ms}\``,
|
||||||
|
`- Read timeout (ms): \`${source.read_timeout_ms}\``,
|
||||||
|
`- Maximum source bytes: \`${source.max_bytes}\``,
|
||||||
|
`- Maximum redirects: \`${source.max_redirects}\``,
|
||||||
|
`- Private hosts allowed: \`${source.allow_private_hosts}\``,
|
||||||
|
`- Maximum cache bytes: \`${source.max_cache_bytes}\``,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
details = [
|
||||||
|
`- URI: \`${source.uri}\``,
|
||||||
|
...(source.endpoint_url === undefined ? [] : [`- Endpoint URL: \`${source.endpoint_url}\``]),
|
||||||
|
...(source.region === undefined ? [] : [`- Region: \`${source.region}\``]),
|
||||||
|
`- Credentials: \`${source.credentials}\`. ${source.credentials === "ambient"
|
||||||
|
? "Use ambient credentials; no Evidence credential file is required."
|
||||||
|
: "Provide credentials through the installation file variables listed below."}`,
|
||||||
|
`- Trusted endpoint: \`${source.trusted_endpoint}\``,
|
||||||
|
`- Private endpoint allowed: \`${source.allow_private_endpoint}\``,
|
||||||
|
`- Insecure endpoint allowed: \`${source.allow_insecure_endpoint}\``,
|
||||||
|
`- Maximum source bytes: \`${source.max_bytes}\``,
|
||||||
|
`- Maximum objects: \`${source.max_objects}\``,
|
||||||
|
`- Maximum pages: \`${source.max_pages}\``,
|
||||||
|
`- Page size: \`${source.page_size}\``,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const evidenceVariables = variables.filter(({ role }) => role === "EVIDENCE");
|
||||||
|
return [
|
||||||
|
...common,
|
||||||
|
...details,
|
||||||
|
`- Maximum chunk characters: \`${policy.max_chunk_chars}\``,
|
||||||
|
`- Retained published generations: \`${policy.retain_published_generations}\``,
|
||||||
|
...(evidenceVariables.length === 0 ? [] : [
|
||||||
|
"- Required installation file variables:",
|
||||||
|
...evidenceVariables.map(({ name }) => ` - \`${name}\``),
|
||||||
|
]),
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export function renderWorkspaceDocs(workspace: WorkspaceDescriptor): { envExample: string; markdown: string } {
|
export function renderWorkspaceDocs(workspace: WorkspaceDescriptor): { envExample: string; markdown: string } {
|
||||||
const descriptor = validateWorkspaceDescriptor(workspace);
|
const descriptor = validateWorkspaceDescriptor(workspace);
|
||||||
const contract = buildInstallationContract(descriptor);
|
const contract = buildInstallationContract(descriptor);
|
||||||
@@ -213,6 +279,7 @@ export function renderWorkspaceDocs(workspace: WorkspaceDescriptor): { envExampl
|
|||||||
)),
|
)),
|
||||||
"",
|
"",
|
||||||
]),
|
]),
|
||||||
|
...evidenceDocumentation(descriptor, contract.variables),
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
return { envExample, markdown };
|
return { envExample, markdown };
|
||||||
|
|||||||
@@ -790,6 +790,8 @@ export class WorkspaceRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private expectedSnapshotFiles(state: ActiveState): string[] {
|
private expectedSnapshotFiles(state: ActiveState): string[] {
|
||||||
|
// P1 snapshots only descriptors and derived public docs. P6 owns revision-pinned
|
||||||
|
// workspace-content materialization and its recursive containment checks.
|
||||||
return state.revisions.flatMap((revision) => revision.state === "operational"
|
return state.revisions.flatMap((revision) => revision.state === "operational"
|
||||||
? [`${revision.id}.yaml`, `${revision.id}.env.example`, `${revision.id}.md`]
|
? [`${revision.id}.yaml`, `${revision.id}.env.example`, `${revision.id}.md`]
|
||||||
: [`${revision.id}.yaml`]);
|
: [`${revision.id}.yaml`]);
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
|
import { execFile } from "node:child_process";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { once } from "node:events";
|
import { once } from "node:events";
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
import { expect, test, vi } from "vitest";
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, 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 yauzl from "yauzl";
|
||||||
import yazl from "yazl";
|
import yazl from "yazl";
|
||||||
import { buildApp } from "../src/app.js";
|
import { buildApp } from "../src/app.js";
|
||||||
import { loadConfig } from "../src/config.js";
|
import { loadConfig } from "../src/config.js";
|
||||||
import { createProductionWorkspaceDiagnoser } from "../src/workspaces/diagnostics.js";
|
import { createProductionWorkspaceDiagnoser } from "../src/workspaces/diagnostics.js";
|
||||||
import { WorkspaceRegistryError } from "../src/workspaces/git-repository.js";
|
import { WorkspaceRegistryError } from "../src/workspaces/git-repository.js";
|
||||||
import type { WorkspaceRegistry, WorkspaceRevision } from "../src/workspaces/registry.js";
|
import { WorkspaceRegistry, type WorkspaceRevision } from "../src/workspaces/registry.js";
|
||||||
import { renderWorkspaceDocs, serializeWorkspaceYaml, type CanonicalWorkspace, type WorkspaceV2 } from "../src/workspaces/schema.js";
|
import {
|
||||||
|
parseWorkspaceYaml, renderWorkspaceDocs, serializeWorkspaceYaml, validateCanonicalWorkspace,
|
||||||
|
type CanonicalWorkspace, type WorkspaceV2,
|
||||||
|
} from "../src/workspaces/schema.js";
|
||||||
|
|
||||||
const workspace: CanonicalWorkspace = {
|
const workspace: CanonicalWorkspace = {
|
||||||
workspace: {
|
workspace: {
|
||||||
@@ -131,7 +140,7 @@ function appFor(registry: RegistryFake, diagnose = vi.fn(async () => ({ activata
|
|||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sha256(value: string): string {
|
function sha256(value: string | Buffer): string {
|
||||||
return createHash("sha256").update(value).digest("hex");
|
return createHash("sha256").update(value).digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,3 +428,362 @@ test("imports an exact generated bundle only as a browser draft", async () => {
|
|||||||
expect(res.json()).toMatchObject({ draft: { workspace } });
|
expect(res.json()).toMatchObject({ draft: { workspace } });
|
||||||
expect(registry.publish).not.toHaveBeenCalled();
|
expect(registry.publish).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const runFile = promisify(execFile);
|
||||||
|
const realRouteRoots: string[] = [];
|
||||||
|
|
||||||
|
interface RealRouteFixture {
|
||||||
|
root: string;
|
||||||
|
remote: string;
|
||||||
|
author: string;
|
||||||
|
registryRoot: string;
|
||||||
|
initialCommit: string;
|
||||||
|
app: ReturnType<typeof buildApp>;
|
||||||
|
registry: WorkspaceRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EVIDENCE_FILE_BYTES = "PUBLIC-EVIDENCE-FILE-BYTES-NOT-FOR-ZIP\n";
|
||||||
|
const SECRET_CANARY = "CANARY-EVIDENCE-ROUTE-SECRET-DO-NOT-LEAK";
|
||||||
|
|
||||||
|
function withEvidence(
|
||||||
|
source: Partial<CanonicalWorkspace["evidence"]["source"]> & { type: "filesystem" | "http" | "s3" },
|
||||||
|
changes: Partial<CanonicalWorkspace["evidence"]["policy"]> = {},
|
||||||
|
): CanonicalWorkspace {
|
||||||
|
return validateCanonicalWorkspace({
|
||||||
|
...workspace,
|
||||||
|
evidence: { source, policy: changes },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesystemEvidenceWorkspace = withEvidence({
|
||||||
|
type: "filesystem", uri: "workspace-content/psd-clinical/evidence",
|
||||||
|
});
|
||||||
|
const httpEvidenceWorkspace = withEvidence({
|
||||||
|
type: "http",
|
||||||
|
uris: ["https://evidence.example.test/guide.md"],
|
||||||
|
authentication: "signed_urls_file",
|
||||||
|
});
|
||||||
|
|
||||||
|
async function realGit(cwd: string, args: string[]): Promise<string> {
|
||||||
|
return (await runFile("git", args, { cwd })).stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRealRouteFixture(
|
||||||
|
initialWorkspace: CanonicalWorkspace = filesystemEvidenceWorkspace,
|
||||||
|
): Promise<RealRouteFixture> {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "thoth-real-workspace-route-"));
|
||||||
|
realRouteRoots.push(root);
|
||||||
|
const remote = join(root, "remote.git");
|
||||||
|
const author = join(root, "author");
|
||||||
|
const registryRoot = join(root, "registry");
|
||||||
|
await realGit(root, ["init", "--bare", "--initial-branch=main", remote]);
|
||||||
|
mkdirSync(author);
|
||||||
|
await realGit(author, ["init", "--initial-branch=main"]);
|
||||||
|
await realGit(author, ["config", "user.name", "Workspace Route Test"]);
|
||||||
|
await realGit(author, ["config", "user.email", "workspace-route@example.invalid"]);
|
||||||
|
mkdirSync(join(author, "workspaces"));
|
||||||
|
writeFileSync(join(author, "workspaces", "psd-clinical.yaml"), serializeWorkspaceYaml(initialWorkspace));
|
||||||
|
if (initialWorkspace.evidence?.source.type === "filesystem") {
|
||||||
|
mkdirSync(join(author, "workspace-content", "psd-clinical", "evidence"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(author, "workspace-content", "psd-clinical", "evidence", "guide.md"),
|
||||||
|
EVIDENCE_FILE_BYTES,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await realGit(author, ["add", "."]);
|
||||||
|
await realGit(author, ["commit", "-m", "Initial Evidence workspace"]);
|
||||||
|
await realGit(author, ["remote", "add", "origin", remote]);
|
||||||
|
await realGit(author, ["push", "origin", "main"]);
|
||||||
|
const initialCommit = await realGit(author, ["rev-parse", "HEAD"]);
|
||||||
|
const config = loadConfig({
|
||||||
|
THT_HARNESS_DIR: "/missing-harness",
|
||||||
|
THT_WORKSPACE_REGISTRY_ROOT: registryRoot,
|
||||||
|
THT_WORKSPACE_GIT_REMOTE: remote,
|
||||||
|
THT_WORKSPACE_GIT_AUTHOR_NAME: "Workspace Route Publisher",
|
||||||
|
THT_WORKSPACE_GIT_AUTHOR_EMAIL: "workspace-route-publisher@example.invalid",
|
||||||
|
});
|
||||||
|
const registry = new WorkspaceRegistry(config.workspaceRegistry);
|
||||||
|
const app = buildApp(config, {
|
||||||
|
thtRunner: {} as any,
|
||||||
|
workspaceRegistry: registry,
|
||||||
|
workspaceDiagnoser: vi.fn(async () => ({ activatable: true, diagnostics: [] })),
|
||||||
|
});
|
||||||
|
return { root, remote, author, registryRoot, initialCommit, app, registry };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractZip(source: Buffer): Promise<Record<string, Buffer>> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
yauzl.fromBuffer(source, { lazyEntries: true, strictFileNames: true }, (error, archive) => {
|
||||||
|
if (error || !archive) return reject(error ?? new Error("archive unavailable"));
|
||||||
|
const files: Record<string, Buffer> = {};
|
||||||
|
archive.on("error", reject);
|
||||||
|
archive.on("entry", (entry) => {
|
||||||
|
if (entry.fileName.startsWith("/") || entry.fileName.includes("..") || entry.fileName.includes("\\")) {
|
||||||
|
archive.close();
|
||||||
|
reject(new Error("unsafe exported path"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
archive.openReadStream(entry, (streamError, stream) => {
|
||||||
|
if (streamError || !stream) return reject(streamError ?? new Error("entry unavailable"));
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
stream.on("error", reject);
|
||||||
|
stream.on("end", () => {
|
||||||
|
files[entry.fileName] = Buffer.concat(chunks);
|
||||||
|
archive.readEntry();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
archive.on("end", () => resolve(files));
|
||||||
|
archive.readEntry();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
realRouteRoots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
source: { type: "filesystem", uri: "workspace-content/psd-clinical/evidence" },
|
||||||
|
expectedVariables: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: {
|
||||||
|
type: "http", uris: ["https://evidence.example.test/guide.md"],
|
||||||
|
authentication: "signed_urls_file",
|
||||||
|
},
|
||||||
|
expectedVariables: ["THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: { type: "s3", uri: "s3://clinical-evidence/published/", credentials: "static_files" },
|
||||||
|
expectedVariables: [
|
||||||
|
"THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE",
|
||||||
|
"THT_WS_PSD_CLINICAL_EVIDENCE_SECRET_KEY_FILE",
|
||||||
|
"THT_WS_PSD_CLINICAL_EVIDENCE_SESSION_TOKEN_FILE",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])("real validate route canonicalizes $source.type Evidence and returns only its file contract", async ({
|
||||||
|
source, expectedVariables,
|
||||||
|
}) => {
|
||||||
|
const fixture = await createRealRouteFixture();
|
||||||
|
const response = await fixture.app.inject({
|
||||||
|
method: "POST", url: "/workspaces/validate",
|
||||||
|
payload: { workspace: { ...workspace, evidence: { source } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
const body = response.json();
|
||||||
|
expect(body.workspace.evidence.policy).toEqual({
|
||||||
|
max_chunk_chars: 4_000, retain_published_generations: 3,
|
||||||
|
});
|
||||||
|
expect(body.workspace.evidence.source.max_bytes).toBe(10 * 1024 * 1024);
|
||||||
|
expect(body.contract.variables.filter(({ role }: { role: string }) => role === "EVIDENCE")
|
||||||
|
.map(({ name }: { name: string }) => name)).toEqual(expectedVariables);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("real publish create/update, pull, list, and read preserve a complete Evidence descriptor", async () => {
|
||||||
|
const fixture = await createRealRouteFixture(httpEvidenceWorkspace);
|
||||||
|
const status = await fixture.app.inject({ method: "GET", url: "/workspace-registry/status" });
|
||||||
|
const created = validateCanonicalWorkspace({
|
||||||
|
...httpEvidenceWorkspace,
|
||||||
|
workspace: { ...httpEvidenceWorkspace.workspace, id: "research-clinical", name: "Research Clinical" },
|
||||||
|
semantic_index: {
|
||||||
|
...httpEvidenceWorkspace.semantic_index,
|
||||||
|
vector_store: { ...httpEvidenceWorkspace.semantic_index.vector_store, collection: "research-clinical" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const create = await fixture.app.inject({
|
||||||
|
method: "POST", url: "/workspaces/publish",
|
||||||
|
payload: { action: "create", workspace: created, baseCommit: status.json().head },
|
||||||
|
});
|
||||||
|
const createdRevision = create.json().revision as WorkspaceRevision;
|
||||||
|
const updated = validateCanonicalWorkspace({
|
||||||
|
...created,
|
||||||
|
evidence: {
|
||||||
|
...created.evidence,
|
||||||
|
policy: { max_chunk_chars: 8_192, retain_published_generations: 7 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const update = await fixture.app.inject({
|
||||||
|
method: "POST", url: "/workspaces/publish",
|
||||||
|
payload: {
|
||||||
|
action: "update", workspace: updated,
|
||||||
|
baseCommit: createdRevision.commit, baseBlob: createdRevision.blob,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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" });
|
||||||
|
|
||||||
|
expect(status.statusCode).toBe(200);
|
||||||
|
expect(create.statusCode).toBe(200);
|
||||||
|
expect(update.statusCode).toBe(200);
|
||||||
|
expect(pull.statusCode).toBe(200);
|
||||||
|
expect(list.statusCode).toBe(200);
|
||||||
|
expect(list.json().find(({ id }: { id: string }) => id === "research-clinical").workspace).toEqual(updated);
|
||||||
|
expect(read.statusCode).toBe(200);
|
||||||
|
expect(read.json().workspace).toEqual(updated);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("real route reports a safe field for an Evidence-only concurrent edit", async () => {
|
||||||
|
const fixture = await createRealRouteFixture(httpEvidenceWorkspace);
|
||||||
|
await fixture.registry.bootstrap();
|
||||||
|
const base = await fixture.registry.read("psd-clinical");
|
||||||
|
const remote = withEvidence(
|
||||||
|
{ ...httpEvidenceWorkspace.evidence!.source },
|
||||||
|
{ max_chunk_chars: 9_000, retain_published_generations: 3 },
|
||||||
|
);
|
||||||
|
writeFileSync(join(fixture.author, "workspaces", "psd-clinical.yaml"), serializeWorkspaceYaml(remote));
|
||||||
|
await realGit(fixture.author, ["add", "workspaces/psd-clinical.yaml"]);
|
||||||
|
await realGit(fixture.author, ["commit", "-m", "Change Evidence policy only"]);
|
||||||
|
await realGit(fixture.author, ["push", "origin", "main"]);
|
||||||
|
const local = withEvidence(
|
||||||
|
{ ...httpEvidenceWorkspace.evidence!.source },
|
||||||
|
{ max_chunk_chars: 4_000, retain_published_generations: 8 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await fixture.app.inject({
|
||||||
|
method: "POST", url: "/workspaces/publish",
|
||||||
|
payload: {
|
||||||
|
action: "update", workspace: local,
|
||||||
|
baseCommit: base.revision.commit, baseBlob: base.revision.blob,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(409);
|
||||||
|
expect(response.json()).toMatchObject({
|
||||||
|
code: "workspace_conflict", fields: ["evidence.policy.max_chunk_chars"],
|
||||||
|
});
|
||||||
|
expect(response.body).not.toContain(SECRET_CANARY);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["absolute", "/tmp/CANARY-EVIDENCE-ROUTE-SECRET-DO-NOT-LEAK"],
|
||||||
|
["traversal", "workspace-content/psd-clinical/../CANARY-EVIDENCE-ROUTE-SECRET-DO-NOT-LEAK"],
|
||||||
|
["cross-workspace", "workspace-content/research/evidence"],
|
||||||
|
])("real publish rejects %s filesystem Evidence paths without changing HEAD", async (_label, uri) => {
|
||||||
|
const fixture = await createRealRouteFixture();
|
||||||
|
await fixture.registry.bootstrap();
|
||||||
|
const base = await fixture.registry.read("psd-clinical");
|
||||||
|
const invalid = structuredClone(filesystemEvidenceWorkspace) as any;
|
||||||
|
invalid.evidence.source.uri = uri;
|
||||||
|
|
||||||
|
const response = await fixture.app.inject({
|
||||||
|
method: "POST", url: "/workspaces/publish",
|
||||||
|
payload: { action: "update", workspace: invalid, baseCommit: base.revision.commit, baseBlob: base.revision.blob },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.json()).toEqual({ code: "workspace_invalid", message: "Workspace request or bundle is invalid." });
|
||||||
|
expect(response.body).not.toContain(SECRET_CANARY);
|
||||||
|
expect(await realGit(fixture.author, ["--git-dir", fixture.remote, "rev-parse", "HEAD"]))
|
||||||
|
.toBe(fixture.initialCommit);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
label: "credential-bearing HTTP URI",
|
||||||
|
source: { type: "http", uris: [`https://user:${SECRET_CANARY}@evidence.example.test/guide.md`] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "unsupported HTTP protocol",
|
||||||
|
source: { type: "http", uris: [`ftp://evidence.example.test/${SECRET_CANARY}`] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 }) => {
|
||||||
|
const fixture = await createRealRouteFixture();
|
||||||
|
const response = await fixture.app.inject({
|
||||||
|
method: "POST", url: "/workspaces/validate",
|
||||||
|
payload: { workspace: { ...workspace, evidence: { source } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.json()).toEqual({ code: "workspace_invalid", message: "Workspace request or bundle is invalid." });
|
||||||
|
expect(response.body).not.toContain(SECRET_CANARY);
|
||||||
|
expect(await realGit(fixture.author, ["--git-dir", fixture.remote, "rev-parse", "HEAD"]))
|
||||||
|
.toBe(fixture.initialCommit);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("real publish and pull fail safely when the contextual Evidence Git tree is missing", async () => {
|
||||||
|
const fixture = await createRealRouteFixture();
|
||||||
|
await fixture.registry.bootstrap();
|
||||||
|
const current = await fixture.registry.read("psd-clinical");
|
||||||
|
const missing = validateCanonicalWorkspace({
|
||||||
|
...workspace,
|
||||||
|
workspace: { ...workspace.workspace, id: "missing-evidence", name: "Missing Evidence" },
|
||||||
|
semantic_index: {
|
||||||
|
...workspace.semantic_index,
|
||||||
|
vector_store: { ...workspace.semantic_index.vector_store, collection: "missing-evidence" },
|
||||||
|
},
|
||||||
|
evidence: { source: { type: "filesystem", uri: "workspace-content/missing-evidence/evidence" } },
|
||||||
|
});
|
||||||
|
const publish = await fixture.app.inject({
|
||||||
|
method: "POST", url: "/workspaces/publish",
|
||||||
|
payload: { action: "create", workspace: missing, baseCommit: current.revision.commit },
|
||||||
|
});
|
||||||
|
expect(publish.statusCode).toBe(400);
|
||||||
|
expect(publish.json()).toEqual({ code: "workspace_invalid", message: "Workspace request or bundle is invalid." });
|
||||||
|
expect(await realGit(fixture.author, ["--git-dir", fixture.remote, "rev-parse", "HEAD"]))
|
||||||
|
.toBe(fixture.initialCommit);
|
||||||
|
|
||||||
|
rmSync(join(fixture.author, "workspace-content", "psd-clinical", "evidence"), { recursive: true });
|
||||||
|
await realGit(fixture.author, ["add", "-A"]);
|
||||||
|
await realGit(fixture.author, ["commit", "-m", "Remove Evidence tree"]);
|
||||||
|
await realGit(fixture.author, ["push", "origin", "main"]);
|
||||||
|
const pull = await fixture.app.inject({ method: "POST", url: "/workspace-registry/pull" });
|
||||||
|
expect(pull.statusCode).toBe(400);
|
||||||
|
expect(pull.json()).toEqual({ code: "workspace_invalid", message: "Workspace request or bundle is invalid." });
|
||||||
|
expect(pull.body).not.toContain(SECRET_CANARY);
|
||||||
|
await expect(fixture.registry.read("psd-clinical")).resolves.toMatchObject({
|
||||||
|
revision: { commit: fixture.initialCommit },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("real export and import preserve stable public Evidence artifacts without Evidence or secret bytes", async () => {
|
||||||
|
const fixture = await createRealRouteFixture();
|
||||||
|
const secretDirectory = join(fixture.root, "fixture-secrets");
|
||||||
|
mkdirSync(secretDirectory);
|
||||||
|
writeFileSync(join(secretDirectory, "credential"), SECRET_CANARY);
|
||||||
|
await fixture.registry.bootstrap();
|
||||||
|
|
||||||
|
const firstResponse = await fixture.app.inject({ method: "GET", url: "/workspaces/psd-clinical/export" });
|
||||||
|
const secondResponse = await fixture.app.inject({ method: "GET", url: "/workspaces/psd-clinical/export" });
|
||||||
|
expect(firstResponse.statusCode).toBe(200);
|
||||||
|
expect(secondResponse.statusCode).toBe(200);
|
||||||
|
const first = await extractZip(firstResponse.rawPayload);
|
||||||
|
const second = await extractZip(secondResponse.rawPayload);
|
||||||
|
const names = ["manifest.json", "workspace.yaml", "contract.env.example", "README.md"];
|
||||||
|
expect(Object.keys(first).sort()).toEqual([...names].sort());
|
||||||
|
expect(Object.keys(second).sort()).toEqual([...names].sort());
|
||||||
|
for (const name of names) expect(second[name]).toEqual(first[name]);
|
||||||
|
|
||||||
|
const descriptor = parseWorkspaceYaml(first["workspace.yaml"].toString("utf8"));
|
||||||
|
const docs = renderWorkspaceDocs(descriptor);
|
||||||
|
const manifest = JSON.parse(first["manifest.json"].toString("utf8"));
|
||||||
|
expect(descriptor).toEqual(filesystemEvidenceWorkspace);
|
||||||
|
expect(first["contract.env.example"].toString("utf8")).toBe(docs.envExample);
|
||||||
|
expect(first["README.md"].toString("utf8")).toBe(docs.markdown);
|
||||||
|
expect(manifest.files).toEqual({
|
||||||
|
"workspace.yaml": sha256(first["workspace.yaml"]),
|
||||||
|
"contract.env.example": sha256(first["contract.env.example"]),
|
||||||
|
"README.md": sha256(first["README.md"]),
|
||||||
|
});
|
||||||
|
const publicBytes = Buffer.concat(Object.values(first)).toString("utf8");
|
||||||
|
expect(publicBytes).not.toContain(EVIDENCE_FILE_BYTES.trim());
|
||||||
|
expect(publicBytes).not.toContain(SECRET_CANARY);
|
||||||
|
|
||||||
|
const imported = await importBundle(fixture.app, firstResponse.rawPayload);
|
||||||
|
expect(imported.statusCode).toBe(200);
|
||||||
|
expect(imported.json().draft.workspace).toEqual(filesystemEvidenceWorkspace);
|
||||||
|
expect(imported.json().draft.contract.variables.some(({ role }: { role: string }) => role === "EVIDENCE"))
|
||||||
|
.toBe(false);
|
||||||
|
expect(imported.body).not.toContain(EVIDENCE_FILE_BYTES.trim());
|
||||||
|
expect(imported.body).not.toContain(SECRET_CANARY);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
chmodSync, existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync,
|
chmodSync, existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -9,7 +9,9 @@ import { promisify } from "node:util";
|
|||||||
import { afterEach, expect, test } from "vitest";
|
import { afterEach, expect, test } from "vitest";
|
||||||
import { WorkspaceRepositoryLock } from "../src/workspaces/git-repository.js";
|
import { WorkspaceRepositoryLock } from "../src/workspaces/git-repository.js";
|
||||||
import { WorkspaceRegistry } from "../src/workspaces/registry.js";
|
import { WorkspaceRegistry } from "../src/workspaces/registry.js";
|
||||||
import { parseWorkspaceYaml, type CanonicalWorkspace } from "../src/workspaces/schema.js";
|
import {
|
||||||
|
parseWorkspaceYaml, renderWorkspaceDocs, serializeWorkspaceYaml, type CanonicalWorkspace,
|
||||||
|
} from "../src/workspaces/schema.js";
|
||||||
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
||||||
|
|
||||||
const validYaml = `workspace:
|
const validYaml = `workspace:
|
||||||
@@ -988,3 +990,96 @@ test("rejects a corrupt fallback snapshot instead of returning degraded active s
|
|||||||
|
|
||||||
await expect(registry.pull()).rejects.toMatchObject({ code: "workspace_invalid" });
|
await expect(registry.pull()).rejects.toMatchObject({ code: "workspace_invalid" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("snapshots canonical Evidence artifacts at the active commit without copying Evidence bytes", async () => {
|
||||||
|
const remote = await fixture(withFilesystemEvidence(validYaml));
|
||||||
|
const registryRoot = join(remote.root, "registry");
|
||||||
|
const registry = new WorkspaceRegistry(config(registryRoot, remote.remote));
|
||||||
|
|
||||||
|
const status = await registry.bootstrap();
|
||||||
|
const active = await registry.read("psd-clinical");
|
||||||
|
const snapshotDirectory = join(registryRoot, "snapshots", status.head!);
|
||||||
|
const descriptor = active.workspace as CanonicalWorkspace;
|
||||||
|
const docs = renderWorkspaceDocs(descriptor);
|
||||||
|
const expectedFiles: Record<string, string> = {
|
||||||
|
"psd-clinical.yaml": serializeWorkspaceYaml(descriptor),
|
||||||
|
"psd-clinical.env.example": docs.envExample,
|
||||||
|
"psd-clinical.md": docs.markdown,
|
||||||
|
};
|
||||||
|
const manifest = JSON.parse(readFileSync(join(snapshotDirectory, "snapshot.json"), "utf8"));
|
||||||
|
|
||||||
|
expect(status.head).toBe(remote.initialCommit);
|
||||||
|
const committedDescriptor = parseWorkspaceYaml(await gitOutput(remote.source, [
|
||||||
|
"show", `${remote.initialCommit}:workspaces/psd-clinical.yaml`,
|
||||||
|
])) as CanonicalWorkspace;
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
expect(Object.keys(manifest.files).sort()).toEqual(Object.keys(expectedFiles).sort());
|
||||||
|
for (const [name, contents] of Object.entries(expectedFiles)) {
|
||||||
|
expect(readFileSync(join(snapshotDirectory, name), "utf8")).toBe(contents);
|
||||||
|
expect(manifest.files[name]).toBe(createHash("sha256").update(contents).digest("hex"));
|
||||||
|
}
|
||||||
|
expect(JSON.stringify(manifest)).not.toContain("workspace-content/");
|
||||||
|
expect(readdirSync(snapshotDirectory).some((name) => name === "workspace-content")).toBe(false);
|
||||||
|
expect(readFileSync(join(remote.source, "workspace-content/psd-clinical/evidence/guide.md"), "utf8"))
|
||||||
|
.toBe("guide v1\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("never copies an installation secret canary into Git, generated artifacts, metadata, or errors", async () => {
|
||||||
|
const remote = await fixture(validYaml.concat(`evidence:
|
||||||
|
source:
|
||||||
|
type: http
|
||||||
|
uris: [https://evidence.example.test/guide.md]
|
||||||
|
authentication: signed_urls_file
|
||||||
|
`));
|
||||||
|
const canary = "CANARY-EVIDENCE-SECRET-ONLY-IN-FIXTURE";
|
||||||
|
const secretDirectory = join(remote.root, "fixture-secrets");
|
||||||
|
mkdirSync(secretDirectory);
|
||||||
|
const secretFile = join(secretDirectory, "signed-urls");
|
||||||
|
writeFileSync(secretFile, canary);
|
||||||
|
const registryRoot = join(remote.root, "registry");
|
||||||
|
const registry = new WorkspaceRegistry(config(registryRoot, remote.remote, {
|
||||||
|
secretRoots: [secretDirectory],
|
||||||
|
}));
|
||||||
|
const previous = process.env.THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE;
|
||||||
|
process.env.THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE = secretFile;
|
||||||
|
try {
|
||||||
|
const status = await registry.bootstrap();
|
||||||
|
const snapshotDirectory = join(registryRoot, "snapshots", status.head!);
|
||||||
|
let gitBlobText = "";
|
||||||
|
try {
|
||||||
|
gitBlobText = (await runFile(
|
||||||
|
"git", ["grep", "-I", "-h", "-e", canary, "HEAD", "--", "."], { cwd: remote.source },
|
||||||
|
)).stdout;
|
||||||
|
} catch (error) {
|
||||||
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== 1) throw error;
|
||||||
|
gitBlobText = "stdout" in error ? String(error.stdout ?? "") : "";
|
||||||
|
}
|
||||||
|
expect(gitBlobText).toBe("");
|
||||||
|
for (const name of readdirSync(snapshotDirectory)) {
|
||||||
|
expect(readFileSync(join(snapshotDirectory, name), "utf8")).not.toContain(canary);
|
||||||
|
}
|
||||||
|
let thrown: unknown;
|
||||||
|
try {
|
||||||
|
await registry.publish({
|
||||||
|
action: "create",
|
||||||
|
workspace: filesystemWorkspace("missing-secret-canary-tree"),
|
||||||
|
baseCommit: status.head!,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
thrown = error;
|
||||||
|
}
|
||||||
|
expect(thrown).toMatchObject({ code: "workspace_invalid" });
|
||||||
|
expect(String(thrown)).not.toContain(canary);
|
||||||
|
} finally {
|
||||||
|
if (previous === undefined) delete process.env.THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE;
|
||||||
|
else process.env.THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE = previous;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { parse } from "yaml";
|
import { parse } from "yaml";
|
||||||
import { buildInstallationContract, renderWorkspaceDocs } from "../src/workspaces/contracts.js";
|
import { buildInstallationContract, renderWorkspaceDocs } from "../src/workspaces/contracts.js";
|
||||||
@@ -385,3 +387,115 @@ semantic_index:
|
|||||||
llm_policy: { allowed: [zai/glm-5.2] }
|
llm_policy: { allowed: [zai/glm-5.2] }
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const evidenceSources = [
|
||||||
|
{
|
||||||
|
label: "filesystem",
|
||||||
|
source: { type: "filesystem", uri: "workspace-content/psd-clinical/evidence" },
|
||||||
|
variables: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "HTTP signed URL file",
|
||||||
|
source: {
|
||||||
|
type: "http",
|
||||||
|
uris: ["https://evidence.example.test/guide.md", "http://public.example.test/policy.pdf"],
|
||||||
|
authentication: "signed_urls_file",
|
||||||
|
},
|
||||||
|
variables: ["THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "S3 static files",
|
||||||
|
source: {
|
||||||
|
type: "s3", uri: "s3://clinical-evidence/published/", credentials: "static_files",
|
||||||
|
},
|
||||||
|
variables: [
|
||||||
|
"THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE",
|
||||||
|
"THT_WS_PSD_CLINICAL_EVIDENCE_SECRET_KEY_FILE",
|
||||||
|
"THT_WS_PSD_CLINICAL_EVIDENCE_SESSION_TOKEN_FILE",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
test.each(evidenceSources)("renders deterministic public Evidence docs for $label", ({ source, variables }) => {
|
||||||
|
const descriptor = parseWorkspaceYaml(
|
||||||
|
`${renderWorkspaceWithoutEvidence()}evidence:\n source: ${JSON.stringify(source)}\n`,
|
||||||
|
);
|
||||||
|
const firstContract = buildInstallationContract(descriptor);
|
||||||
|
const secondContract = buildInstallationContract(descriptor);
|
||||||
|
const firstDocs = renderWorkspaceDocs(descriptor);
|
||||||
|
const secondDocs = renderWorkspaceDocs(descriptor);
|
||||||
|
|
||||||
|
expect(secondContract).toEqual(firstContract);
|
||||||
|
expect(secondDocs).toEqual(firstDocs);
|
||||||
|
expect(firstContract.variables.filter(({ role }) => role === "EVIDENCE").map(({ name }) => name))
|
||||||
|
.toEqual(variables);
|
||||||
|
expect(firstDocs.markdown).toContain("## Evidence source");
|
||||||
|
expect(firstDocs.markdown).toContain(`- Type: \`${source.type}\``);
|
||||||
|
for (const uri of "uris" in source ? source.uris : [source.uri]) {
|
||||||
|
expect(firstDocs.markdown).toContain(`\`${uri}\``);
|
||||||
|
}
|
||||||
|
expect(firstDocs.markdown).toContain("- Maximum source bytes: `10485760`");
|
||||||
|
expect(firstDocs.markdown).toContain("- Maximum chunk characters: `4000`");
|
||||||
|
expect(firstDocs.markdown).toContain("- Retained published generations: `3`");
|
||||||
|
for (const variable of variables) {
|
||||||
|
expect(firstDocs.markdown).toContain(`\`${variable}\``);
|
||||||
|
expect(firstDocs.envExample).toContain(`${variable}=`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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`,
|
||||||
|
);
|
||||||
|
const docs = renderWorkspaceDocs(descriptor).markdown;
|
||||||
|
|
||||||
|
expect(docs).toContain("`workspace-content/psd-clinical/evidence`");
|
||||||
|
expect(docs).toMatch(/same Git revision/i);
|
||||||
|
expect(docs).toMatch(/P6.*materializ/i);
|
||||||
|
expect(docs).toMatch(/containment.*symlink/i);
|
||||||
|
expect(docs).toMatch(/does not include Evidence file bytes/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
source: { type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "none" },
|
||||||
|
expected: "No credential file is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: { type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "signed_urls_file" },
|
||||||
|
expected: "signed URL file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: { type: "s3", uri: "s3://clinical-evidence/published/", credentials: "ambient" },
|
||||||
|
expected: "ambient credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: { type: "s3", uri: "s3://clinical-evidence/published/", credentials: "static_files" },
|
||||||
|
expected: "installation file variables",
|
||||||
|
},
|
||||||
|
])("documents $source.type credential mode without reading credential contents", ({ source, expected }) => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "thoth-evidence-doc-secret-"));
|
||||||
|
const secrets = join(root, "secrets");
|
||||||
|
const canary = "CANARY-EVIDENCE-CREDENTIAL-DO-NOT-LEAK";
|
||||||
|
mkdirSync(secrets);
|
||||||
|
const binding = join(secrets, "credential");
|
||||||
|
writeFileSync(binding, canary);
|
||||||
|
const previous = process.env.THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE;
|
||||||
|
process.env.THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE = binding;
|
||||||
|
try {
|
||||||
|
const descriptor = parseWorkspaceYaml(
|
||||||
|
`${renderWorkspaceWithoutEvidence()}evidence:\n source: ${JSON.stringify(source)}\n`,
|
||||||
|
);
|
||||||
|
const generated = JSON.stringify({
|
||||||
|
contract: buildInstallationContract(descriptor),
|
||||||
|
docs: renderWorkspaceDocs(descriptor),
|
||||||
|
});
|
||||||
|
expect(generated).toContain(expected);
|
||||||
|
expect(generated).not.toContain(canary);
|
||||||
|
} finally {
|
||||||
|
if (previous === undefined) delete process.env.THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE;
|
||||||
|
else process.env.THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE = previous;
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user