Files
open-design/tools/pack/tests/cache.test.ts
T
8f6cdaaba1 Harden release-beta-s self-hosted beta lane (#3641)
* Fix Windows smoke launcher payload layout

* Harden release-beta-s runner probe

* Use cmd shell for release-beta-s probe

* Add win beta build script for self-hosted lane

* Keep release-beta-s checkout lightweight

* Use explicit local beta version for release-beta-s

* Install dependencies in beta build script

* Repair Electron dist before Windows beta build

* Add release-beta-s timings and diagnostics

* Polish release-beta-s observability guardrails

* Trace tools-pack cache materialization

* Reuse materialized workspace build cache

* Trace Windows packaging segments

* Trace Windows archive process details

* Reuse installer unpacked materialization

* Preserve reusable Windows unpacked projection

* Add fast smoke telemetry for self-hosted beta

* Cache Windows archive outputs for beta lane

* Tighten fast Windows smoke lane

* Scope Windows beta artifact reporting by target

* Clean stale Windows target artifacts

* Wire Windows Authenticode signing

* Expose self-hosted beta validation modes

* Add core Windows smoke profile

* Decouple core smoke from updater fixture version

* Trace Windows smoke lifecycle steps

* Skip unpacked materialization on NSIS cache hit

* Add Nexu S3 beta publisher

* Harden self-hosted Windows beta lane

* Use Windows PowerShell for publish probe

* Run publish probe via repo script

* Require explicit self-hosted S3 public origin

* Run real installer acceptance for external feeds

* Skip ready-prompt checks for external feeds

* Relax optional self-hosted update inputs

* Tolerate BOM in self-hosted publish probe

* Converge self-hosted beta metadata and signing flow

* Run self-hosted beta metadata prep on the runner lane

* Use Windows PowerShell for self-hosted beta metadata checks

* Bypass execution policy in self-hosted beta metadata checks

* Split self-hosted beta metadata publish

* Suppress mc output in beta publish helpers

* Split Windows NSIS payload cache layers

* Require fresh tool dist metadata

* Preserve Windows overlay payload paths

* Default beta-s gate to core

* Optimize win beta packaging reuse

* simplify windows full smoke lifecycle

* preserve onboarding state after windows upgrade smoke

* fix windows tools-pack test portability

* recover stale tools-pack locks

* Allow auto signing fallback for beta builds

* Add self-hosted beta mac arm64 lane

* Quote self-hosted signing choices

* Run self-hosted beta metadata on mac lane

* Use shallow metadata checkout on self-hosted beta

* Use sparse beta metadata checkout

* Make mac release profile bash-compatible

* Allow file sparse metadata checkout

* Isolate self-hosted metadata checkout

* Clear sparse skip-worktree before mac build

* Isolate self-hosted mac build checkout

* Run self-hosted beta publish without aws cli

* Use unsigned beta prefix for mixed platform publish

* Sparse checkout self-hosted beta publish

* Publish mac beta platform before metadata

* Pass mac publish release profile

* Probe mac signing keychain import

* Expand mac signing preflight variants

* Use sudo keychain helper for self-hosted mac signing

* Route self-hosted mac codesign through sudo wrapper

* Normalize mac signing identity name

* Harden self-hosted mac notarization

* Optimize self-hosted mac release cache

* Fix mac signing helper cleanup

* Retry transient mac notarization uploads

* Keep mac notarization fail-fast

* Reduce mac symlink diagnostics noise

* Normalize self-hosted beta platform inputs

* chore: bump beta base version to 0.9.1

* chore: tune release tools-pack cache retention

* test: add temporary cache validation marker

* chore: key windows nsis base payload by content

* Revert "test: add temporary cache validation marker"

This reverts commit e8487bfd3e.

* chore: probe windows nsis payload cache before materializing

* chore: align self-hosted beta smoke modes

* chore: skip mac smoke report upload when disabled

* chore: default self-hosted beta to publish both platforms

* chore: make beta signing modes explicit

* Add release report artifacts and metatool metadata

* Fix Windows release report zip publishing

* Remove artifact handoff from release beta self-hosted lane

* Fix release beta merge follow-ups

* Fix release beta install regressions

* Restore web build dependencies

* Route Windows beta downloads through mirrors

* Use PowerShell 7 for Windows beta lane

* Fix release beta preflight gates

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Fix Windows beta smoke update reuse

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Fix release beta review follow-ups

* Refresh Nix pnpm dependency hashes

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Fix Windows signtool fallback resolution

* Reject stale beta platform manifests

* Validate beta platform manifest run identity

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Validate beta manifest commit identity

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Allow beta metadata publish reruns

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Build tools-pack fully in beta Windows bootstrap

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Emit r2 metadata for Windows beta publish

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Stabilize beta publish browser test

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Stabilize Windows beta metadata smoke test

Generated-By: looper 0.9.4 (runner=fixer, agent=codex)

* Restore multi-platform Vela CLI package

* Refresh Nix pnpm dependency hashes

Generated-By: looper 0.9.5 (runner=fixer, agent=codex)

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-06-05 03:00:55 +00:00

205 lines
8.1 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 });
}
});
});