mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 05:46:17 +00:00
* test: strengthen and prune audited test coverage * test: require delivered DeepSeek skill instructions * test: keep schema fixture types private * test: verify Gemini receives skill instructions * test: bind Qwen skill evidence to its tool result * test: report NativeAOT verifier checks precisely * fix: verify Ghidra cleanup paths reliably * fix: verify Command Code skill activation * test: tighten cleanup and fixture checks * test: verify packet capture evidence is recorded * fix: report HTTP3 capture provenance accurately * test: preserve resource coverage through MCP * test: prune repeated assertions and retain cancellation identity * fix: validate client verifier fixture paths * test: skip Copilot verifier cases on Windows * test: keep verifier setup target relative
501 lines
15 KiB
TypeScript
501 lines
15 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { chmod, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { delimiter, join } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
listAllCaches,
|
|
retainCaches,
|
|
selectNpmDownloadDuplicateDeletions,
|
|
selectPrefixDeletions,
|
|
} from "../../../scripts/ci/cache-retention.mjs";
|
|
|
|
const cache = (
|
|
id: number,
|
|
key: string,
|
|
created: string,
|
|
accessed = created,
|
|
size = 100,
|
|
version = "a".repeat(64),
|
|
ref = "refs/heads/main",
|
|
) => ({
|
|
id,
|
|
key,
|
|
version,
|
|
ref,
|
|
created_at: created,
|
|
last_accessed_at: accessed,
|
|
size_in_bytes: size,
|
|
});
|
|
|
|
describe("Turbo snapshot retention", () => {
|
|
const prefix = "rea-static-v1-Linux-X64-";
|
|
|
|
it("keeps the two newest snapshots and the saved key", () => {
|
|
const keepKey = `${prefix}saved`;
|
|
const deletions = selectPrefixDeletions(
|
|
[
|
|
cache(1, `${prefix}old`, "2026-10-20T01:00:00Z", undefined, 10),
|
|
cache(2, `${prefix}older`, "2026-10-20T02:00:00Z", undefined, 20),
|
|
cache(3, `${prefix}recent`, "2026-10-20T03:00:00Z", undefined, 30),
|
|
cache(4, keepKey, "2026-10-20T00:00:00Z", undefined, 40),
|
|
],
|
|
{ prefix, keepKey },
|
|
);
|
|
expect(deletions.map((entry) => entry.id)).toEqual([1]);
|
|
expect(deletions.reduce((sum, entry) => sum + entry.size, 0)).toBe(10);
|
|
});
|
|
|
|
it("keeps an in-flight generation when the saved key is already newest", () => {
|
|
const keepKey = `${prefix}newest`;
|
|
const deletions = selectPrefixDeletions(
|
|
[
|
|
cache(1, `${prefix}a`, "2026-10-20T01:00:00Z"),
|
|
cache(2, `${prefix}b`, "2026-10-20T02:00:00Z"),
|
|
cache(3, `${prefix}c`, "2026-10-20T03:00:00Z"),
|
|
cache(4, keepKey, "2026-10-20T04:00:00Z"),
|
|
],
|
|
{ prefix, keepKey },
|
|
);
|
|
expect(
|
|
deletions.map((entry) => entry.id).sort((left, right) => left - right),
|
|
).toEqual([1, 2]);
|
|
});
|
|
|
|
it("keeps two generations of the deep and Knip snapshot families", () => {
|
|
const deep = "rea-static-deep-v1-Linux-X64-";
|
|
const knip = "knip-meta-v1-Linux-";
|
|
expect(
|
|
selectPrefixDeletions(
|
|
[
|
|
cache(1, `${deep}old`, "2026-10-20T01:00:00Z"),
|
|
cache(2, `${deep}mid`, "2026-10-20T02:00:00Z"),
|
|
cache(3, `${deep}new`, "2026-10-20T03:00:00Z"),
|
|
],
|
|
{ prefix: deep, keepKey: `${deep}new` },
|
|
).map((entry) => entry.id),
|
|
).toEqual([1]);
|
|
expect(
|
|
selectPrefixDeletions(
|
|
[
|
|
cache(4, `${knip}old`, "2026-10-20T01:00:00Z"),
|
|
cache(5, `${knip}mid`, "2026-10-20T02:00:00Z"),
|
|
cache(6, `${knip}new`, "2026-10-20T03:00:00Z"),
|
|
],
|
|
{ prefix: knip, keepKey: `${knip}new` },
|
|
).map((entry) => entry.id),
|
|
).toEqual([4]);
|
|
});
|
|
|
|
it("retains the newest portable snapshots without a caller keep key", () => {
|
|
const portable = "rea-portable-build-v2-Linux-X64-";
|
|
const deletions = selectPrefixDeletions(
|
|
[
|
|
cache(7, `${portable}one`, "2026-10-20T01:00:00Z"),
|
|
cache(8, `${portable}two`, "2026-10-20T02:00:00Z"),
|
|
cache(9, `${portable}three`, "2026-10-20T03:00:00Z"),
|
|
],
|
|
{ prefix: portable },
|
|
);
|
|
expect(deletions.map((entry) => entry.id)).toEqual([7]);
|
|
});
|
|
|
|
it("refuses prefixes and lists that can reach download caches", () => {
|
|
expect(() =>
|
|
selectPrefixDeletions([], { prefix: "npm-download-Linux-x64-" }),
|
|
).toThrow(/one Turbo family/u);
|
|
expect(() =>
|
|
selectPrefixDeletions([], { prefix: "rea-static-v1-" }),
|
|
).toThrow(/one Turbo family/u);
|
|
expect(() =>
|
|
selectPrefixDeletions([], { prefix: "knip-meta-v1-" }),
|
|
).toThrow(/one Turbo family/u);
|
|
expect(() =>
|
|
selectPrefixDeletions(
|
|
[cache(1, "npm-download-Linux-x64-abc", "2026-10-20T01:00:00Z")],
|
|
{
|
|
prefix,
|
|
keepKey: `${prefix}saved`,
|
|
},
|
|
),
|
|
).toThrow(/outside/u);
|
|
expect(() =>
|
|
selectPrefixDeletions([], {
|
|
prefix,
|
|
keepKey: "rea-static-v1-Windows-X64-other",
|
|
}),
|
|
).toThrow(/keep key/u);
|
|
});
|
|
});
|
|
|
|
describe("npm download duplicate retention", () => {
|
|
it("deletes only an older copy of the same key, version, and ref", () => {
|
|
const key = "npm-download-Linux-x64-lock";
|
|
const version = "a".repeat(64);
|
|
const ref = "refs/heads/main";
|
|
const deletions = selectNpmDownloadDuplicateDeletions([
|
|
cache(
|
|
1,
|
|
key,
|
|
"2026-10-20T01:00:00Z",
|
|
"2026-10-20T02:00:00Z",
|
|
179,
|
|
version,
|
|
ref,
|
|
),
|
|
cache(
|
|
2,
|
|
key,
|
|
"2026-10-20T01:05:00Z",
|
|
"2026-10-20T03:00:00Z",
|
|
199,
|
|
version,
|
|
ref,
|
|
),
|
|
cache(
|
|
3,
|
|
"npm-download-Windows-x64-lock",
|
|
"2026-10-20T01:00:00Z",
|
|
undefined,
|
|
186,
|
|
),
|
|
]);
|
|
expect(deletions.map((entry) => entry.id)).toEqual([1]);
|
|
});
|
|
|
|
it("keeps records that share a key but differ by version or ref", () => {
|
|
const key = "npm-download-Linux-x64-lock";
|
|
const version = "a".repeat(64);
|
|
const otherVersion = "b".repeat(64);
|
|
const deletions = selectNpmDownloadDuplicateDeletions([
|
|
cache(
|
|
1,
|
|
key,
|
|
"2026-10-20T01:00:00Z",
|
|
"2026-10-20T04:00:00Z",
|
|
199,
|
|
version,
|
|
"refs/heads/main",
|
|
),
|
|
cache(
|
|
2,
|
|
key,
|
|
"2026-10-20T01:00:00Z",
|
|
"2026-10-20T03:00:00Z",
|
|
188,
|
|
otherVersion,
|
|
"refs/heads/main",
|
|
),
|
|
cache(
|
|
3,
|
|
key,
|
|
"2026-10-20T01:00:00Z",
|
|
"2026-10-20T02:00:00Z",
|
|
180,
|
|
version,
|
|
"refs/heads/feature",
|
|
),
|
|
]);
|
|
expect(deletions).toEqual([]);
|
|
});
|
|
|
|
it("refuses a download record without a version or ref", () => {
|
|
const key = "npm-download-Linux-x64-lock";
|
|
const { version: _version, ...withoutVersion } = cache(
|
|
1,
|
|
key,
|
|
"2026-10-20T01:00:00Z",
|
|
);
|
|
expect(() => selectNpmDownloadDuplicateDeletions([withoutVersion])).toThrow(
|
|
/version/u,
|
|
);
|
|
const { ref: _ref, ...withoutRef } = cache(2, key, "2026-10-20T01:00:00Z");
|
|
expect(() => selectNpmDownloadDuplicateDeletions([withoutRef])).toThrow(
|
|
/ref/u,
|
|
);
|
|
});
|
|
|
|
it("does not delete the only copy when pagination repeats its id", () => {
|
|
const key = "npm-download-Linux-x64-lock";
|
|
const version = "a".repeat(64);
|
|
const ref = "refs/heads/main";
|
|
const deletions = selectNpmDownloadDuplicateDeletions([
|
|
cache(
|
|
5,
|
|
key,
|
|
"2026-10-20T01:00:00Z",
|
|
"2026-10-20T02:00:00Z",
|
|
179,
|
|
version,
|
|
ref,
|
|
),
|
|
cache(
|
|
5,
|
|
key,
|
|
"2026-10-20T01:00:00Z",
|
|
"2026-10-20T04:00:00Z",
|
|
179,
|
|
version,
|
|
ref,
|
|
),
|
|
]);
|
|
expect(deletions).toEqual([]);
|
|
});
|
|
|
|
it("aborts when the same id is listed with a different version", () => {
|
|
const key = "npm-download-Linux-x64-lock";
|
|
expect(() =>
|
|
selectNpmDownloadDuplicateDeletions([
|
|
cache(5, key, "2026-10-20T01:00:00Z", undefined, 179, "a".repeat(64)),
|
|
cache(5, key, "2026-10-20T01:00:00Z", undefined, 179, "b".repeat(64)),
|
|
]),
|
|
).toThrow(/inconsistent/u);
|
|
});
|
|
|
|
it("refuses a Turbo snapshot in the download list", () => {
|
|
expect(() =>
|
|
selectNpmDownloadDuplicateDeletions([
|
|
cache(1, "rea-static-v1-Linux-X64-sha", "2026-10-20T01:00:00Z"),
|
|
]),
|
|
).toThrow(/non-download/u);
|
|
});
|
|
});
|
|
|
|
describe("retention orchestration", () => {
|
|
const repository = "morluto/rea";
|
|
const prefix = "rea-static-v1-Linux-X64-";
|
|
|
|
it("deletes nothing when a later list is incomplete", async () => {
|
|
const deleted: number[] = [];
|
|
const requests: string[] = [];
|
|
const page = [
|
|
cache(1, `${prefix}old`, "2026-10-20T01:00:00Z"),
|
|
cache(2, `${prefix}mid`, "2026-10-20T02:00:00Z"),
|
|
cache(3, `${prefix}new`, "2026-10-20T03:00:00Z"),
|
|
];
|
|
await expect(
|
|
retainCaches({
|
|
repository,
|
|
request: async (_method, endpoint) => {
|
|
requests.push(
|
|
new URL(endpoint, "https://example.test").searchParams.get("key") ??
|
|
"",
|
|
);
|
|
if (endpoint.includes("npm-download-"))
|
|
throw new Error("list failed after the first family");
|
|
return { actions_caches: page };
|
|
},
|
|
deleteCache: async (id) => {
|
|
deleted.push(id);
|
|
},
|
|
families: [{ prefix, keepKey: `${prefix}new` }],
|
|
dedupeNpmDownloads: true,
|
|
}),
|
|
).rejects.toThrow("list failed after the first family");
|
|
expect(requests).toEqual([prefix, "npm-download-"]);
|
|
expect(deleted).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("cache listing and retention", () => {
|
|
const repository = "morluto/rea";
|
|
const prefix = "rea-static-v1-Linux-X64-";
|
|
|
|
it("deletes nothing when the cache listing reaches the page cap", async () => {
|
|
const deleted: number[] = [];
|
|
let requests = 0;
|
|
const page = Array.from({ length: 100 }, (_, index) =>
|
|
cache(index + 1, `${prefix}${String(index)}`, "2026-10-20T01:00:00Z"),
|
|
);
|
|
await expect(
|
|
retainCaches({
|
|
repository,
|
|
request: async () => {
|
|
requests += 1;
|
|
return { actions_caches: page };
|
|
},
|
|
deleteCache: async (id) => {
|
|
deleted.push(id);
|
|
},
|
|
families: [{ prefix, keepKey: `${prefix}0` }],
|
|
}),
|
|
).rejects.toThrow(/exceeded.*pages/u);
|
|
expect(requests).toBe(10);
|
|
expect(deleted).toEqual([]);
|
|
});
|
|
|
|
it("stops listing once a short page proves the inventory is complete", async () => {
|
|
const requests: string[] = [];
|
|
const caches = await listAllCaches(
|
|
async (_method, endpoint) => {
|
|
requests.push(endpoint);
|
|
const page = new URL(endpoint, "https://example.test").searchParams.get(
|
|
"page",
|
|
);
|
|
return {
|
|
actions_caches:
|
|
page === "1"
|
|
? Array.from({ length: 100 }, (_, index) =>
|
|
cache(
|
|
index + 1,
|
|
`${prefix}${String(index)}`,
|
|
"2026-10-20T01:00:00Z",
|
|
),
|
|
)
|
|
: [cache(101, `${prefix}last`, "2026-10-20T02:00:00Z")],
|
|
};
|
|
},
|
|
repository,
|
|
prefix,
|
|
);
|
|
expect(caches).toHaveLength(101);
|
|
expect(requests).toHaveLength(2);
|
|
});
|
|
|
|
it("applies every family only after both lists succeed", async () => {
|
|
const deleted: number[] = [];
|
|
const keepKey = `${prefix}saved`;
|
|
const portable = "rea-portable-build-v2-Linux-X64-";
|
|
const result = await retainCaches({
|
|
repository,
|
|
request: async (_method, endpoint) => {
|
|
const key = new URL(endpoint, "https://example.test").searchParams.get(
|
|
"key",
|
|
);
|
|
if (key === prefix)
|
|
return {
|
|
actions_caches: [
|
|
cache(1, `${prefix}old`, "2026-10-20T01:00:00Z"),
|
|
cache(2, `${prefix}mid`, "2026-10-20T02:00:00Z"),
|
|
cache(3, keepKey, "2026-10-20T03:00:00Z"),
|
|
],
|
|
};
|
|
if (key === portable)
|
|
return {
|
|
actions_caches: [
|
|
cache(4, `${portable}old`, "2026-10-20T01:00:00Z"),
|
|
cache(5, `${portable}new`, "2026-10-20T02:00:00Z"),
|
|
],
|
|
};
|
|
return {
|
|
actions_caches: [
|
|
cache(
|
|
6,
|
|
"npm-download-Linux-x64-lock",
|
|
"2026-10-20T01:00:00Z",
|
|
"2026-10-20T01:00:00Z",
|
|
),
|
|
cache(
|
|
7,
|
|
"npm-download-Linux-x64-lock",
|
|
"2026-10-20T02:00:00Z",
|
|
"2026-10-20T03:00:00Z",
|
|
),
|
|
],
|
|
};
|
|
},
|
|
deleteCache: async (id) => {
|
|
deleted.push(id);
|
|
},
|
|
families: [{ prefix, keepKey }, { prefix: portable }],
|
|
dedupeNpmDownloads: true,
|
|
});
|
|
expect(deleted).toEqual([1, 6]);
|
|
expect(result.plan[0]?.deletions.map((entry) => entry.id)).toEqual([1]);
|
|
expect(result.plan[1]?.deletions).toEqual([]);
|
|
expect(result.npmDeletions.map((entry) => entry.id)).toEqual([6]);
|
|
});
|
|
|
|
it("dry-run reports the same deletions without calling delete", async () => {
|
|
const result = await retainCaches({
|
|
repository,
|
|
request: async () => ({
|
|
actions_caches: [
|
|
cache(1, `${prefix}old`, "2026-10-20T01:00:00Z", undefined, 50),
|
|
cache(2, `${prefix}mid`, "2026-10-20T02:00:00Z", undefined, 60),
|
|
cache(3, `${prefix}new`, "2026-10-20T03:00:00Z", undefined, 70),
|
|
],
|
|
}),
|
|
deleteCache: async () => {
|
|
throw new Error("dry-run must not delete");
|
|
},
|
|
families: [{ prefix, keepKey: `${prefix}new` }],
|
|
dryRun: true,
|
|
});
|
|
expect(result.dryRun).toBe(true);
|
|
expect(result.plan[0]?.deletions.map((entry) => entry.id)).toEqual([1]);
|
|
});
|
|
});
|
|
|
|
describe("repeated cache ids", () => {
|
|
const repository = "morluto/rea";
|
|
const prefix = "rea-static-v1-Linux-X64-";
|
|
|
|
it("treats one cache id on two pages as a single snapshot", () => {
|
|
const keepKey = `${prefix}saved`;
|
|
const deletions = selectPrefixDeletions(
|
|
[
|
|
cache(1, `${prefix}old`, "2026-10-20T01:00:00Z"),
|
|
cache(2, `${prefix}mid`, "2026-10-20T02:00:00Z"),
|
|
cache(3, keepKey, "2026-10-20T03:00:00Z", "2026-10-20T03:00:00Z"),
|
|
cache(3, keepKey, "2026-10-20T03:00:00Z", "2026-10-20T04:00:00Z"),
|
|
],
|
|
{ prefix, keepKey },
|
|
);
|
|
expect(deletions.map((entry) => entry.id)).toEqual([1]);
|
|
});
|
|
|
|
it("does not delete when one cache id is listed with inconsistent fields", async () => {
|
|
const deleted: number[] = [];
|
|
const repeated = cache(3, `${prefix}new`, "2026-10-20T03:00:00Z");
|
|
await expect(
|
|
retainCaches({
|
|
repository,
|
|
request: async () => ({
|
|
actions_caches: [
|
|
cache(1, `${prefix}old`, "2026-10-20T01:00:00Z"),
|
|
cache(2, `${prefix}mid`, "2026-10-20T02:00:00Z"),
|
|
repeated,
|
|
{ ...repeated, version: "b".repeat(64) },
|
|
],
|
|
}),
|
|
deleteCache: async (id) => {
|
|
deleted.push(id);
|
|
},
|
|
families: [{ prefix, keepKey: `${prefix}new` }],
|
|
}),
|
|
).rejects.toThrow(/inconsistent/u);
|
|
expect(deleted).toEqual([]);
|
|
});
|
|
});
|
|
|
|
it("runs retention from the command line without deleting on an unsafe prefix", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-cache-retention-"));
|
|
const log = join(root, "gh.log");
|
|
const gh = join(root, "gh");
|
|
await writeFile(
|
|
gh,
|
|
`#!/bin/sh
|
|
printf '%s\\n' "$@" >> "${log}"
|
|
exit 1
|
|
`,
|
|
);
|
|
await chmod(gh, 0o755);
|
|
expect(() =>
|
|
execFileSync("node", ["scripts/ci/cache-retention.mjs"], {
|
|
cwd: process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
PATH: `${root}${delimiter}${process.env.PATH ?? ""}`,
|
|
CI_REPOSITORY: "morluto/rea",
|
|
CACHE_PREFIX: "npm-download-Linux-x64-",
|
|
CACHE_KEEP_KEY: "npm-download-Linux-x64-lock",
|
|
},
|
|
encoding: "utf8",
|
|
}),
|
|
).toThrow(/one Turbo family/u);
|
|
await expect(readFile(log, "utf8")).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|