mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-07 14:38:42 +08:00
* Unify release metadata publishing * Allow empty release asset suffix * Harden beta-s release publishing * Clean beta Windows release namespace * Clean Windows launcher namespace * Retry transient mac notarization uploads * Control mac notarization upload mode * Notarize release mac DMGs after build * Harden release mac DMG notarization retries * Make beta-s mac updates payload-first * Bake mac updater metadata URL * Add unsafe DMG install helper * Fix beta-s feed and About version * Improve release validation observability * Build launcher proto before packaging * Notarize release-stable mac artifacts * Enable mac notarization in tools-pack * test: update packaged release workflow assertions
247 lines
9.7 KiB
TypeScript
247 lines
9.7 KiB
TypeScript
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { ToolPackCache } from "../src/cache.js";
|
|
|
|
async function pathExists(path: string): Promise<boolean> {
|
|
try {
|
|
await access(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
describe("ToolPackCache", () => {
|
|
it("builds once, materializes copies, and reports cache hits", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-cache-"));
|
|
const cacheRoot = join(root, "cache");
|
|
const firstOut = join(root, "first", "payload");
|
|
const secondOut = join(root, "second", "payload");
|
|
let builds = 0;
|
|
const cache = new ToolPackCache(cacheRoot);
|
|
const node = {
|
|
id: "test.node",
|
|
key: "key-1",
|
|
outputs: ["payload"],
|
|
invalidate: async () => null,
|
|
build: async ({ entryRoot }: { entryRoot: string }) => {
|
|
builds += 1;
|
|
await mkdir(join(entryRoot, "payload"), { recursive: true });
|
|
await writeFile(join(entryRoot, "payload", "value.txt"), `build-${builds}\n`, "utf8");
|
|
return { builds };
|
|
},
|
|
};
|
|
|
|
try {
|
|
const firstManifest = await cache.acquire({ materialize: [{ from: "payload", to: firstOut }], node });
|
|
await writeFile(join(firstOut, "value.txt"), "mutated\n", "utf8");
|
|
await cache.acquire({ materialize: [{ from: "payload", to: secondOut }], node });
|
|
|
|
expect(builds).toBe(1);
|
|
expect(firstManifest.entryPath).toContain("test.node");
|
|
expect(await readFile(join(secondOut, "value.txt"), "utf8")).toBe("build-1\n");
|
|
expect(cache.report().entries.map((entry) => entry.status)).toEqual(["miss", "hit"]);
|
|
} finally {
|
|
await rm(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("rebuilds stale entries when declared outputs are missing", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-cache-stale-"));
|
|
const cacheRoot = join(root, "cache");
|
|
const out = join(root, "out", "payload");
|
|
let builds = 0;
|
|
const cache = new ToolPackCache(cacheRoot);
|
|
const node = {
|
|
id: "test.stale",
|
|
key: "key-1",
|
|
outputs: ["payload"],
|
|
invalidate: async () => null,
|
|
build: async ({ entryRoot }: { entryRoot: string }) => {
|
|
builds += 1;
|
|
if (builds === 1) {
|
|
await mkdir(join(entryRoot, "payload"), { recursive: true });
|
|
await writeFile(join(entryRoot, "payload", "value.txt"), "first\n", "utf8");
|
|
return { builds };
|
|
}
|
|
await mkdir(join(entryRoot, "payload"), { recursive: true });
|
|
await writeFile(join(entryRoot, "payload", "value.txt"), "second\n", "utf8");
|
|
return { builds };
|
|
},
|
|
};
|
|
|
|
try {
|
|
await cache.acquire({ materialize: [{ from: "payload", to: out }], node });
|
|
const entryPath = cache.report().entries[0]?.entryPath;
|
|
expect(entryPath).toBeDefined();
|
|
await rm(join(entryPath!, "payload"), { force: true, recursive: true });
|
|
await cache.acquire({ materialize: [{ from: "payload", to: out }], node });
|
|
|
|
expect(builds).toBe(2);
|
|
expect(await readFile(join(out, "value.txt"), "utf8")).toBe("second\n");
|
|
expect(cache.report().entries.map((entry) => entry.status)).toEqual(["miss", "stale"]);
|
|
expect(await pathExists(join(cacheRoot, "locks", "global.lock"))).toBe(false);
|
|
} finally {
|
|
await rm(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("reads existing hits without building", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-cache-read-hit-"));
|
|
const cacheRoot = join(root, "cache");
|
|
const out = join(root, "out", "payload");
|
|
let builds = 0;
|
|
const cache = new ToolPackCache(cacheRoot);
|
|
const node = {
|
|
id: "test.read-hit",
|
|
key: "key-1",
|
|
outputs: ["payload"],
|
|
invalidate: async () => null,
|
|
build: async ({ entryRoot }: { entryRoot: string }) => {
|
|
builds += 1;
|
|
await mkdir(join(entryRoot, "payload"), { recursive: true });
|
|
await writeFile(join(entryRoot, "payload", "value.txt"), `build-${builds}\n`, "utf8");
|
|
return { builds };
|
|
},
|
|
};
|
|
|
|
try {
|
|
expect(await cache.readHit({ materialize: [{ from: "payload", to: out }], node })).toBeNull();
|
|
await cache.acquire({ materialize: [], node });
|
|
const hit = await cache.readHit({ materialize: [{ from: "payload", to: out }], node });
|
|
|
|
expect(hit?.payloadMetadata.builds).toBe(1);
|
|
expect(builds).toBe(1);
|
|
expect(await readFile(join(out, "value.txt"), "utf8")).toBe("build-1\n");
|
|
expect(cache.report().entries.map((entry) => entry.status)).toEqual(["miss", "hit"]);
|
|
} finally {
|
|
await rm(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("reuses stamped materialized targets until the cache key changes", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-cache-reuse-"));
|
|
const cacheRoot = join(root, "cache");
|
|
const out = join(root, "out", "payload");
|
|
let builds = 0;
|
|
const cache = new ToolPackCache(cacheRoot);
|
|
const createNode = (key: string) => ({
|
|
id: "test.reuse",
|
|
key,
|
|
outputs: ["payload"],
|
|
invalidate: async () => null,
|
|
build: async ({ entryRoot }: { entryRoot: string }) => {
|
|
builds += 1;
|
|
await mkdir(join(entryRoot, "payload"), { recursive: true });
|
|
await writeFile(join(entryRoot, "payload", "value.txt"), `build-${builds}\n`, "utf8");
|
|
return { builds };
|
|
},
|
|
});
|
|
|
|
try {
|
|
await cache.acquire({ materialize: [{ from: "payload", reuse: true, to: out }], node: createNode("key-1") });
|
|
await writeFile(join(out, "value.txt"), "mutated\n", "utf8");
|
|
await cache.acquire({ materialize: [{ from: "payload", reuse: true, to: out }], node: createNode("key-1") });
|
|
|
|
expect(await readFile(join(out, "value.txt"), "utf8")).toBe("mutated\n");
|
|
expect(cache.report().entries.at(-1)?.materialized[0]?.skipped).toBe(true);
|
|
|
|
await cache.acquire({ materialize: [{ from: "payload", reuse: true, to: out }], node: createNode("key-2") });
|
|
|
|
expect(await readFile(join(out, "value.txt"), "utf8")).toBe("build-2\n");
|
|
expect(cache.report().entries.at(-1)?.materialized[0]?.skipped).toBeUndefined();
|
|
} finally {
|
|
await rm(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("seeds a miss from an alias but still builds the exact entry", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-cache-seed-"));
|
|
const cacheRoot = join(root, "cache");
|
|
const out = join(root, "out", "payload");
|
|
let builds = 0;
|
|
const cache = new ToolPackCache(cacheRoot);
|
|
const createNode = (key: string) => ({
|
|
id: "test.seed",
|
|
key,
|
|
outputs: ["payload"],
|
|
invalidate: async () => null,
|
|
build: async ({ entryRoot }: { entryRoot: string }) => {
|
|
builds += 1;
|
|
const seed = await readFile(join(out, "value.txt"), "utf8").catch(() => "<none>\n");
|
|
await mkdir(join(entryRoot, "payload"), { recursive: true });
|
|
await writeFile(join(entryRoot, "payload", "value.txt"), `build-${builds};seed=${seed.trim()}\n`, "utf8");
|
|
return { builds };
|
|
},
|
|
});
|
|
|
|
try {
|
|
await cache.acquire({
|
|
aliases: ["family"],
|
|
materialize: [{ from: "payload", reuse: true, to: out }],
|
|
node: createNode("key-1"),
|
|
});
|
|
await rm(out, { force: true, recursive: true });
|
|
await cache.acquire({
|
|
aliases: ["family"],
|
|
materialize: [{ from: "payload", reuse: true, to: out }],
|
|
node: createNode("key-2"),
|
|
seedFrom: [{ aliasKey: "family", materialize: [{ from: "payload", to: out }] }],
|
|
});
|
|
|
|
expect(builds).toBe(2);
|
|
expect(await readFile(join(out, "value.txt"), "utf8")).toBe("build-2;seed=build-1;seed=<none>\n");
|
|
expect(cache.report().entries.map((entry) => entry.status)).toEqual(["miss", "miss"]);
|
|
expect(cache.report().entries.at(-1)?.reason).toContain("seeded from alias");
|
|
} finally {
|
|
await rm(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("skips alias seeds that cannot materialize the requested source", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "open-design-tools-pack-cache-missing-seed-source-"));
|
|
const cacheRoot = join(root, "cache");
|
|
const out = join(root, "out", "payload");
|
|
let builds = 0;
|
|
const cache = new ToolPackCache(cacheRoot);
|
|
const createNode = (key: string) => ({
|
|
id: "test.missing-seed-source",
|
|
key,
|
|
outputs: ["payload"],
|
|
invalidate: async () => null,
|
|
build: async ({ entryRoot }: { entryRoot: string }) => {
|
|
builds += 1;
|
|
const seed = await readFile(join(out, "value.txt"), "utf8").catch(() => "<none>\n");
|
|
await mkdir(join(entryRoot, "payload"), { recursive: true });
|
|
await writeFile(join(entryRoot, "payload", "value.txt"), `build-${builds};seed=${seed.trim()}\n`, "utf8");
|
|
return { builds };
|
|
},
|
|
});
|
|
|
|
try {
|
|
await cache.acquire({
|
|
aliases: ["family"],
|
|
materialize: [{ from: "payload", to: out }],
|
|
node: createNode("key-1"),
|
|
});
|
|
await rm(out, { force: true, recursive: true });
|
|
await cache.acquire({
|
|
aliases: ["family"],
|
|
materialize: [{ from: "payload", to: out }],
|
|
node: createNode("key-2"),
|
|
seedFrom: [{ aliasKey: "family", materialize: [{ from: "missing", to: out }] }],
|
|
});
|
|
|
|
expect(builds).toBe(2);
|
|
expect(await readFile(join(out, "value.txt"), "utf8")).toBe("build-2;seed=<none>\n");
|
|
expect(cache.report().entries.at(-1)?.reason).not.toContain("seeded from alias");
|
|
} finally {
|
|
await rm(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
});
|