From cd6e019ec2bc5f5ac112048313478047d295ae48 Mon Sep 17 00:00:00 2001 From: Free Wortley Date: Wed, 23 Sep 2026 23:19:36 -0700 Subject: [PATCH] improve(ci): explain Vitest transform cache reuse and invalidation (#155444) * improve: distinguish Vitest archive restores from local cache invalidation * test(ci): model transform cache restore outputs * test(ci): follow upstream transform cache root contract * test(ci): require transform cache restore prefix --- .github/actions/setup-node-env/action.yml | 12 ++++ test/scripts/ci-workflow-guards.test.ts | 14 ++++ .../setup-node-env-transform-cache.test.ts | 71 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 test/scripts/setup-node-env-transform-cache.test.ts diff --git a/.github/actions/setup-node-env/action.yml b/.github/actions/setup-node-env/action.yml index 474b5358ced6..f35405fd352d 100644 --- a/.github/actions/setup-node-env/action.yml +++ b/.github/actions/setup-node-env/action.yml @@ -269,6 +269,10 @@ runs: env: CACHE_GENERATION: ${{ steps.vitest-cache-generation.outputs.value }} CACHE_WRITER: "0" + # Prefix restores report cache-hit=false too; matched-key owns whether + # the archive was restored, independently of runner-local contents. + CACHE_RESTORED: ${{ steps.vitest-cache.outputs['cache-matched-key'] != '' && 'true' || 'false' }} + CACHE_RUNNER_ENVIRONMENT: ${{ runner.environment }} shell: bash run: | set -euo pipefail @@ -280,13 +284,21 @@ runs: cache_generation="$(<"$generation_file")" fi cache_entry="$(find "$cache_root" -mindepth 1 -maxdepth 1 -print -quit)" + cache_local=empty + if [[ -n "$cache_entry" ]]; then + cache_local=retained + fi # Every restore is runner-local. Never mix incompatible transform # inputs even when an older archive was selected through a prefix. if [[ -n "$cache_entry" ]] && [[ "$cache_generation" != "$CACHE_GENERATION" ]]; then echo "Vitest transform inputs changed; clearing incompatible cache generation" find "$cache_root" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + cache_local=cleared fi printf '%s\n' "$CACHE_GENERATION" > "$generation_file" + # This describes archive/local generation state, not per-file transform hits. + printf 'Vitest transform cache: restored=%s local=%s generation=%s runner=%s\n' \ + "$CACHE_RESTORED" "$cache_local" "$CACHE_GENERATION" "$CACHE_RUNNER_ENVIRONMENT" # The shard runner treats this as a persistent root and assigns one # isolated subdirectory per concurrent Vitest worker. echo "OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT=$cache_root" >> "$GITHUB_ENV" diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 5dd4dc572268..c197e2b1c174 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -5705,6 +5705,7 @@ server.listen(0, "127.0.0.1", () => { }); let cacheInputs: Record | undefined; let configuredGeneration: string | undefined; + let configuredRestored: string | undefined; for (const step of transformSteps) { // Runner v2.336.0 evaluates embedded env before if; run/with inputs // are evaluated only after admission (CompositeActionHandler/ActionRunner). @@ -5734,8 +5735,19 @@ server.listen(0, "127.0.0.1", () => { cacheInputs = Object.fromEntries( Object.entries(step.with ?? {}).map(([key, value]) => [key, render(value)]), ); + // Exercise misses and prefix hits; neither reports cache-hit=true. + steps[expectDefined(step.id, "transform restore step id")] = { + outputs: { + "cache-hit": generation === "a".repeat(64) ? "" : "false", + "cache-matched-key": + generation === "a".repeat(64) + ? "" + : `${expectDefined(cacheInputs["restore-keys"], "transform restore prefix").trim()}9-1`, + }, + }; } else { configuredGeneration = env.CACHE_GENERATION; + configuredRestored = env.CACHE_RESTORED; } } const enabled = os !== "Windows" && mode !== "off" && flags.includes("true"); @@ -5761,9 +5773,11 @@ server.listen(0, "127.0.0.1", () => { "restore-keys": `${prefix}\n`, }); expect(configuredGeneration).toBe(generation); + expect(configuredRestored).toBe(generation === "a".repeat(64) ? "false" : "true"); } else { expect(cacheInputs).toBeUndefined(); expect(configuredGeneration).toBeUndefined(); + expect(configuredRestored).toBeUndefined(); } } } diff --git a/test/scripts/setup-node-env-transform-cache.test.ts b/test/scripts/setup-node-env-transform-cache.test.ts new file mode 100644 index 000000000000..70336489293a --- /dev/null +++ b/test/scripts/setup-node-env-transform-cache.test.ts @@ -0,0 +1,71 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { afterEach, expect, it } from "vitest"; +import { parse } from "yaml"; +import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; + +const tempDirs = useAutoCleanupTempDirTracker(afterEach); + +it.skipIf(process.platform === "win32")( + "reports archive restoration separately from local generation retention", + () => { + const action = parse(readFileSync(".github/actions/setup-node-env/action.yml", "utf8")); + const step = action.runs.steps.find( + (candidate: { name: string }) => candidate.name === "Configure Vitest transform cache", + ); + expect(step.env.CACHE_RESTORED).toBe( + "${{ steps.vitest-cache.outputs['cache-matched-key'] != '' && 'true' || 'false' }}", + ); + const generation = "a".repeat(64); + for (const scenario of [ + { restored: "true", marker: generation, entry: true, local: "retained" }, + { restored: "false", marker: generation, entry: true, local: "retained" }, + { restored: "true", marker: "b".repeat(64), entry: true, local: "cleared" }, + { restored: "true", marker: undefined, entry: true, local: "cleared" }, + { restored: "false", marker: undefined, entry: false, local: "empty" }, + ]) { + const root = tempDirs.make("openclaw-transform-cache-"); + const cache = path.join(root, "cache"); + const githubEnv = path.join(root, "env"); + const marker = path.join(cache, ".openclaw-transform-generation"); + const entry = path.join(cache, "entry"); + mkdirSync(cache); + if (scenario.marker !== undefined) { + writeFileSync(marker, scenario.marker); + } + if (scenario.entry) { + writeFileSync(entry, "cached transform"); + } + // Only relocate the fixed cache root; execute the shipped action body. + const script = step.run.replace( + "cache_root=/var/tmp/openclaw-vitest-fs-cache", + 'cache_root="$TEST_CACHE_ROOT"', + ); + const result = spawnSync("/bin/bash", ["-c", script], { + encoding: "utf8", + env: { + PATH: process.env.PATH, + TEST_CACHE_ROOT: cache, + GITHUB_ENV: githubEnv, + CACHE_GENERATION: generation, + CACHE_WRITER: step.env.CACHE_WRITER, + CACHE_RESTORED: scenario.restored, + CACHE_RUNNER_ENVIRONMENT: "github-hosted", + }, + }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain( + `Vitest transform cache: restored=${scenario.restored} local=${scenario.local} generation=${generation} runner=github-hosted`, + ); + expect(readFileSync(marker, "utf8")).toBe(`${generation}\n`); + expect(existsSync(entry)).toBe(scenario.local === "retained"); + if (scenario.local === "retained") { + expect(readFileSync(entry, "utf8")).toBe("cached transform"); + } + expect(readFileSync(githubEnv, "utf8")).toBe( + `OPENCLAW_VITEST_FS_MODULE_CACHE_ROOT=${cache}\nOPENCLAW_VITEST_FS_MODULE_CACHE_WRITER=0\n`, + ); + } + }, +);