|
1 | 1 | import assert from "node:assert/strict" |
2 | 2 | import { execFile } from "node:child_process" |
3 | 3 | import { createHash } from "node:crypto" |
4 | | -import { cp, link, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises" |
| 4 | +import { cp, link, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises" |
5 | 5 | import { tmpdir } from "node:os" |
6 | 6 | import { join, resolve } from "node:path" |
7 | 7 | import { promisify } from "node:util" |
8 | | -import { calculateArtifactContentDigest, calculateArtifactManifestFileSha256 } from "@automattic/wp-codebox-core" |
| 8 | +import { calculateArtifactContentDigest, calculateArtifactManifestFileListDigest, calculateArtifactManifestFileSha256 } from "@automattic/wp-codebox-core" |
9 | 9 | import { preflightArtifactBundleApply, verifyArtifactBundle } from "@automattic/wp-codebox-core/artifacts" |
10 | | -import type { ArtifactManifest } from "@automattic/wp-codebox-core" |
| 10 | +import type { ArtifactBundle, ArtifactManifest } from "@automattic/wp-codebox-core" |
| 11 | +import { appendRecipeRuntimeEvidence } from "../packages/cli/src/recipe-evidence.js" |
11 | 12 |
|
12 | 13 | const execFileAsync = promisify(execFile) |
13 | 14 | const root = resolve(import.meta.dirname, "..") |
|
36 | 37 | const artifactsOption = await execFileAsync(process.execPath, ["packages/cli/dist/index.js", "artifacts", "verify", "--artifacts", validBundle, "--json"], { cwd: root }) |
37 | 38 | assert.equal(JSON.parse(artifactsOption.stdout).valid, true) |
38 | 39 |
|
| 40 | + const recipeEvidenceBundle = await copyBundle(validBundle, join(workspace, "recipe-evidence-appended")) |
| 41 | + await rewriteBundleToManifestFileListDigest(recipeEvidenceBundle) |
| 42 | + const recipeArtifacts = await artifactBundleFixture(recipeEvidenceBundle) |
| 43 | + await appendRecipeRuntimeEvidence(recipeArtifacts, [{ filename: "run-attestation.json", kind: "run-attestation", value: { schema: "fixture/run-attestation/v1", status: "passed" } }]) |
| 44 | + const recipeEvidenceVerification = await verifyArtifactBundle(recipeEvidenceBundle) |
| 45 | + assert.equal(recipeEvidenceVerification.valid, true) |
| 46 | + assert.equal(recipeEvidenceVerification.manifest?.contentDigest.value, recipeArtifacts.contentDigest) |
| 47 | + assert.equal(recipeEvidenceVerification.manifest?.files.some((file) => file.path === "files/runtime-evidence/run-attestation.json"), true) |
| 48 | + |
39 | 49 | const missingManifest = join(workspace, "missing-manifest") |
40 | 50 | await mkdir(missingManifest, { recursive: true }) |
41 | 51 | assertViolation(await verifyArtifactBundle(missingManifest), "missing-manifest") |
@@ -161,6 +171,56 @@ async function writeValidBundle(directory: string): Promise<void> { |
161 | 171 | await writeJson(join(directory, "manifest.json"), manifest) |
162 | 172 | } |
163 | 173 |
|
| 174 | +async function rewriteBundleToManifestFileListDigest(directory: string): Promise<void> { |
| 175 | + const manifest = JSON.parse(await readFile(join(directory, "manifest.json"), "utf8")) as ArtifactManifest |
| 176 | + const digest = calculateArtifactManifestFileListDigest(manifest.files) |
| 177 | + manifest.id = `artifact-bundle-sha256-${digest}` |
| 178 | + manifest.contentDigest = { algorithm: "sha256", inputs: ["manifest.files"], value: digest } |
| 179 | + await writeJson(join(directory, "files/review.json"), reviewFixture(digest)) |
| 180 | + await writeJson(join(directory, "metadata.json"), { |
| 181 | + id: manifest.id, |
| 182 | + contentDigest: manifest.contentDigest, |
| 183 | + artifacts: { |
| 184 | + changedFiles: "files/changed-files.json", |
| 185 | + patch: "files/patch.diff", |
| 186 | + review: "files/review.json", |
| 187 | + testResults: "files/test-results.json", |
| 188 | + runtimeEpisodeTrace: "files/runtime-episode-trace.json", |
| 189 | + runtimeEpisodeEvents: "files/runtime-episode.jsonl", |
| 190 | + }, |
| 191 | + }) |
| 192 | + await attachManifestFileHashes(directory, manifest) |
| 193 | + await writeJson(join(directory, "manifest.json"), manifest) |
| 194 | +} |
| 195 | + |
| 196 | +async function artifactBundleFixture(directory: string): Promise<ArtifactBundle> { |
| 197 | + const manifest = JSON.parse(await readFile(join(directory, "manifest.json"), "utf8")) as ArtifactManifest |
| 198 | + return { |
| 199 | + id: manifest.id, |
| 200 | + directory, |
| 201 | + manifestPath: join(directory, "manifest.json"), |
| 202 | + metadataPath: join(directory, "metadata.json"), |
| 203 | + blueprintAfterPath: join(directory, "blueprint.after.json"), |
| 204 | + blueprintAfterNotesPath: join(directory, "blueprint.after-notes.json"), |
| 205 | + eventsPath: join(directory, "events.jsonl"), |
| 206 | + commandsPath: join(directory, "commands.jsonl"), |
| 207 | + observationsPath: join(directory, "observations.jsonl"), |
| 208 | + runtimeLogPath: join(directory, "logs/runtime.log"), |
| 209 | + commandsLogPath: join(directory, "logs/commands.log"), |
| 210 | + mountsPath: join(directory, "files/mounts.json"), |
| 211 | + capturedMountsPath: join(directory, "files/mounted-files.json"), |
| 212 | + diffsPath: join(directory, "files/diffs.json"), |
| 213 | + workspacePatchPath: join(directory, "files/workspace-patch.json"), |
| 214 | + changedFilesPath: join(directory, "files/changed-files.json"), |
| 215 | + patchPath: join(directory, "files/patch.diff"), |
| 216 | + diagnosticsPath: join(directory, "files/diagnostics.json"), |
| 217 | + testResultsPath: join(directory, "files/test-results.json"), |
| 218 | + reviewPath: join(directory, "files/review.json"), |
| 219 | + contentDigest: manifest.contentDigest.value, |
| 220 | + createdAt: manifest.createdAt, |
| 221 | + } |
| 222 | +} |
| 223 | + |
164 | 224 | function manifestFixture(digest: string): ArtifactManifest { |
165 | 225 | return { |
166 | 226 | id: `artifact-bundle-sha256-${digest}`, |
|
0 commit comments