diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8023fafd6155..8e389b524541 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -420,7 +420,18 @@ jobs: env: T3CODE_TRANSFER_BUDGET_REPORT_PATH: ${{ runner.temp }}/t3code-transfer-budget.md T3CODE_TRANSFER_BUDGET_RESULT_PATH: ${{ runner.temp }}/thread-transfer-result.json - run: vp run --filter t3 test --shard ${{ matrix.shard }}/${{ strategy.job-total }} + run: >- + vp run test:cached -- --shard ${{ matrix.shard }}/${{ strategy.job-total }} + --uncached apps/server/integration/transferBudgetV2.integration.test.ts + --verify-hits 1 --report "${{ runner.temp }}/test-cache-${{ matrix.shard }}.json" apps/server + + - name: Upload test cache measurements + if: always() && steps.changes.outputs.test_server != 'false' + uses: actions/upload-artifact@v7 + with: + name: test-cache-${{ matrix.shard }} + path: ${{ runner.temp }}/test-cache-${{ matrix.shard }}.json + if-no-files-found: error # integration/transferBudgetV2.integration.test.ts writes the report; exactly one shard # produces these files. Gating the upload on their presence keeps a diff --git a/package.json b/package.json index 4012a03c20d1..d785f794cda6 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "test": "vp run -r test", + "test:cached": "node scripts/test-cached.ts", "test:perf:v2-wire": "vp test run apps/server/src/orchestration-v2/WireProjection.test.ts apps/server/src/orchestration-v2/threadHistoryPaging.test.ts apps/server/src/orchestration-v2/ThreadStream.test.ts apps/server/src/orchestration-v2/ThreadTransportPerformance.test.ts apps/server/src/orchestration-v2/ShellStream.test.ts apps/server/src/orchestration-v2/CommandPolicy.test.ts apps/server/src/orchestration-v2/runtimeLayer.test.ts apps/server/src/ws.test.ts packages/contracts/src/orchestrationV2.test.ts packages/client-runtime/src/operations/commands.test.ts packages/client-runtime/src/operations/commands.performance.test.ts packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts packages/client-runtime/src/state/threadHistoryController.test.ts packages/client-runtime/src/state/threadHistoryMerge.test.ts packages/client-runtime/src/state/threads-sync.test.ts packages/client-runtime/src/state/shellReducer.test.ts", "test:resource-monitor": "cargo test --locked --manifest-path native/resource-monitor/Cargo.toml", "test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test", diff --git a/scripts/lib/cached-tests.test.ts b/scripts/lib/cached-tests.test.ts new file mode 100644 index 000000000000..7079006ddd05 --- /dev/null +++ b/scripts/lib/cached-tests.test.ts @@ -0,0 +1,140 @@ +// @effect-diagnostics nodeBuiltinImport:off - verifies native task caching in isolated subprocesses. +import { describe, expect, it } from "vite-plus/test"; +import * as NodeChildProcess from "node:child_process"; +import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import * as NodeURL from "node:url"; + +import { cachedTestTasks, matchesTestPath } from "./cached-tests.ts"; + +describe("cachedTestTasks", () => { + it("selects every test for a checkout-root path", () => { + const root = NodePath.resolve(import.meta.dirname, "../.."); + for (const path of [".", root]) { + expect( + matchesTestPath("apps/server/src/auth/utils.test.ts", [ + NodePath.relative(root, NodePath.resolve(root, path)), + ]), + ).toBe(true); + } + expect(matchesTestPath("apps/server/src/auth/utils.test.ts", ["apps/server"])).toBe(true); + expect(matchesTestPath("apps/server-other/a.test.ts", ["apps/server"])).toBe(false); + }); + it("keeps a file's cache task stable when unrelated tests are added", () => { + const first = cachedTestTasks(JSON.stringify({ files: ["src/b.test.ts"], groupSize: 1 })); + const second = cachedTestTasks( + JSON.stringify({ files: ["src/a.test.ts", "src/b.test.ts"], groupSize: 1 }), + ); + const key = Object.keys(first).find((key) => key !== "test-cache")!; + expect(second[key]).toEqual(first[key]); + }); + + it("keeps shell metacharacters in filenames out of the command", () => { + const tasks = cachedTestTasks( + JSON.stringify({ files: ["src/a'$(touch injected).test.ts"], groupSize: 1 }), + ); + const task = Object.entries(tasks).find(([key]) => key !== "test-cache")![1]; + expect(task.command).toBe("vp test run 'src/a'\"'\"'$(touch injected).test.ts'"); + }); + + it("rejects paths outside the checkout and empty groups", () => { + for (const request of [ + { files: ["../elsewhere.test.ts"], groupSize: 1 }, + { files: ["/absolute.test.ts"], groupSize: 1 }, + { files: ["src/a.test.ts"], groupSize: 0 }, + ]) { + expect(() => cachedTestTasks(JSON.stringify(request))).toThrow(); + } + }); + + it("accepts spec files discovered by Vitest", () => { + const tasks = cachedTestTasks(JSON.stringify({ files: ["src/example.spec.ts"], groupSize: 1 })); + expect( + Object.values(tasks).some((task) => task.command === "vp test run 'src/example.spec.ts'"), + ).toBe(true); + }); + + it("does not add tasks without an explicit request", () => { + expect(cachedTestTasks(undefined)).toEqual({}); + }); + + it("reuses individual results, invalidates filesystem inputs, and never caches failures", async () => { + const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "cached-tests-")); + const root = NodePath.resolve(import.meta.dirname, "../.."); + const run = (files = ["a.test.ts", "b.test.ts"], uncached: Array = []) => + new Promise((resolve, reject) => { + const child = NodeChildProcess.spawn( + process.execPath, + [NodePath.join(root, "node_modules/vite-plus/dist/bin.js"), "run", "test-cache"], + { + cwd: directory, + env: { + ...process.env, + NODE_OPTIONS: "", + VP_REMOTE_CACHE: "off", + T3_CACHED_TESTS: JSON.stringify({ files, groupSize: 1, uncached }), + VITE_CACHE_PATH: NodePath.join(directory, "cache"), + PATH: `${NodePath.join(root, "node_modules/.bin")}${NodePath.delimiter}${process.env.PATH}`, + }, + stdio: "ignore", + }, + ); + child.once("error", reject); + child.once("close", resolve); + }); + const summary = async () => { + const value: { tasks: Array<{ result: Record }> } = JSON.parse( + await NodeFSP.readFile(NodePath.join(directory, "cache/v18/last-summary.json"), "utf8"), + ); + return value.tasks.filter((task) => "CacheHit" in task.result).length; + }; + try { + await NodeFSP.mkdir(NodePath.join(directory, "node_modules")); + await NodeFSP.symlink( + NodePath.join(root, "node_modules/vite-plus"), + NodePath.join(directory, "node_modules/vite-plus"), + "dir", + ); + await NodeFSP.writeFile(NodePath.join(directory, "package.json"), '{"type":"module"}'); + await NodeFSP.writeFile( + NodePath.join(directory, "vite.config.ts"), + `import {cachedTestTasks} from ${JSON.stringify(NodeURL.pathToFileURL(NodePath.join(root, "scripts/lib/cached-tests.ts")).href)}; export default {run:{tasks:cachedTestTasks(process.env.T3_CACHED_TESTS, import.meta.dirname)},test:{include:["*.test.ts"]}};`, + ); + await NodeFSP.writeFile( + NodePath.join(directory, "a.test.ts"), + 'import {test,expect} from "vite-plus/test"; import {readFileSync} from "node:fs"; test("fixture",()=>expect(readFileSync("fixture.txt","utf8")).toMatch(/^hello/));', + ); + await NodeFSP.writeFile( + NodePath.join(directory, "b.test.ts"), + 'import {test,expect} from "vite-plus/test"; test("independent",()=>expect(1).toBe(1));', + ); + await NodeFSP.writeFile(NodePath.join(directory, "fixture.txt"), "hello"); + expect(await run()).toBe(0); + expect(await run()).toBe(0); + expect(await summary()).toBe(2); + expect(await run(["b.test.ts"])).toBe(0); + expect(await summary()).toBe(1); + expect(await run()).toBe(0); + expect(await summary()).toBe(2); + await NodeFSP.writeFile(NodePath.join(directory, "fixture.txt"), "hello changed"); + expect(await run()).toBe(0); + expect(await summary()).toBe(1); + await NodeFSP.writeFile(NodePath.join(directory, "fixture.txt"), "broken"); + expect(await run()).toBe(1); + expect(await run()).toBe(1); + expect(await summary()).toBe(1); + await NodeFSP.writeFile(NodePath.join(directory, "fixture.txt"), "hello again"); + await NodeFSP.writeFile( + NodePath.join(directory, "b.test.ts"), + 'import {test} from "vite-plus/test";import {writeFileSync} from "node:fs";test("report",()=>writeFileSync("report.json","{}"));', + ); + expect(await run(undefined, ["b.test.ts"])).toBe(0); + await NodeFSP.unlink(NodePath.join(directory, "report.json")); + expect(await run(undefined, ["b.test.ts"])).toBe(0); + expect(await NodeFSP.readFile(NodePath.join(directory, "report.json"), "utf8")).toBe("{}"); + } finally { + await NodeFSP.rm(directory, { recursive: true, force: true }); + } + }); +}); diff --git a/scripts/lib/cached-tests.ts b/scripts/lib/cached-tests.ts new file mode 100644 index 000000000000..a21d13bd83d8 --- /dev/null +++ b/scripts/lib/cached-tests.ts @@ -0,0 +1,118 @@ +// @effect-diagnostics nodeBuiltinImport:off - Vite loads this before dependency installation and service initialization. +import * as NodeCrypto from "node:crypto"; +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; + +interface CachedTestTask { + command: string; + cwd?: string; + dependsOn?: Array; + cache: + | false + | { + input: Array; + output: Array; + }; +} + +export function matchesTestPath(file: string, filters: Array) { + return ( + filters.length === 0 || + filters.some((filter) => filter === "" || file === filter || file.startsWith(`${filter}/`)) + ); +} + +export function cachedTestTasks( + raw: string | undefined, + root = NodePath.resolve(import.meta.dirname, "../.."), +): Record { + if (!raw) return {}; + const request: unknown = JSON.parse(raw); + if ( + typeof request !== "object" || + request === null || + !("files" in request) || + !Array.isArray(request.files) || + !("groupSize" in request) || + typeof request.groupSize !== "number" || + !Number.isSafeInteger(request.groupSize) || + request.groupSize < 1 + ) { + throw new Error("Invalid cached test request"); + } + const uncached = "uncached" in request ? request.uncached : []; + if (!Array.isArray(uncached) || !uncached.every((file) => typeof file === "string")) { + throw new Error("Invalid uncached test files"); + } + const files: Array = []; + for (const file of request.files) { + if ( + typeof file !== "string" || + NodePath.isAbsolute(file) || + file.split(/[\\/]/).includes("..") || + !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(file) + ) { + throw new Error("Cached tests must be test files inside the checkout"); + } + files.push(file); + } + const tasks: Record = {}; + const sorted = [...new Set(files)].sort(); + const packages = new Map>(); + for (const file of sorted) { + let directory = NodePath.dirname(NodePath.join(root, file)); + while (directory !== root && !NodeFS.existsSync(NodePath.join(directory, "package.json"))) { + directory = NodePath.dirname(directory); + } + const entries = packages.get(directory) ?? []; + entries.push(file); + packages.set(directory, entries); + } + for (const [directory, entries] of packages) { + const manifest: unknown = JSON.parse( + NodeFS.readFileSync(NodePath.join(directory, "package.json"), "utf8"), + ); + const command = + directory !== root && + typeof manifest === "object" && + manifest !== null && + "scripts" in manifest && + typeof manifest.scripts === "object" && + manifest.scripts !== null && + "test" in manifest.scripts && + typeof manifest.scripts.test === "string" + ? manifest.scripts.test + : "vp test run"; + if (!command.startsWith("vp test run")) { + throw new Error(`Unsupported test command in ${directory}`); + } + const cacheable = entries.filter((file) => !uncached.includes(file)); + const groups: Array> = []; + for (let index = 0; index < cacheable.length; index += request.groupSize) { + groups.push(cacheable.slice(index, index + request.groupSize)); + } + groups.push(...entries.filter((file) => uncached.includes(file)).map((file) => [file])); + for (const group of groups) { + const key = NodeCrypto.createHash("sha256").update(group.join("\0")).digest("hex"); + tasks[`test-cache:${key}`] = { + command: `${command} ${group.map((file) => `'${NodePath.relative(directory, NodePath.join(root, file)).replaceAll("'", "'\"'\"'")}'`).join(" ")}`, + ...(directory === root ? {} : { cwd: NodePath.relative(root, directory) }), + cache: uncached.includes(group[0]!) + ? false + : { + // Vitest reads and rewrites these caches while running otherwise unchanged tests. + input: [ + { auto: true }, + "!**/node_modules/.vite/**", + "!**/node_modules/.vitest-cache/**", + ], + output: [], + }, + }; + } + } + return { + ...tasks, + "test-cache": { command: 'node -e ""', dependsOn: Object.keys(tasks), cache: false }, + }; +} diff --git a/scripts/lib/sharded-tests.test.ts b/scripts/lib/sharded-tests.test.ts new file mode 100644 index 000000000000..d12e351087ca --- /dev/null +++ b/scripts/lib/sharded-tests.test.ts @@ -0,0 +1,55 @@ +// @effect-diagnostics nodeBuiltinImport:off - verifies Vitest discovery in a disposable project. +import { expect, it } from "vite-plus/test"; +import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import { shardedTestFiles } from "./sharded-tests.ts"; + +it("uses the configured sequencer and discovers every file exactly once", async () => { + const root = await NodeFSP.realpath( + await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "test-shards-")), + ); + try { + await NodeFSP.mkdir(NodePath.join(root, "node_modules")); + await NodeFSP.symlink( + NodePath.resolve(import.meta.dirname, "../../node_modules/vite-plus"), + NodePath.join(root, "node_modules/vite-plus"), + "dir", + ); + await NodeFSP.writeFile(NodePath.join(root, "package.json"), '{"type":"module"}'); + await NodeFSP.writeFile( + NodePath.join(root, "vite.config.ts"), + `import {BaseSequencer} from "vite-plus/test/node"; + class Sequencer extends BaseSequencer { + async shard(files) { + const {index,count}=this.ctx.config.shard; + return files.sort((a,b)=>a.moduleId.localeCompare(b.moduleId)).filter((_,i)=>i%count===index-1); + } + } + export default {test:{include:["included/*.{test,spec}.ts"],sequence:{sequencer:Sequencer}}};`, + ); + await NodeFSP.mkdir(NodePath.join(root, "included")); + for (const file of [ + "included/a.test.ts", + "included/b.test.ts", + "included/c.test.ts", + "included/d.spec.ts", + "excluded.test.ts", + ]) { + await NodeFSP.writeFile(NodePath.join(root, file), "export {};"); + } + const first = await shardedTestFiles(root, "1/2"); + const second = await shardedTestFiles(root, "2/2"); + expect(first).toEqual( + ["a", "c"].map((file) => NodePath.join(root, `included/${file}.test.ts`)), + ); + expect(second).toEqual([ + NodePath.join(root, "included/b.test.ts"), + NodePath.join(root, "included/d.spec.ts"), + ]); + expect(new Set([...first, ...second]).size).toBe(4); + await expect(shardedTestFiles(root, "0/2")).rejects.toThrow("Shard must be"); + } finally { + await NodeFSP.rm(root, { recursive: true, force: true }); + } +}); diff --git a/scripts/lib/sharded-tests.ts b/scripts/lib/sharded-tests.ts new file mode 100644 index 000000000000..5ab2aba2709b --- /dev/null +++ b/scripts/lib/sharded-tests.ts @@ -0,0 +1,21 @@ +// @effect-diagnostics nodeBuiltinImport:off - Vitest discovery runs outside the application runtime. +import * as NodePath from "node:path"; +import { createVitest } from "vite-plus/test/node"; + +export async function shardedTestFiles(root: string, shard: string) { + const match = /^(\d+)\/(\d+)$/.exec(shard); + const index = Number(match?.[1]); + const count = Number(match?.[2]); + if (!Number.isSafeInteger(index) || !Number.isSafeInteger(count) || index < 1 || index > count) { + throw new Error("Shard must be index/count with 1 <= index <= count"); + } + const context = await createVitest("test", { root, watch: false, shard }); + try { + const files = await context.globTestSpecifications(); + const sequencer = new context.config.sequence.sequencer(context); + const selected = await sequencer.shard(files); + return selected.map((file) => NodePath.resolve(file.moduleId)); + } finally { + await context.close(); + } +} diff --git a/scripts/test-cached.ts b/scripts/test-cached.ts new file mode 100644 index 000000000000..a03953fde4ca --- /dev/null +++ b/scripts/test-cached.ts @@ -0,0 +1,159 @@ +// @effect-diagnostics nodeBuiltinImport:off globalConsole:off +import * as NodeCrypto from "node:crypto"; +import * as NodeChildProcess from "node:child_process"; +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; +import * as NodeUtil from "node:util"; + +import { matchesTestPath } from "./lib/cached-tests.ts"; +import { shardedTestFiles } from "./lib/sharded-tests.ts"; + +const root = NodePath.resolve(import.meta.dirname, ".."); +const { values, positionals } = NodeUtil.parseArgs({ + args: process.argv[2] === "--" ? process.argv.slice(3) : process.argv.slice(2), + allowPositionals: true, + options: { + "group-size": { type: "string", default: "1" }, + concurrency: { type: "string", default: "1" }, + shard: { type: "string" }, + report: { type: "string" }, + "verify-hits": { type: "string", default: "0" }, + uncached: { type: "string", multiple: true, default: [] }, + help: { type: "boolean", short: "h" }, + }, +}); +if (values.help) { + console.log( + "vp run test:cached [--group-size 1] [--concurrency 1] [--shard index/count] [--uncached file] [test files or directories]", + ); + process.exit(0); +} +const groupSize = Number(values["group-size"]); +const concurrency = Number(values.concurrency); +const verifyHits = Number(values["verify-hits"]); +if (!Number.isSafeInteger(verifyHits) || verifyHits < 0) + throw new Error("Invalid verification count"); +if (![groupSize, concurrency].every((value) => Number.isSafeInteger(value) && value > 0)) { + throw new Error("Group size and concurrency must be positive integers"); +} +if (values.shard && positionals.length !== 1) { + throw new Error("Sharding requires exactly one test project directory"); +} +const candidates = values.shard + ? (await shardedTestFiles(NodePath.resolve(positionals[0]!), values.shard)).map((file) => + NodePath.relative(root, file), + ) + : NodeChildProcess.execFileSync( + "git", + ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], + { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }, + ).split("\0"); +const filters = positionals.map((path) => NodePath.relative(root, NodePath.resolve(path))); +const isSelectable = (file: string) => + /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(file) && + !file.startsWith(".repos/") && + NodeFS.existsSync(NodePath.join(root, file)) && + matchesTestPath(file, filters); +if (values.shard) { + const rejected = candidates.filter((file) => !isSelectable(file)); + if (rejected.length) + throw new Error(`Discovered tests cannot be selected: ${rejected.join(", ")}`); +} +const files = [...new Set(candidates)].filter(isSelectable); +if (files.length === 0) { + console.error("No test files matched"); + process.exit(values.shard ? 0 : 1); +} +console.log( + `Cached tests: ${files.length} files, ${groupSize} per task, concurrency ${concurrency}`, +); +const env = { + ...process.env, + T3_CACHED_TESTS: JSON.stringify({ + files, + groupSize, + uncached: values.uncached.map((file) => NodePath.relative(root, NodePath.resolve(file))), + }), +}; +const started = performance.now(); +const result = NodeChildProcess.spawnSync( + NodePath.join(root, "node_modules/.bin/vp"), + ["run", "--concurrency-limit", String(concurrency), "test-cache"], + { + cwd: root, + env, + stdio: "inherit", + }, +); +if (result.error) throw result.error; +const cacheRunMs = performance.now() - started; +process.exitCode = result.status ?? 1; +if (values.report || verifyHits > 0) { + const cacheRoot = + process.env.VITE_CACHE_PATH || NodePath.join(root, "node_modules/.vite/task-cache"); + const summaryFiles = NodeFS.readdirSync(cacheRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => NodePath.join(cacheRoot, entry.name, "last-summary.json")) + .filter((file) => NodeFS.existsSync(file)) + .sort((a, b) => NodeFS.statSync(b).mtimeMs - NodeFS.statSync(a).mtimeMs); + const summary: { + tasks: Array<{ + task_name: string; + result: { CacheHit?: { source: string; saved_duration_ms: number } }; + }>; + } = JSON.parse(NodeFS.readFileSync(summaryFiles[0]!, "utf8")); + const rank = (task: string) => + NodeCrypto.createHash("sha256") + .update( + `${process.env.GITHUB_RUN_ID || "local"}:${process.env.GITHUB_RUN_ATTEMPT || "1"}:${task}`, + ) + .digest("hex"); + const hits = summary.tasks.filter((task) => task.result.CacheHit); + const verify = hits + .toSorted((a, b) => rank(a.task_name).localeCompare(rank(b.task_name))) + .slice(0, verifyHits); + const verification: Array<{ task: string; exitCode: number; durationMs: number }> = []; + if (result.status === 0) { + for (const task of verify) { + console.log(`Verifying cached result without cache: ${task.task_name}`); + const start = performance.now(); + const audit = NodeChildProcess.spawnSync( + NodePath.join(root, "node_modules/.bin/vp"), + ["run", "--no-cache", "--remote-cache", "off", task.task_name], + { cwd: root, env, stdio: "inherit" }, + ); + if (audit.error) throw audit.error; + verification.push({ + task: task.task_name, + exitCode: audit.status ?? 1, + durationMs: performance.now() - start, + }); + if (audit.status !== 0) process.exitCode = audit.status ?? 1; + } + } + const report = { + shard: values.shard, + files, + cacheRunMs, + exitCode: result.status, + summary, + verification, + }; + if (values.report) { + NodeFS.mkdirSync(NodePath.dirname(values.report), { recursive: true }); + NodeFS.writeFileSync(values.report, `${JSON.stringify(report, null, 2)}\n`); + } + if (process.env.GITHUB_STEP_SUMMARY) { + const remote = hits.filter((task) => task.result.CacheHit?.source === "Remote").length; + const savedMs = hits.reduce( + (total, task) => total + (task.result.CacheHit?.saved_duration_ms ?? 0), + 0, + ); + NodeFS.appendFileSync( + process.env.GITHUB_STEP_SUMMARY, + `### Server test cache (${values.shard || "unsharded"})\n\n` + + `${files.length} files selected; ${hits.length} hits (${remote} remote); ${(savedMs / 1000).toFixed(1)}s task runtime avoided; ${(cacheRunMs / 1000).toFixed(1)}s elapsed.\n\n` + + `${verification.length} cached results re-executed without cache; ${verification.filter((audit) => audit.exitCode !== 0).length} verification failures.\n`, + ); + } +} diff --git a/vite.config.ts b/vite.config.ts index 63dd5266bf86..875cb2cbe394 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import "vite-plus/test/config"; import { defineConfig } from "vite-plus"; import * as NodeURL from "node:url"; import { remoteCacheUrl } from "./infra/task-cache/src/remoteCache.ts"; +import { cachedTestTasks } from "./scripts/lib/cached-tests.ts"; const DOM_TEST_ENVIRONMENT_ALLOWLIST: Array = []; @@ -89,6 +90,7 @@ const taskCacheConfig = { export default defineConfig({ run: { cache: taskCacheConfig, + tasks: cachedTestTasks(process.env.T3_CACHED_TESTS), }, resolve: { alias: {