142 lines
4.5 KiB
JavaScript
142 lines
4.5 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
|
|
const preview = require("../ci_artifact_preview.cjs");
|
|
|
|
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ci-artifact-preview-"));
|
|
const MANIFEST = path.join(ROOT, "preview.json");
|
|
const OUTPUT = path.join(ROOT, "github-output.txt");
|
|
const SUMMARY = path.join(ROOT, "summary.md");
|
|
const WORKFLOW_SHA = "a".repeat(40);
|
|
const HEAD_SHA = "b".repeat(40);
|
|
|
|
assert.strictEqual(
|
|
preview.escapeHtml(`<tag attr="value">&'\\\``),
|
|
"<tag attr="value">&'\\`",
|
|
"step-summary values must be HTML-encoded instead of relying on incomplete Markdown escaping",
|
|
);
|
|
|
|
process.env.GITHUB_OUTPUT = OUTPUT;
|
|
const created = preview.createManifest({
|
|
output: MANIFEST,
|
|
mode: "source-preview",
|
|
repository: "owner/repo",
|
|
runId: "12345",
|
|
runAttempt: "1",
|
|
workflowSha: WORKFLOW_SHA,
|
|
headSha: HEAD_SHA,
|
|
primaryCategory: "skill",
|
|
categoriesJson: '["docs","skill"]',
|
|
driftFile: ["CATALOG.md", "data/skills.json"],
|
|
writeGithubOutput: true,
|
|
});
|
|
assert.match(created.digest, /^[0-9a-f]{64}$/);
|
|
assert.strictEqual(fs.readFileSync(OUTPUT, "utf8"), `manifest_digest=${created.digest}\n`);
|
|
assert.strictEqual(
|
|
fs.readFileSync(MANIFEST, "utf8"),
|
|
`${preview.canonicalJson(created.manifest)}\n`,
|
|
"create must use byte-stable canonical JSON",
|
|
);
|
|
|
|
process.env.GITHUB_STEP_SUMMARY = SUMMARY;
|
|
const verified = preview.verifySummary({
|
|
manifest: MANIFEST,
|
|
expectedRepository: "owner/repo",
|
|
expectedRunId: "12345",
|
|
expectedRunAttempt: "1",
|
|
expectedWorkflowSha: WORKFLOW_SHA,
|
|
expectedHeadSha: HEAD_SHA,
|
|
expectedDigest: created.digest,
|
|
writeStepSummary: true,
|
|
});
|
|
assert.deepStrictEqual(verified, created.manifest);
|
|
assert.match(fs.readFileSync(SUMMARY, "utf8"), /Artifact Preview[\s\S]*CATALOG\.md/);
|
|
|
|
for (const [field, value, pattern] of [
|
|
["expectedRepository", "other/repo", /repository/],
|
|
["expectedRunId", "999", /runId/],
|
|
["expectedRunAttempt", "2", /runAttempt/],
|
|
["expectedWorkflowSha", "c".repeat(40), /workflowSha/],
|
|
["expectedHeadSha", "d".repeat(40), /headSha/],
|
|
["expectedDigest", "e".repeat(64), /SHA-256/],
|
|
]) {
|
|
const options = {
|
|
manifest: MANIFEST,
|
|
expectedRepository: "owner/repo",
|
|
expectedRunId: "12345",
|
|
expectedRunAttempt: "1",
|
|
expectedWorkflowSha: WORKFLOW_SHA,
|
|
expectedHeadSha: HEAD_SHA,
|
|
expectedDigest: created.digest,
|
|
[field]: value,
|
|
};
|
|
assert.throws(() => preview.verifySummary(options), pattern);
|
|
}
|
|
|
|
assert.throws(
|
|
() => preview.validateManifest({ ...created.manifest, workflowSha: "short" }),
|
|
/full lowercase SHA-1/,
|
|
);
|
|
assert.throws(
|
|
() => preview.validateManifest({ ...created.manifest, categories: ["skill", "docs"] }),
|
|
/strictly sorted/,
|
|
);
|
|
assert.throws(
|
|
() => preview.validateManifest({ ...created.manifest, driftFiles: ["CATALOG.md", "CATALOG.md"] }),
|
|
/strictly sorted/,
|
|
);
|
|
assert.throws(
|
|
() => preview.validateManifest({ ...created.manifest, driftFiles: ["../escape.md"] }),
|
|
/unsafe path segment/,
|
|
);
|
|
assert.throws(
|
|
() => preview.validateManifest({ ...created.manifest, driftFiles: ["bad\\path.md"] }),
|
|
/normalized repository-relative/,
|
|
);
|
|
assert.throws(
|
|
() => preview.validateManifest({ ...created.manifest, driftFiles: ["bad\npath.md"] }),
|
|
/control characters/,
|
|
);
|
|
assert.throws(
|
|
() => preview.validateManifest({ ...created.manifest, mode: "canonical-exact-tree" }),
|
|
/must not contain generated drift/,
|
|
);
|
|
assert.doesNotThrow(() => preview.validateManifest({
|
|
...created.manifest,
|
|
mode: "canonical-exact-tree",
|
|
driftFiles: [],
|
|
}));
|
|
|
|
const nonCanonicalPath = path.join(ROOT, "noncanonical.json");
|
|
fs.writeFileSync(nonCanonicalPath, `${JSON.stringify(created.manifest, null, 2)}\n`, "utf8");
|
|
assert.throws(
|
|
() => preview.readCanonicalManifest(nonCanonicalPath),
|
|
/not encoded as canonical JSON/,
|
|
);
|
|
|
|
const tamperedPath = path.join(ROOT, "tampered.json");
|
|
fs.writeFileSync(tamperedPath, fs.readFileSync(MANIFEST, "utf8").replace("CATALOG.md", "README.md"), "utf8");
|
|
assert.throws(
|
|
() => preview.verifySummary({
|
|
manifest: tamperedPath,
|
|
expectedRepository: "owner/repo",
|
|
expectedRunId: "12345",
|
|
expectedRunAttempt: "1",
|
|
expectedWorkflowSha: WORKFLOW_SHA,
|
|
expectedHeadSha: HEAD_SHA,
|
|
expectedDigest: created.digest,
|
|
}),
|
|
/SHA-256/,
|
|
);
|
|
|
|
assert.throws(
|
|
() => preview.parseOptions(["create", "--mode", "source-preview", "--mode", "canonical-exact-tree"]),
|
|
/Duplicate option/,
|
|
);
|
|
assert.throws(() => preview.parseOptions(["unknown"]), /Unknown command/);
|
|
|
|
fs.rmSync(ROOT, { recursive: true, force: true });
|
|
console.log("ci artifact preview tests passed");
|