92 lines
5.2 KiB
JavaScript
92 lines
5.2 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { waitForCloudArtifacts } from "../../../scripts/cloud-readiness.mjs";
|
|
import { previewManifest } from "../../../scripts/preview-artifacts.mjs";
|
|
|
|
const sha = "a".repeat(40);
|
|
const digest = `sha256:${"b".repeat(64)}`;
|
|
const json = (body, status = 200) => new Response(JSON.stringify(body), { status });
|
|
function registry({ missing = new Set(), failure, wrongImage = false, wrongPackage = false } = {}) {
|
|
return async (url) => {
|
|
if (failure) return json({}, failure);
|
|
if (url.startsWith("https://registry.npmjs.org/")) {
|
|
const name = decodeURIComponent(new URL(url).pathname.split("/")[1]);
|
|
if (missing.has(name.split("/")[1])) return json({}, 404);
|
|
const pkg = previewManifest({ name, version: "0.0.0" }, sha);
|
|
return json({ ...pkg, ...(wrongPackage ? { gitHead: "c".repeat(40) } : {}), dist: { integrity: "sha512-fixture", tarball: "https://registry.npmjs.org/fixture.tgz" } });
|
|
}
|
|
if (url.includes("/token?")) return json({ token: "fixture" });
|
|
if (url.includes("/manifests/")) return missing.has("image") ? json({}, 404) : json({ config: { digest } });
|
|
if (url.includes("/blobs/")) return json({ config: { Labels: { "org.opencontainers.image.revision": wrongImage ? "c".repeat(40) : sha } } });
|
|
throw new Error(`Unexpected request: ${url}`);
|
|
};
|
|
}
|
|
|
|
test("readiness requires the image and both exact-source packages on the successful poll", async () => {
|
|
const missing = new Set(["image", "shared", "db"]);
|
|
let clock = 0;
|
|
const states = [];
|
|
const result = await waitForCloudArtifacts(sha, {
|
|
fetchImpl: registry({ missing }), now: () => clock, intervalMs: 10, timeoutMs: 100, log: (message) => states.push(message),
|
|
sleep: async (ms) => {
|
|
clock += ms;
|
|
if (clock === 10) missing.delete("image");
|
|
if (clock === 20) missing.delete("shared");
|
|
if (clock === 30) { missing.delete("db"); missing.add("image"); }
|
|
if (clock === 40) missing.delete("image");
|
|
},
|
|
});
|
|
assert.equal(clock, 40, "an artifact disappearing before the final poll must prevent readiness");
|
|
assert.deepEqual(result, { version: 1, sha, packageVersion: `0.0.0-preview.g${sha}` });
|
|
assert.match(states.at(-1), /Cloud artifacts available/);
|
|
});
|
|
|
|
test("missing artifacts time out with a precise inventory and bounded sleep", async () => {
|
|
let clock = 0;
|
|
const sleeps = [];
|
|
await assert.rejects(waitForCloudArtifacts(sha, {
|
|
fetchImpl: registry({ missing: new Set(["db"]) }), now: () => clock, timeoutMs: 25, intervalMs: 20, log: () => {},
|
|
sleep: async (ms) => { sleeps.push(ms); clock += ms; },
|
|
}), /timed out.*missing: db/);
|
|
assert.deepEqual(sleeps, [20, 5]);
|
|
});
|
|
|
|
for (const fixture of [{ failure: 403 }, { failure: 503 }, { wrongImage: true }, { wrongPackage: true }]) {
|
|
test(`registry errors and identity mismatches fail without waiting: ${JSON.stringify(fixture)}`, async () => {
|
|
await assert.rejects(waitForCloudArtifacts(sha, {
|
|
fetchImpl: registry(fixture), sleep: async () => assert.fail("must not retry an invalid artifact or upstream error"), log: () => {},
|
|
}));
|
|
});
|
|
}
|
|
|
|
test("invalid source and timing configuration are rejected before registry access", async () => {
|
|
const fetchImpl = async () => assert.fail("invalid inputs must not reach a registry");
|
|
await assert.rejects(waitForCloudArtifacts("master", { fetchImpl }), /full immutable commit SHA/);
|
|
for (const options of [{ timeoutMs: 0 }, { intervalMs: -1 }, { timeoutMs: Infinity }]) {
|
|
await assert.rejects(waitForCloudArtifacts(sha, { ...options, fetchImpl }), /positive finite/);
|
|
}
|
|
});
|
|
|
|
test("the versioned readiness job requires successful source, image and artifact jobs", () => {
|
|
const workflow = readFileSync(new URL("../../workflows/cloud-readiness.yml", import.meta.url), "utf8");
|
|
assert.match(workflow, /push:\s*\n\s*branches: \[master\]/);
|
|
assert.match(workflow, /group: cloud-readiness-\$\{\{ github.sha \}\}/);
|
|
assert.match(workflow, /uses: \.\/\.github\/workflows\/release-verify.yml\s+with:\s+ref: \$\{\{ github.sha \}\}/);
|
|
assert.match(workflow, /uses: \.\/\.github\/workflows\/docker-cloud.yml/);
|
|
const ready = workflow.split(" ready:")[1];
|
|
assert.match(ready, /name: Cloud deployable v1/);
|
|
assert.match(ready, /needs: \[verify, image, artifacts\]/);
|
|
assert.match(ready, /if: github.repository == 'paperclipai\/paperclip' && github.ref == 'refs\/heads\/master'/);
|
|
assert.doesNotMatch(ready, /^\s*(?:if:.*always\(|continue-on-error:)/m);
|
|
assert.doesNotMatch(workflow, /secrets: inherit|id-token: write|actions: write|checks: write|uses: .*@v\d\b/);
|
|
const cloud = readFileSync(new URL("../../workflows/docker-cloud.yml", import.meta.url), "utf8");
|
|
assert.doesNotMatch(cloud, /^ push:/m, "the master image must build only once");
|
|
const migrator = readFileSync(new URL("../../workflows/cloud-artifacts.yml", import.meta.url), "utf8");
|
|
assert.match(migrator, /push:\s*\n\s*branches: \[master\]/);
|
|
assert.match(migrator, /SOURCE_SHA: \$\{\{ github.sha \}\}/);
|
|
assert.match(migrator, /gh workflow run release.yml .*--ref master/);
|
|
assert.match(migrator, /--field channel=cloud-migrator/);
|
|
assert.match(migrator, /--field source_ref="\$SOURCE_SHA"/);
|
|
});
|