diff --git a/package.json b/package.json index 6f829ef0fe..b43df4efa5 100644 --- a/package.json +++ b/package.json @@ -329,7 +329,7 @@ "@types/pngjs": "^6.0.5", "@types/tar-stream": "^3.1.4", "@types/yauzl": "^3.4.0", - "@vitest/coverage-v8": "4.1.11", + "@vitest/coverage-v8": "5.0.4", "ai": "^7.0.68", "eslint-plugin-no-only-tests": "^3.4.0", "eslint-plugin-perfectionist": "^5.10.1", @@ -347,7 +347,7 @@ "typescript": "^7.0.2", "undici": "7.29.1", "vite": "^8.2.1", - "vitest": "^4.1.11", + "vitest": "5.0.4", "yaml": "^2.9.0", "yauzl": "^3.4.0", "@agent-device/testmu": "workspace:*" diff --git a/packages/host-kit/src/internal/exec.test.ts b/packages/host-kit/src/internal/exec.test.ts index 1690783164..680639fa25 100644 --- a/packages/host-kit/src/internal/exec.test.ts +++ b/packages/host-kit/src/internal/exec.test.ts @@ -103,34 +103,38 @@ test('runCmd writes stdin through pipeline', async () => { assert.equal(result.stdout, String(stdin.length)); }); -test.sequential('runCmdBackground emits bounded exec_command diagnostics when AGENT_DEVICE_EXEC_TRACE is enabled', async () => { - const diagnosticsPath = await withExecTraceEnv( - async () => - await withDiagnosticsScope( - { - session: 'exec-trace', - requestId: 'exec-trace-1', - command: 'background', - }, - async () => { - const { wait } = runCmdBackground(process.execPath, [ - '-e', - 'process.stdout.write("ok")', - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - ]); - await wait; - return flushDiagnosticsToSessionFile()?.path ?? null; - }, - ), - ); +test( + 'runCmdBackground emits bounded exec_command diagnostics when AGENT_DEVICE_EXEC_TRACE is enabled', + { concurrent: false }, + async () => { + const diagnosticsPath = await withExecTraceEnv( + async () => + await withDiagnosticsScope( + { + session: 'exec-trace', + requestId: 'exec-trace-1', + command: 'background', + }, + async () => { + const { wait } = runCmdBackground(process.execPath, [ + '-e', + 'process.stdout.write("ok")', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + ]); + await wait; + return flushDiagnosticsToSessionFile()?.path ?? null; + }, + ), + ); - assertBackgroundExecTraceEvents(readExecDiagnosticEvents(diagnosticsPath)); -}); + assertBackgroundExecTraceEvents(readExecDiagnosticEvents(diagnosticsPath)); + }, +); test('runCmdBackground can leave output streams to the caller', async () => { const { child, wait } = runCmdBackground( @@ -157,7 +161,7 @@ test('runCmdBackground can leave output streams to the caller', async () => { assert.equal(stderr, 'err'); }); -test.sequential('runCmd stays silent when exec tracing is not enabled', async () => { +test('runCmd stays silent when exec tracing is not enabled', { concurrent: false }, async () => { const previousTraceEnv = process.env.AGENT_DEVICE_EXEC_TRACE; delete process.env.AGENT_DEVICE_EXEC_TRACE; @@ -281,21 +285,25 @@ test('whichCmd rejects suspicious command strings', async () => { assert.equal(await whichCmd('./node'), false); }); -test.sequential('whichCmd ignores directories that match a command name in PATH', async () => { - const root = mkdtempForTestSync('agent-device-whichcmd-'); - const fakeCommandDir = path.join(root, 'fake-tool'); - fs.mkdirSync(fakeCommandDir); +test( + 'whichCmd ignores directories that match a command name in PATH', + { concurrent: false }, + async () => { + const root = mkdtempForTestSync('agent-device-whichcmd-'); + const fakeCommandDir = path.join(root, 'fake-tool'); + fs.mkdirSync(fakeCommandDir); - const previousPath = process.env.PATH; - process.env.PATH = `${root}${path.delimiter}${previousPath ?? ''}`; + const previousPath = process.env.PATH; + process.env.PATH = `${root}${path.delimiter}${previousPath ?? ''}`; - try { - assert.equal(await whichCmd('fake-tool'), false); - } finally { - process.env.PATH = previousPath; - fs.rmSync(root, { recursive: true, force: true }); - } -}); + try { + assert.equal(await whichCmd('fake-tool'), false); + } finally { + process.env.PATH = previousPath; + fs.rmSync(root, { recursive: true, force: true }); + } + }, +); function readExecDiagnosticEvents(diagnosticsPath: string | null): Array<{ level?: string; @@ -542,55 +550,63 @@ test('an allowFailure xcrun result guarded by requireExecSuccess gets the toolch ); }); -test.sequential('without a developer dir the toolchain hint points at xcode-select', () => { - const saved = process.env.DEVELOPER_DIR; - delete process.env.DEVELOPER_DIR; - try { - // No cmd/args: the call site only had the result, so xcrun's own stderr identifies the tool. - assert.throws( - () => requireExecSuccess(XCRUN_DEVICECTL_NOT_FOUND, 'Failed to list devices'), - (error: unknown) => { - assert.ok(error instanceof AppError); - assert.equal(error.details?.developerDir, null); - assert.match( - String(error.details?.hint), - /devicectl in the developer dir selected by xcode-select/, - ); - return true; - }, - ); - } finally { - if (saved !== undefined) process.env.DEVELOPER_DIR = saved; - } -}); +test( + 'without a developer dir the toolchain hint points at xcode-select', + { concurrent: false }, + () => { + const saved = process.env.DEVELOPER_DIR; + delete process.env.DEVELOPER_DIR; + try { + // No cmd/args: the call site only had the result, so xcrun's own stderr identifies the tool. + assert.throws( + () => requireExecSuccess(XCRUN_DEVICECTL_NOT_FOUND, 'Failed to list devices'), + (error: unknown) => { + assert.ok(error instanceof AppError); + assert.equal(error.details?.developerDir, null); + assert.match( + String(error.details?.hint), + /devicectl in the developer dir selected by xcode-select/, + ); + return true; + }, + ); + } finally { + if (saved !== undefined) process.env.DEVELOPER_DIR = saved; + } + }, +); -test.sequential('a request that clears DEVELOPER_DIR is not reported under the daemon one', async () => { - const saved = process.env.DEVELOPER_DIR; - process.env.DEVELOPER_DIR = '/daemon/Developer'; - try { - // The child runs without a usable DEVELOPER_DIR (xcode-select decides), so the key and report - // must too, whether the request clears it or carries it unset. - for (const requestEnv of [{ DEVELOPER_DIR: '' }, { DEVELOPER_DIR: undefined }]) { - await withRequestCommandEnv(requestEnv, async () => { - assert.equal((await runCmd(process.execPath, PRINT_DEVELOPER_DIR)).stdout, ''); - assert.equal(commandDeveloperDir(), undefined); - assert.throws( - () => requireExecSuccess(XCRUN_DEVICECTL_NOT_FOUND, 'Failed to list devices'), - (error: unknown) => { - assert.ok(error instanceof AppError); - assert.equal(error.details?.developerDir, null); - assert.match(String(error.details?.hint), /selected by xcode-select/); - return true; - }, - ); - }); +test( + 'a request that clears DEVELOPER_DIR is not reported under the daemon one', + { concurrent: false }, + async () => { + const saved = process.env.DEVELOPER_DIR; + process.env.DEVELOPER_DIR = '/daemon/Developer'; + try { + // The child runs without a usable DEVELOPER_DIR (xcode-select decides), so the key and report + // must too, whether the request clears it or carries it unset. + for (const requestEnv of [{ DEVELOPER_DIR: '' }, { DEVELOPER_DIR: undefined }]) { + await withRequestCommandEnv(requestEnv, async () => { + assert.equal((await runCmd(process.execPath, PRINT_DEVELOPER_DIR)).stdout, ''); + assert.equal(commandDeveloperDir(), undefined); + assert.throws( + () => requireExecSuccess(XCRUN_DEVICECTL_NOT_FOUND, 'Failed to list devices'), + (error: unknown) => { + assert.ok(error instanceof AppError); + assert.equal(error.details?.developerDir, null); + assert.match(String(error.details?.hint), /selected by xcode-select/); + return true; + }, + ); + }); + } + assert.equal(commandDeveloperDir(), '/daemon/Developer'); + } finally { + if (saved === undefined) delete process.env.DEVELOPER_DIR; + else process.env.DEVELOPER_DIR = saved; } - assert.equal(commandDeveloperDir(), '/daemon/Developer'); - } finally { - if (saved === undefined) delete process.env.DEVELOPER_DIR; - else process.env.DEVELOPER_DIR = saved; - } -}); + }, +); test('a non-xcrun failure keeps the caller hint', () => { assert.throws( diff --git a/packages/kernel/src/ttl-memo.test.ts b/packages/kernel/src/ttl-memo.test.ts index 8b889676c8..e4829adc9f 100644 --- a/packages/kernel/src/ttl-memo.test.ts +++ b/packages/kernel/src/ttl-memo.test.ts @@ -4,13 +4,13 @@ import { createTtlMemo } from './ttl-memo.ts'; const crossTestMemo = createTtlMemo({ ttlMs: 60_000 }); -test.sequential('process memo setup clears values after each test: seed', () => { +test('process memo setup clears values after each test: seed', { concurrent: false }, () => { crossTestMemo.set('key', 'value'); expect(crossTestMemo.get('key')).toBe('value'); }); -test.sequential('process memo setup clears values after each test: verify', () => { +test('process memo setup clears values after each test: verify', { concurrent: false }, () => { expect(crossTestMemo.get('key')).toBeUndefined(); }); diff --git a/packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts b/packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts index 1991ff970a..472ec19ff1 100644 --- a/packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts +++ b/packages/platform-apple/src/core/__tests__/simulator-booted-memo.test.ts @@ -78,16 +78,24 @@ test('booted memo is scoped per simulator set path', async () => { expect(countSimctlListCalls()).toBe(2); }); -test.sequential('process memo test setup clears simulator boot memo: seed', async () => { - markSimulatorBooted(simulator); - - await ensureBootedSimulator(simulator); - - expect(countSimctlListCalls()).toBe(0); -}); - -test.sequential('process memo test setup clears simulator boot memo: verify', async () => { - await ensureBootedSimulator(simulator); - - expect(countSimctlListCalls()).toBe(1); -}); +test( + 'process memo test setup clears simulator boot memo: seed', + { concurrent: false }, + async () => { + markSimulatorBooted(simulator); + + await ensureBootedSimulator(simulator); + + expect(countSimctlListCalls()).toBe(0); + }, +); + +test( + 'process memo test setup clears simulator boot memo: verify', + { concurrent: false }, + async () => { + await ensureBootedSimulator(simulator); + + expect(countSimctlListCalls()).toBe(1); + }, +); diff --git a/packages/test-kit/src/legacy-daemon-fixture.ts b/packages/test-kit/src/legacy-daemon-fixture.ts index 47b8474b60..8e2d526096 100644 --- a/packages/test-kit/src/legacy-daemon-fixture.ts +++ b/packages/test-kit/src/legacy-daemon-fixture.ts @@ -73,7 +73,14 @@ const acquired = acquireDaemonLock(paths.baseDir, paths.lockPath, { }); fs.writeFileSync(paths.baseDir + '/legacy-disposition.json', JSON.stringify({ acquired })); if (!acquired) process.exit(0); -fs.writeFileSync(paths.infoPath, JSON.stringify({ ...identity, port: 4210, token: 'legacy-token', version: '0.21.20' })); +const registration = JSON.stringify({ ...identity, port: 4210, token: 'legacy-token', version: '0.21.20' }); +// Published through a temp file like the real owner does: callers wait on +// fs.existsSync(paths.infoPath) as the signal that registration completed, and a direct +// writeFileSync creates the file before its bytes exist, so that wait can return an +// empty registration. Renaming last also means the lock file is fully written by then. +const registrationTemp = paths.infoPath + '.' + process.pid + '.tmp'; +fs.writeFileSync(registrationTemp, registration); +fs.renameSync(registrationTemp, paths.infoPath); process.on('SIGTERM', () => { releaseDaemonLock(paths.lockPath); process.exit(0); }); setInterval(() => {}, 1000); `, diff --git a/patches/@vitest__coverage-v8@4.1.11.patch b/patches/@vitest__coverage-v8@4.1.11.patch deleted file mode 100644 index 4b40830231..0000000000 --- a/patches/@vitest__coverage-v8@4.1.11.patch +++ /dev/null @@ -1,196 +0,0 @@ -diff --git a/dist/index.js b/dist/index.js -index 0f623c0606ebd70526656f9571c998cac69d3317..5cf50ac3cf31cb61751210d7c6903def9f49c004 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url'; - import { provider } from 'std-env'; - import { l as loadProvider } from './load-provider-CdgAx3rL.js'; - import { n as normalize } from './pathe.M-eThtNZ-BTaAGrLg.js'; -+import { UNKNOWN_WRAPPER_LENGTH } from './wrapper-lengths.js'; - - const session = new inspector.Session(); - let enabled = false; -@@ -25,12 +26,19 @@ const mod = { - } - const coverage = await session.post("Profiler.takePreciseCoverage"); - const result = []; -+ const moduleExecutionInfo = options?.moduleExecutionInfo; - // Reduce amount of data sent over rpc by doing some early result filtering - for (const entry of coverage.result) { - if (filterResult(entry)) { -+ const module = moduleExecutionInfo?.get(normalize(fileURLToPath(entry.url))); - result.push({ - ...entry, -- startOffset: options?.moduleExecutionInfo?.get(normalize(fileURLToPath(entry.url)))?.startOffset || 0 -+ // Length of the wrapper the evaluator ran this script behind. The evaluator -+ // writes its entry in a `finally` after awaiting the module body, so a -+ // dynamic import still in flight at this moment has none, and this script's -+ // offsets then have no known origin. `0` is a real length (an untransformed -+ // script has no wrapper), so it cannot also mean "unknown". -+ startOffset: module ? module.startOffset : moduleExecutionInfo ? UNKNOWN_WRAPPER_LENGTH : void 0 - }); - } - } -diff --git a/dist/provider.js b/dist/provider.js -index f01c3c84a91f4ebf699ba61b39c575f225a3afb0..ae6df02bcd5c9fe959ff3778e50c299d5883f682 100644 ---- a/dist/provider.js -+++ b/dist/provider.js -@@ -11,6 +11,7 @@ import { provider } from 'std-env'; - import c from 'tinyrainbow'; - import { BaseCoverageProvider, parseAstAsync } from 'vitest/node'; - import { n as normalize } from './pathe.M-eThtNZ-BTaAGrLg.js'; -+import { UNKNOWN_WRAPPER_LENGTH, WrapperLengths, unmappableCoverageRecord } from './wrapper-lengths.js'; - - var version = "4.1.11"; - -@@ -29,23 +30,24 @@ class V8CoverageProvider extends BaseCoverageProvider { - const start = debug.enabled ? performance.now() : 0; - const coverageMap = this.createCoverageMap(); - let merged = { result: [] }; -+ // `mergeProcessCovs` rebuilds each script as { scriptId, url, functions }, so the -+ // wrapper length is gone after the first merge and has to come from here. Re-deriving -+ // it from the file just read makes the length a lottery over read order, because every -+ // file that does not contain the module contributes its fallback of 0. -+ const wrapperLengths = new WrapperLengths(); - await this.readCoverageFiles({ - onFileRead(coverage) { -+ for (const script of coverage.result) wrapperLengths.add(script.url, script.startOffset); - merged = mergeProcessCovs([merged, coverage]); -- // mergeProcessCovs sometimes loses startOffset, e.g. in vue -- merged.result.forEach((result) => { -- if (!result.startOffset) { -- const original = coverage.result.find((r) => r.url === result.url); -- result.startOffset = original?.startOffset || 0; -- } -- }); - }, - onFinished: async (project, environment) => { -+ for (const result of merged.result) result.startOffset = wrapperLengths.resolve(result.url); - // Source maps can change based on projectName and transform mode. - // Coverage transform re-uses source maps so we need to separate transforms from each other. - const converted = await this.convertCoverage(merged, project, environment); - coverageMap.merge(converted); - merged = { result: [] }; -+ wrapperLengths.clear(); - }, - onDebug: debug - }); -@@ -128,6 +130,12 @@ class V8CoverageProvider extends BaseCoverageProvider { - return coverageMap; - } - async remapCoverage(filename, wrapperLength, result, functions) { -+ if (wrapperLength === UNKNOWN_WRAPPER_LENGTH) { -+ this.ctx.logger.warn( -+ `Could not measure the wrapper length of ${filename}, so its block offsets cannot be mapped to source lines. Its record is marked invalid instead of guessed.`, -+ ); -+ return unmappableCoverageRecord(filename, result.map); -+ } - let ast; - try { - ast = await parseAstAsync(result.code); -diff --git a/dist/wrapper-lengths.js b/dist/wrapper-lengths.js -new file mode 100644 -index 0000000000000000000000000000000000000000..1856ce238c60d46780c8beb6dd6b7833dc660706 ---- /dev/null -+++ b/dist/wrapper-lengths.js -@@ -0,0 +1,100 @@ -+import { dirname, resolve } from 'node:path'; -+import { fileURLToPath } from 'node:url'; -+ -+/** -+ * Wrapper length of a transformed module. -+ * -+ * The module evaluator runs a transformed module as ``, -+ * and V8 reports block offsets against the whole script, so turning a module's -+ * coverage back into source coordinates needs the length of that wrapper. The -+ * evaluator records it per module only after the module body has been awaited, so -+ * a dynamic import still in flight when the worker takes its coverage has no entry -+ * and therefore no known length. `0` is itself a real length (an untransformed -+ * script has no wrapper), so it cannot double as "unknown": the worker reports -+ * this sentinel instead, and the merge below resolves the module from the workers -+ * that did measure a wrapper. -+ */ -+const UNKNOWN_WRAPPER_LENGTH = -1; -+ -+/** -+ * Collects the wrapper length of every script URL in one project/environment group -+ * while its coverage files are read, so the length stays a fact about the module -+ * instead of a fact about the last coverage file that happened to be read. -+ */ -+class WrapperLengths { -+ /** url -> the single length its workers measured, `null` once two differed */ -+ lengths = /* @__PURE__ */ new Map(); -+ /** urls a worker explicitly reported as unmeasured */ -+ unmeasured = /* @__PURE__ */ new Set(); -+ add(url, startOffset) { -+ if (startOffset === void 0) { -+ // A provider that does not report wrapper lengths at all (browser mode runs -+ // modules unwrapped in the page, so its scripts have no entry to carry). -+ return; -+ } -+ if (startOffset === UNKNOWN_WRAPPER_LENGTH) { -+ this.unmeasured.add(url); -+ return; -+ } -+ const known = this.lengths.get(url); -+ if (known === void 0) this.lengths.set(url, startOffset); -+ else if (known !== startOffset) this.lengths.set(url, null); -+ } -+ /** -+ * The length to convert `url` with, or `UNKNOWN_WRAPPER_LENGTH` when the module's -+ * offsets cannot be placed in its own source: no worker measured the wrapper, or -+ * workers measured different ones. Guessing `0` there would shift every offset by -+ * the wrapper and report plausible-looking counts for the wrong statements. -+ */ -+ resolve(url) { -+ const known = this.lengths.get(url); -+ if (known !== void 0) return known === null ? UNKNOWN_WRAPPER_LENGTH : known; -+ return this.unmeasured.has(url) ? UNKNOWN_WRAPPER_LENGTH : 0; -+ } -+ clear() { -+ this.lengths.clear(); -+ this.unmeasured.clear(); -+ } -+} -+ -+/** -+ * The paths `ast-v8-to-istanbul` keys its output under for `url`: the mapped -+ * sources when the transform carried a source map (the provider resolves them to -+ * absolute `file://` hrefs before converting), the script itself otherwise. -+ */ -+function reportPaths(url, sourceMap) { -+ const sources = sourceMap?.sources; -+ if (!sources?.length) return [fileURLToPath(url)]; -+ const directory = dirname(fileURLToPath(url)); -+ return sources.map((source) => -+ source.startsWith('file://') ? fileURLToPath(source) : resolve(directory, source), -+ ); -+} -+ -+/** -+ * Istanbul records for a module whose coverage could not be placed in its source. -+ * -+ * A record with no counters would merge as 0% coverage and score as though the -+ * file were untested, so each record carries one impossible hit count instead: -+ * `null` survives merging with a measured record for the same file (istanbul adds -+ * counts only when both sides are numbers), so the file cannot be repaired into -+ * looking valid, and consumers that validate counts drop the record and report the -+ * file as unscored rather than scoring it from shifted offsets. The statement sits -+ * past any real line so the marker never lands on a line the file really has. -+ */ -+function unmappableCoverageRecord(url, sourceMap) { -+ const line = Number.MAX_SAFE_INTEGER; -+ const location = { line, column: 0 }; -+ const record = (path) => ({ -+ path, -+ statementMap: { 0: { start: location, end: { line, column: location.column + 1 } } }, -+ fnMap: {}, -+ branchMap: {}, -+ s: { 0: null }, -+ f: {}, -+ b: {}, -+ }); -+ return Object.fromEntries(reportPaths(url, sourceMap).map((path) => [path, record(path)])); -+} -+ -+export { UNKNOWN_WRAPPER_LENGTH, WrapperLengths, unmappableCoverageRecord }; diff --git a/patches/ast-v8-to-istanbul@1.0.5.patch b/patches/ast-v8-to-istanbul@1.0.7.patch similarity index 88% rename from patches/ast-v8-to-istanbul@1.0.5.patch rename to patches/ast-v8-to-istanbul@1.0.7.patch index 9f3b27c845..68f0816780 100644 --- a/patches/ast-v8-to-istanbul@1.0.5.patch +++ b/patches/ast-v8-to-istanbul@1.0.7.patch @@ -1,8 +1,8 @@ diff --git a/dist/index.mjs b/dist/index.mjs -index ebe37dd027ab90208d32131aa5e114ce9c9124b0..28b2332fd37e5bd6d08cf1e385804f8ff7bd076f 100644 +index 1c85b95f5e48e6e47ee8bc720133ec162b1088bb..3924e7a06a2bf7a317ccdb2b4d742fae802e3c14 100644 --- a/dist/index.mjs +++ b/dist/index.mjs -@@ -526,6 +526,22 @@ var y = class CoverageMapper { +@@ -528,6 +528,22 @@ var b = class CoverageMapper { let i = [], a = []; for (let e of n) { if (!e) { @@ -25,7 +25,7 @@ index ebe37dd027ab90208d32131aa5e114ce9c9124b0..28b2332fd37e5bd6d08cf1e385804f8f i.push({ start: { line: void 0, -@@ -536,10 +552,6 @@ var y = class CoverageMapper { +@@ -538,10 +554,6 @@ var b = class CoverageMapper { column: void 0 } }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 437fec45a2..62654b0d05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,7 @@ overrides: react-router-dom: 7.18.2 patchedDependencies: - '@vitest/coverage-v8@4.1.11': 9c8a14435f152f76c2092d5212a3a200e7e48efd93d48eddea8b788d95247dd1 - ast-v8-to-istanbul@1.0.5: 1033b65c551f1e34f0852288a94128e9f39f596fccdd9aff984f26f0486ab050 + ast-v8-to-istanbul@1.0.7: 8f07585d066beb92a665ea78ec8bef87af3af32de9076c9e23122fbabd9fb475 importers: @@ -130,7 +129,7 @@ importers: version: 10.0.0 '@stryker-mutator/vitest-runner': specifier: 10.0.0 - version: 10.0.0(@stryker-mutator/core@10.0.0(@types/node@22.20.1))(vitest@4.1.11) + version: 10.0.0(@stryker-mutator/core@10.0.0(@types/node@22.20.1))(vitest@5.0.4) '@types/json-schema': specifier: ^7.0.15 version: 7.0.15 @@ -147,8 +146,8 @@ importers: specifier: ^3.4.0 version: 3.4.0 '@vitest/coverage-v8': - specifier: 4.1.11 - version: 4.1.11(patch_hash=9c8a14435f152f76c2092d5212a3a200e7e48efd93d48eddea8b788d95247dd1)(vitest@4.1.11) + specifier: 5.0.4 + version: 5.0.4(vitest@5.0.4) ai: specifier: ^7.0.68 version: 7.0.68(zod@4.4.3) @@ -201,8 +200,8 @@ importers: specifier: ^8.2.1 version: 8.2.1(@types/node@22.20.1)(yaml@2.9.0) vitest: - specifier: ^4.1.11 - version: 4.1.11(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)) + specifier: 5.0.4 + version: 5.0.4(@types/node@22.20.1)(@vitest/coverage-v8@5.0.4)(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)) yaml: specifier: ^2.9.0 version: 2.9.0 @@ -1178,6 +1177,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -2186,22 +2188,27 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} - '@vitest/coverage-v8@4.1.11': - resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} + '@vitest/coverage-v8@5.0.4': + resolution: {integrity: sha512-P5YcQY8Y6RmoE9xRXI38rT8pssbcubEzqtYTM2XQoxOXMDn04nRaTI1SM8qCl9/NfW3ag9GlzdSXu7IwO5Ft8A==} peerDependencies: - '@vitest/browser': 4.1.11 - vitest: 4.1.11 + '@vitest/browser': 5.0.4 + vitest: 5.0.4 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + '@vitest/istanbul-lib-coverage@1.0.2': + resolution: {integrity: sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==} + engines: {node: '>=22'} + + '@vitest/istanbul-lib-report@1.0.2': + resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==} + engines: {node: '>=22'} - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + '@vitest/mocker@5.0.4': + resolution: {integrity: sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==} peerDependencies: - msw: ^2.4.9 + msw: ^2.4.9 || ^3.0.0 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: @@ -2209,20 +2216,8 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.11': - resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - - '@vitest/utils@4.1.11': - resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + '@vitest/spy@5.0.4': + resolution: {integrity: sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==} '@workflow/serde@4.1.0': resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} @@ -2419,8 +2414,8 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-v8-to-istanbul@1.0.5: - resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + ast-v8-to-istanbul@1.0.7: + resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==} astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} @@ -2513,8 +2508,8 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} engines: {node: '>=18'} chalk@4.1.2: @@ -2981,9 +2976,6 @@ packages: hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -3071,18 +3063,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} @@ -3215,15 +3195,11 @@ packages: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - magicast@0.5.4: - resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} @@ -3505,6 +3481,10 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -3587,9 +3567,6 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -3600,6 +3577,10 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pngjs@7.0.0: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} @@ -3852,9 +3833,6 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3874,11 +3852,8 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} streamx@2.28.0: resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} @@ -3928,13 +3903,18 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} tinyexec@1.3.0: resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} engines: {node: '>=18'} + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -3943,8 +3923,8 @@ packages: resolution: {integrity: sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==} engines: {node: ^20.0.0 || >=22.0.0} - tinyrainbow@3.1.1: - resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} engines: {node: '>=14.0.0'} toggle-selection@1.0.6: @@ -4157,23 +4137,23 @@ packages: yaml: optional: true - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@5.0.4: + resolution: {integrity: sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.4 + '@vitest/browser-preview': 5.0.4 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.4 + '@vitest/coverage-v8': 5.0.4 + '@vitest/ui': 5.0.4 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -4209,9 +4189,9 @@ packages: engines: {node: '>= 8'} hasBin: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} hasBin: true word-wrap@1.2.5: @@ -4814,6 +4794,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -5383,14 +5365,14 @@ snapshots: '@stryker-mutator/util@10.0.0': {} - '@stryker-mutator/vitest-runner@10.0.0(@stryker-mutator/core@10.0.0(@types/node@22.20.1))(vitest@4.1.11)': + '@stryker-mutator/vitest-runner@10.0.0(@stryker-mutator/core@10.0.0(@types/node@22.20.1))(vitest@5.0.4)': dependencies: '@stryker-mutator/api': 10.0.0 '@stryker-mutator/core': 10.0.0(@types/node@22.20.1) '@stryker-mutator/util': 10.0.0 semver: 7.8.5 tslib: 2.8.1 - vitest: 4.1.11(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)) + vitest: 5.0.4(@types/node@22.20.1)(@vitest/coverage-v8@5.0.4)(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)) '@swc/helpers@0.5.23': dependencies: @@ -5594,60 +5576,34 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vitest/coverage-v8@4.1.11(patch_hash=9c8a14435f152f76c2092d5212a3a200e7e48efd93d48eddea8b788d95247dd1)(vitest@4.1.11)': + '@vitest/coverage-v8@5.0.4(vitest@5.0.4)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.11 - ast-v8-to-istanbul: 1.0.5(patch_hash=1033b65c551f1e34f0852288a94128e9f39f596fccdd9aff984f26f0486ab050) - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.4 - obug: 2.1.4 - std-env: 4.2.0 - tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)) + '@vitest/istanbul-lib-coverage': 1.0.2 + '@vitest/istanbul-lib-report': 1.0.2 + ast-v8-to-istanbul: 1.0.7(patch_hash=8f07585d066beb92a665ea78ec8bef87af3af32de9076c9e23122fbabd9fb475) + magicast: 0.5.5 + obug: 3.0.0 + std-env: 4.3.0 + tinyrainbow: 3.2.0 + vitest: 5.0.4(@types/node@22.20.1)(@vitest/coverage-v8@5.0.4)(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)) - '@vitest/expect@4.1.11': + '@vitest/istanbul-lib-coverage@1.0.2': {} + + '@vitest/istanbul-lib-report@1.0.2': dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - chai: 6.2.2 - tinyrainbow: 3.1.1 + '@vitest/istanbul-lib-coverage': 1.0.2 - '@vitest/mocker@4.1.11(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0))': + '@vitest/mocker@5.0.4(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.11 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.4 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.3 optionalDependencies: vite: 8.2.1(@types/node@22.20.1)(yaml@2.9.0) - '@vitest/pretty-format@4.1.11': - dependencies: - tinyrainbow: 3.1.1 - - '@vitest/runner@4.1.11': - dependencies: - '@vitest/utils': 4.1.11 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.11': {} - - '@vitest/utils@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 + '@vitest/spy@5.0.4': {} '@workflow/serde@4.1.0': {} @@ -5774,7 +5730,7 @@ snapshots: assertion-error@2.0.1: {} - ast-v8-to-istanbul@1.0.5(patch_hash=1033b65c551f1e34f0852288a94128e9f39f596fccdd9aff984f26f0486ab050): + ast-v8-to-istanbul@1.0.7(patch_hash=8f07585d066beb92a665ea78ec8bef87af3af32de9076c9e23122fbabd9fb475): dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -5849,7 +5805,7 @@ snapshots: ccount@2.0.1: {} - chai@6.2.2: {} + chai@6.3.0: {} chalk@4.1.2: dependencies: @@ -6085,7 +6041,7 @@ snapshots: estree-util-attach-comments@3.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 estree-util-build-jsx@3.0.1: dependencies: @@ -6313,7 +6269,7 @@ snapshots: hast-util-to-estree@3.1.3(supports-color@7.2.0): dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 comma-separated-tokens: 2.0.3 @@ -6398,8 +6354,6 @@ snapshots: hookable@6.1.1: {} - html-escaper@2.0.2: {} - html-void-elements@3.0.0: {} https-proxy-agent@7.0.6(supports-color@7.2.0): @@ -6460,19 +6414,6 @@ snapshots: isexe@2.0.0: {} - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - jpeg-js@0.4.4: {} js-md4@0.3.2: {} @@ -6563,20 +6504,16 @@ snapshots: lru-cache@11.5.2: {} - magic-string@0.30.21: + magic-string@1.4.3: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - magicast@0.5.4: + magicast@0.5.5: dependencies: '@babel/parser': 7.29.8 '@babel/types': 7.29.8 source-map-js: 1.2.2 - make-dir@4.0.0: - dependencies: - semver: 7.8.5 - markdown-extensions@2.0.0: {} markdown-table@3.0.4: {} @@ -6872,7 +6809,7 @@ snapshots: micromark-extension-mdx-expression@3.0.1: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 micromark-factory-mdx-expression: 2.0.3 micromark-factory-space: 2.0.1 @@ -6883,7 +6820,7 @@ snapshots: micromark-extension-mdx-jsx@3.0.2: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 micromark-factory-mdx-expression: 2.0.3 @@ -6900,7 +6837,7 @@ snapshots: micromark-extension-mdxjs-esm@3.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-util-character: 2.1.1 @@ -6936,7 +6873,7 @@ snapshots: micromark-factory-mdx-expression@2.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 @@ -7000,7 +6937,7 @@ snapshots: micromark-util-events-to-acorn@2.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/unist': 3.0.3 devlop: 1.1.0 estree-util-visit: 2.0.0 @@ -7115,6 +7052,8 @@ snapshots: obug@2.1.4: {} + obug@3.0.0: {} + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.5: @@ -7242,14 +7181,14 @@ snapshots: path-key@4.0.0: {} - pathe@2.0.3: {} - pend@1.2.0: {} picocolors@1.1.1: {} picomatch@4.0.5: {} + picomatch@4.0.7: {} + pngjs@7.0.0: {} postcss@8.5.23: @@ -7340,7 +7279,7 @@ snapshots: recma-parse@1.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esast-util-from-js: 2.0.1 unified: 11.0.5 vfile: 6.0.3 @@ -7556,8 +7495,6 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} - signal-exit@4.1.0: {} skin-tone@2.0.0: @@ -7570,9 +7507,7 @@ snapshots: space-separated-tokens@2.0.2: {} - stackback@0.0.2: {} - - std-env@4.2.0: {} + std-env@4.3.0: {} streamx@2.28.0: dependencies: @@ -7649,10 +7584,12 @@ snapshots: dependencies: any-promise: 1.3.0 - tinybench@2.9.0: {} + tinybench@6.2.2: {} tinyexec@1.3.0: {} + tinyexec@1.3.1: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) @@ -7660,7 +7597,7 @@ snapshots: tinypool@2.1.2: {} - tinyrainbow@3.1.1: {} + tinyrainbow@3.2.0: {} toggle-selection@1.0.6: {} @@ -7855,31 +7792,25 @@ snapshots: fsevents: 2.3.3 yaml: 2.9.0 - vitest@4.1.11(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)): + vitest@5.0.4(@types/node@22.20.1)(@vitest/coverage-v8@5.0.4)(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.4(vite@8.2.1(@types/node@22.20.1)(yaml@2.9.0)) + chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.3.0 + magic-string: 1.4.3 + obug: 3.0.0 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 vite: 8.2.1(@types/node@22.20.1)(yaml@2.9.0) - why-is-node-running: 2.3.0 + why-is-node-running: 3.2.1 optionalDependencies: '@types/node': 22.20.1 - '@vitest/coverage-v8': 4.1.11(patch_hash=9c8a14435f152f76c2092d5212a3a200e7e48efd93d48eddea8b788d95247dd1)(vitest@4.1.11) + '@vitest/coverage-v8': 5.0.4(vitest@5.0.4) transitivePeerDependencies: - msw @@ -7891,10 +7822,7 @@ snapshots: dependencies: isexe: 2.0.0 - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 + why-is-node-running@3.2.1: {} word-wrap@1.2.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0b3ac8a908..27eff70a33 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,5 +16,4 @@ overrides: react-router: 7.18.2 react-router-dom: 7.18.2 patchedDependencies: - '@vitest/coverage-v8@4.1.11': patches/@vitest__coverage-v8@4.1.11.patch - ast-v8-to-istanbul@1.0.5: patches/ast-v8-to-istanbul@1.0.5.patch + ast-v8-to-istanbul@1.0.7: patches/ast-v8-to-istanbul@1.0.7.patch diff --git a/scripts/coverage-crap/model.ts b/scripts/coverage-crap/model.ts index 58fdf2765c..b5ad8304a2 100644 --- a/scripts/coverage-crap/model.ts +++ b/scripts/coverage-crap/model.ts @@ -83,14 +83,14 @@ function countsAreValid(record: IstanbulCounters): boolean { * Drops file records with a negative, null, or non-integer hit count, which fallow refuses the * whole map over. * - * Two upstream defects produced those counts on real runs, both fixed by the patches under - * `patches/`: the v8 provider resolved a module's script wrapper length from whichever coverage - * file it read last, so an unmeasured one shifted every offset off its source, and - * `ast-v8-to-istanbul` subtracted located branch paths from a region that did not contain them, - * which an awaited continuation can make negative. Either can also merge into a record measured - * correctly and pull its counts down, so a record is dropped whole rather than repaired; zeroed - * counts from the same shift are indistinguishable from untested code and stay. A dropped file - * gets no CRAP score rather than a wrong one. + * The one defect that produced those counts on real runs is patched under `patches/`: + * `ast-v8-to-istanbul` subtracts located branch paths from a region that did not contain them, + * which an awaited continuation can make negative. Vitest 5 fixed the other upstream (#10506, + * #10643): the v8 provider used to resolve a module's script wrapper length from whichever coverage + * file it read last, so an unmeasured one shifted every offset off its source. Either defect's + * counts could also merge into a record measured correctly and pull them down, so a record is + * dropped whole rather than repaired; zeroed counts from the same shift are indistinguishable from + * untested code and stay. A dropped file gets no CRAP score rather than a wrong one. */ export function sanitizeIstanbulCoverage(map: IstanbulCoverageMap): SanitizedCoverage { const kept: Record = {}; @@ -463,8 +463,9 @@ function renderDroppedSection(dropped: readonly string[]): readonly string[] { if (dropped.length === 0) return []; return section([ `Unscored: ${dropped.length} file(s) with counts no execution can produce ` + - `(for example, an unresolved script wrapper offset or a negative branch path count, ` + - `both fixed under patches/): ${dropped.map((file) => `\`${file}\``).join(', ')}`, + `(for example, an implicit-else branch path count driven negative by an awaited ` + + `continuation, which \`ast-v8-to-istanbul\` is patched against): ` + + `${dropped.map((file) => `\`${file}\``).join(', ')}`, ]); } diff --git a/scripts/coverage-crap/patches.test.ts b/scripts/coverage-crap/patches.test.ts index b4b884af2c..ddffffc121 100644 --- a/scripts/coverage-crap/patches.test.ts +++ b/scripts/coverage-crap/patches.test.ts @@ -1,12 +1,19 @@ -// Regression coverage for the two dependency patches under `patches/`, which keep merged v8 -// coverage records honest: `@vitest/coverage-v8` must carry a module's wrapper length across the -// merge instead of letting read order decide it, and `ast-v8-to-istanbul` must not publish a -// branch counter no execution could produce. Both defects made whole files unscored (#3425). +// Regression coverage for the dependency patch under `patches/`, which keeps merged v8 coverage +// records honest: `ast-v8-to-istanbul` must not publish a branch counter no execution could +// produce. The defect made whole files unscored (#3425). // -// The async branch cases run on records captured from a real inspector session and pinned in -// `patches.fixture.json`, because V8 serves a repeatedly-evaluated script from its compile cache -// as function-level coverage, which would make a live-capture assertion flaky. Each pinned record +// The cases run on records captured from a real inspector session and pinned in +// `patches.fixture.json`, because V8 serves a repeatedly-evaluated script from its compile cache as +// function-level coverage, which would make a live-capture assertion flaky. Each pinned record // carries the hand-instrumented entered/taken counts that established its expected counters. +// +// #3448's other patch, on `@vitest/coverage-v8`, is gone: Vitest 5 keeps a module's script wrapper +// offset through the merge of the workers' coverage files instead of letting read order or a pending +// dynamic import decide it (vitest-dev/vitest#10506, #10643), so no patched module is left to +// unit-test. Its symptom was a whole file scored from offsets shifted off its source by the wrapper, +// and a shifted record carries only plausible counts, so nothing here can see it: the guard for that +// class is upstream's merge, and `check:coverage-crap` still blocks a touched file missing from the +// map or holding a count no execution can produce. import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -14,11 +21,6 @@ import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; import { parseAstAsync } from 'vitest/node'; import { test } from 'node:test'; -import { - UNKNOWN_WRAPPER_LENGTH, - unmappableCoverageRecord, - WrapperLengths, -} from '@vitest/coverage-v8/dist/wrapper-lengths.js'; import { sanitizeIstanbulCoverage, type IstanbulCoverageMap } from './model.ts'; const PROVIDER_ENTRY = import.meta.resolve('@vitest/coverage-v8'); @@ -27,7 +29,7 @@ const PROVIDER_ENTRY = import.meta.resolve('@vitest/coverage-v8'); const providerRequire = createRequire(PROVIDER_ENTRY); /** - * Loads one such dependency, so the test uses exactly the copy the patched provider loads. + * Loads one such dependency, so the test uses exactly the copy the provider loads. * Named exports stay named, and a CommonJS package keeps its `default`. */ async function loadProviderDependency(name: string): Promise { @@ -88,111 +90,6 @@ function ifCounters(record: IstanbulRecord, line: number): number[] { return record.b[id].map(Number); } -// ---- Defect A: the wrapper length must survive the merge ------------------- - -// Six workers recorded `runner-cache-retention.ts` on the run that left it unscored: five had -// finished evaluating it and measured its wrapper, the sixth's dynamic import was still in -// flight and measured nothing. The provider merged the module with whichever of the two the last -// file it read happened to carry, so the same suite scored or unscored the file at random. -const MEASURED_WRAPPER = 209; -const MODULE_URL = '/virtual/module.ts'; - -test('one unmeasured wrapper cannot replace the length the other workers measured', () => { - for (const unmeasuredFirst of [false, true]) { - const lengths = new WrapperLengths(); - const readOrder = unmeasuredFirst - ? [UNKNOWN_WRAPPER_LENGTH, ...Array.from({ length: 5 }, () => MEASURED_WRAPPER)] - : [...Array.from({ length: 5 }, () => MEASURED_WRAPPER), UNKNOWN_WRAPPER_LENGTH]; - for (const startOffset of readOrder) lengths.add(MODULE_URL, startOffset); - assert.equal( - lengths.resolve(MODULE_URL), - MEASURED_WRAPPER, - `read order must not decide the wrapper length (unmeasured read ${unmeasuredFirst ? 'first' : 'last'})`, - ); - } -}); - -test('a wrapper two workers measured differently is unresolved, never guessed', () => { - for (const readOrder of [ - [MEASURED_WRAPPER, 0], - [0, MEASURED_WRAPPER], - ]) { - const lengths = new WrapperLengths(); - for (const startOffset of readOrder) lengths.add(MODULE_URL, startOffset); - assert.equal(lengths.resolve(MODULE_URL), UNKNOWN_WRAPPER_LENGTH); - } -}); - -test('a module whose wrapper nobody measured is invalid rather than zero-length', () => { - const lengths = new WrapperLengths(); - lengths.add(MODULE_URL, UNKNOWN_WRAPPER_LENGTH); - assert.equal(lengths.resolve(MODULE_URL), UNKNOWN_WRAPPER_LENGTH); - // A module no worker reported at all is a different case: nothing was ever claimed about it. - assert.equal(lengths.resolve('/virtual/other.ts'), 0); -}); - -test('a provider that measures no wrappers at all keeps unwrapped scripts at zero', () => { - // Browser mode evaluates modules in the page without the SSR wrapper, so its scripts carry no - // measurement. Reading that as "unmeasured" would invalidate every browser-mode file. - const lengths = new WrapperLengths(); - lengths.add(MODULE_URL, undefined); - assert.equal(lengths.resolve(MODULE_URL), 0); -}); - -test('wrapper measurements do not leak into the next report window', () => { - const lengths = new WrapperLengths(); - lengths.add(MODULE_URL, MEASURED_WRAPPER); - assert.equal(lengths.resolve(MODULE_URL), MEASURED_WRAPPER); - lengths.clear(); - // Each project/environment group converts the files it just read with the measurements its own - // workers made; a length from another group is not evidence for this one's scripts. - assert.equal(lengths.resolve(MODULE_URL), 0); -}); - -test('an unmeasurable module yields a record the CRAP sanitizer drops', async () => { - const record = unmappableCoverageRecord(`file://${MODULE_URL}`, undefined)[ - MODULE_URL - ] as IstanbulRecord; - assert.equal(record.s[0], null); - - // The marker has to survive merging with a measured record for the same file: a counter that - // added away would hand consumers a plausible record for offsets nobody measured. Merging - // re-keys the statements, so the assertion is on the set of counters rather than an index. - type CoverageMap = { - merge: (data: unknown) => void; - toJSON: () => IstanbulCoverageMap; - }; - const { createCoverageMap } = providerRequire('istanbul-lib-coverage') as { - createCoverageMap: () => CoverageMap; - }; - for (const measuredFirst of [false, true]) { - const map = createCoverageMap(); - const measured = { - ...record, - s: { 0: 7 }, - statementMap: { 0: { start: { line: 3, column: 0 }, end: { line: 3, column: 9 } } }, - }; - const marker = { ...record, statementMap: { 0: record.statementMap[0] } }; - if (measuredFirst) { - map.merge({ [MODULE_URL]: measured }); - map.merge({ [MODULE_URL]: marker }); - } else { - map.merge({ [MODULE_URL]: marker }); - map.merge({ [MODULE_URL]: measured }); - } - const merged = map.toJSON()[MODULE_URL] as IstanbulRecord; - assert.ok( - Object.values(merged.s).includes(null), - `a measured record must not repair an unmeasurable file (measured merged ${measuredFirst ? 'first' : 'last'})`, - ); - const { map: kept, dropped } = sanitizeIstanbulCoverage(map.toJSON() as IstanbulCoverageMap); - assert.deepEqual(kept, {}); - assert.deepEqual(dropped, [MODULE_URL]); - } -}); - -// ---- Defect B: async branch counts --------------------------------------- - // `async-fallback-operand` is the reported shape: `params.release ?? await probe(...)` feeding an // `if` with no `else`. Six callers short-circuit the `??` and take the branch, two await and do // not. The converter invents the missing path's counter by subtracting one block-containment diff --git a/scripts/vitest-slow-test-reporter.ts b/scripts/vitest-slow-test-reporter.ts index 4d47234519..4395252392 100644 --- a/scripts/vitest-slow-test-reporter.ts +++ b/scripts/vitest-slow-test-reporter.ts @@ -1,4 +1,4 @@ -import type { Reporter, TestCase, TestModule } from 'vitest/node'; +import type { Reporter, TestCase } from 'vitest/node'; /** * Slow-test ratchet (see docs/agents/testing.md "Speed rules"). @@ -90,7 +90,7 @@ export default function slowTestGateReporter(): Reporter { if (result.state !== 'passed' && result.state !== 'failed') return; const offender = classifySlowTest({ root, - moduleId: (testCase.module as TestModule).moduleId, + moduleId: testCase.module.moduleId, name: testCase.name, fullName: testCase.fullName, durationMs: testCase.diagnostic()?.duration ?? 0, diff --git a/src/__tests__/install-source.test.ts b/src/__tests__/install-source.test.ts index 08bc57e1f5..324ed67dcf 100644 --- a/src/__tests__/install-source.test.ts +++ b/src/__tests__/install-source.test.ts @@ -114,42 +114,46 @@ test('isTrustedInstallSourceUrl recognizes supported artifact services', () => { ); }); -test.sequential('materializeInstallablePath extracts zip archives without ditto', async () => { - const unzipPath = findExecutableInPath('unzip'); - assert.ok(unzipPath, 'unzip must be available for portable zip extraction'); - - const tempRoot = await mkdtempForTest('agent-device-install-source-unzip-'); - const archivePath = path.join(tempRoot, 'bundle.zip'); - const binDir = path.join(tempRoot, 'bin'); - const payloadDir = path.join(tempRoot, 'payload'); - const apkPath = path.join(payloadDir, 'Sample.apk'); - const previousPath = process.env.PATH; - - try { - await fs.mkdir(binDir); - await fs.symlink(unzipPath, path.join(binDir, 'unzip')); - await fs.mkdir(payloadDir); - await fs.writeFile(apkPath, 'placeholder apk', 'utf8'); - runCmdSync('zip', ['-qr', archivePath, 'payload'], { cwd: tempRoot }); - - process.env.PATH = binDir; - const result = await materializeInstallablePath({ - source: { kind: 'path', path: archivePath }, - isInstallablePath: (candidatePath, stat) => stat.isFile() && candidatePath.endsWith('.apk'), - installableLabel: 'Android installable (.apk or .aab)', - }); +test( + 'materializeInstallablePath extracts zip archives without ditto', + { concurrent: false }, + async () => { + const unzipPath = findExecutableInPath('unzip'); + assert.ok(unzipPath, 'unzip must be available for portable zip extraction'); + + const tempRoot = await mkdtempForTest('agent-device-install-source-unzip-'); + const archivePath = path.join(tempRoot, 'bundle.zip'); + const binDir = path.join(tempRoot, 'bin'); + const payloadDir = path.join(tempRoot, 'payload'); + const apkPath = path.join(payloadDir, 'Sample.apk'); + const previousPath = process.env.PATH; try { - assert.equal(path.basename(result.installablePath), 'Sample.apk'); - assert.equal(await fs.readFile(result.installablePath, 'utf8'), 'placeholder apk'); + await fs.mkdir(binDir); + await fs.symlink(unzipPath, path.join(binDir, 'unzip')); + await fs.mkdir(payloadDir); + await fs.writeFile(apkPath, 'placeholder apk', 'utf8'); + runCmdSync('zip', ['-qr', archivePath, 'payload'], { cwd: tempRoot }); + + process.env.PATH = binDir; + const result = await materializeInstallablePath({ + source: { kind: 'path', path: archivePath }, + isInstallablePath: (candidatePath, stat) => stat.isFile() && candidatePath.endsWith('.apk'), + installableLabel: 'Android installable (.apk or .aab)', + }); + + try { + assert.equal(path.basename(result.installablePath), 'Sample.apk'); + assert.equal(await fs.readFile(result.installablePath, 'utf8'), 'placeholder apk'); + } finally { + await result.cleanup(); + } } finally { - await result.cleanup(); + process.env.PATH = previousPath; + await fs.rm(tempRoot, { recursive: true, force: true }); } - } finally { - process.env.PATH = previousPath; - await fs.rm(tempRoot, { recursive: true, force: true }); - } -}); + }, +); test('materializeInstallablePath extracts tar.gz archives', async () => { const tempRoot = await mkdtempForTest('agent-device-install-source-tar-'); diff --git a/src/daemon-client/__tests__/daemon-client-developer-dir.test.ts b/src/daemon-client/__tests__/daemon-client-developer-dir.test.ts index baf512d997..1167cd6415 100644 --- a/src/daemon-client/__tests__/daemon-client-developer-dir.test.ts +++ b/src/daemon-client/__tests__/daemon-client-developer-dir.test.ts @@ -17,8 +17,9 @@ import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; const DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer'; -test.sequential.for([DEVELOPER_DIR, '', undefined])( +test.for([DEVELOPER_DIR, '', undefined])( 'sendToDaemon forwards local DEVELOPER_DIR (%s) and strips remote developerDir', + { concurrent: false }, async (developerDir, t) => { if (!(await supportsLoopbackBind())) { t.skip('loopback listeners are not permitted in this environment'); diff --git a/src/daemon-client/__tests__/daemon-client-provider-credentials.test.ts b/src/daemon-client/__tests__/daemon-client-provider-credentials.test.ts index 3ba42259bc..96daa7487a 100644 --- a/src/daemon-client/__tests__/daemon-client-provider-credentials.test.ts +++ b/src/daemon-client/__tests__/daemon-client-provider-credentials.test.ts @@ -31,8 +31,9 @@ afterEach(() => { type CapturedDaemon = { bodies: string[]; close: () => Promise }; -test.sequential.for(['socket', 'http'] as const)( +test.for(['socket', 'http'] as const)( 'a local %s daemon gets a fingerprint, never the key, only on lease_allocate with credentials', + { concurrent: false }, async (transport, t) => { if (!(await supportsLoopbackBind())) { t.skip('loopback listeners are not permitted in this environment'); @@ -85,33 +86,37 @@ test.sequential.for(['socket', 'http'] as const)( }, ); -test.sequential('a remote daemon gets no provider credential fingerprint', async (t) => { - if (!(await supportsLoopbackBind())) { - t.skip('loopback listeners are not permitted in this environment'); - return; - } - clearCredentialEnv(); - vi.stubEnv('LIMRUN_API_KEY', API_KEY); - const remote = await startHttpDaemonFixture({}); - vi.stubEnv('AGENT_DEVICE_DAEMON_BASE_URL', `http://127.0.0.1:${remote.port}`); - vi.stubEnv('AGENT_DEVICE_DAEMON_AUTH_TOKEN', 'remote-secret'); +test( + 'a remote daemon gets no provider credential fingerprint', + { concurrent: false }, + async (t) => { + if (!(await supportsLoopbackBind())) { + t.skip('loopback listeners are not permitted in this environment'); + return; + } + clearCredentialEnv(); + vi.stubEnv('LIMRUN_API_KEY', API_KEY); + const remote = await startHttpDaemonFixture({}); + vi.stubEnv('AGENT_DEVICE_DAEMON_BASE_URL', `http://127.0.0.1:${remote.port}`); + vi.stubEnv('AGENT_DEVICE_DAEMON_AUTH_TOKEN', 'remote-secret'); - try { - await sendToDaemon({ - session: 'default', - command: 'lease_allocate', - positionals: [], - meta: { requestId: 'req-remote-lease', leaseProvider: 'limrun', tenantId: 'tenant-a' }, - }); - } finally { - await closeLoopbackServer(remote.server); - } + try { + await sendToDaemon({ + session: 'default', + command: 'lease_allocate', + positionals: [], + meta: { requestId: 'req-remote-lease', leaseProvider: 'limrun', tenantId: 'tenant-a' }, + }); + } finally { + await closeLoopbackServer(remote.server); + } - expect(remote.rpcRequests.map((rpc) => rpc.method)).toEqual(['agent_device.lease.allocate']); - const body = JSON.stringify(remote.rpcRequests[0]); - expect(readFingerprint(body)).toBe(undefined); - expect(body).not.toContain(API_KEY); -}); + expect(remote.rpcRequests.map((rpc) => rpc.method)).toEqual(['agent_device.lease.allocate']); + const body = JSON.stringify(remote.rpcRequests[0]); + expect(readFingerprint(body)).toBe(undefined); + expect(body).not.toContain(API_KEY); + }, +); function readFingerprint(body: string): unknown { const parsed = JSON.parse(body) as { diff --git a/src/daemon/__tests__/request-router-developer-dir.test.ts b/src/daemon/__tests__/request-router-developer-dir.test.ts index 35799ccc4b..ad73fcbd18 100644 --- a/src/daemon/__tests__/request-router-developer-dir.test.ts +++ b/src/daemon/__tests__/request-router-developer-dir.test.ts @@ -8,43 +8,47 @@ import { createRequestHandler } from './test-device-runtime-gateway.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { testCreateCommandSurface } from './command-surface-fixture.ts'; -test.sequential('a request spawns its commands with its own DEVELOPER_DIR, else the daemon one', async () => { - const seen: string[] = []; - const handler = createRequestHandler({ - createCommandSurface: testCreateCommandSurface, - logPath: path.join(mkdtempForTestSync('agent-device-router-developer-dir-'), 'daemon.log'), - token: 'test-token', - sessionStore: makeSessionStore('agent-device-router-developer-dir-store-'), - leaseRegistry: new LeaseRegistry(), - deviceInventoryGateways: createTestDeviceInventoryGateways({ - local: async () => { - const child = await runCmd(process.execPath, [ - '-e', - 'process.stdout.write(process.env.DEVELOPER_DIR ?? "")', - ]); - seen.push(child.stdout); - return []; - }, - }), - trackDownloadableArtifact: () => 'artifact-id', - }); - const devices = (developerDir?: string) => - handler({ +test( + 'a request spawns its commands with its own DEVELOPER_DIR, else the daemon one', + { concurrent: false }, + async () => { + const seen: string[] = []; + const handler = createRequestHandler({ + createCommandSurface: testCreateCommandSurface, + logPath: path.join(mkdtempForTestSync('agent-device-router-developer-dir-'), 'daemon.log'), token: 'test-token', - session: 'default', - command: 'devices', - positionals: [], - flags: { platform: 'ios' }, - meta: { requestId: `req-${developerDir ?? 'none'}`, developerDir }, + sessionStore: makeSessionStore('agent-device-router-developer-dir-store-'), + leaseRegistry: new LeaseRegistry(), + deviceInventoryGateways: createTestDeviceInventoryGateways({ + local: async () => { + const child = await runCmd(process.execPath, [ + '-e', + 'process.stdout.write(process.env.DEVELOPER_DIR ?? "")', + ]); + seen.push(child.stdout); + return []; + }, + }), + trackDownloadableArtifact: () => 'artifact-id', }); + const devices = (developerDir?: string) => + handler({ + token: 'test-token', + session: 'default', + command: 'devices', + positionals: [], + flags: { platform: 'ios' }, + meta: { requestId: `req-${developerDir ?? 'none'}`, developerDir }, + }); - const saved = process.env.DEVELOPER_DIR; - process.env.DEVELOPER_DIR = '/daemon/Developer'; - try { - await Promise.all([devices('/a/Developer'), devices('/b/Developer'), devices(''), devices()]); - } finally { - if (saved === undefined) delete process.env.DEVELOPER_DIR; - else process.env.DEVELOPER_DIR = saved; - } - expect([...seen].sort()).toEqual(['', '/a/Developer', '/b/Developer', '/daemon/Developer']); -}); + const saved = process.env.DEVELOPER_DIR; + process.env.DEVELOPER_DIR = '/daemon/Developer'; + try { + await Promise.all([devices('/a/Developer'), devices('/b/Developer'), devices(''), devices()]); + } finally { + if (saved === undefined) delete process.env.DEVELOPER_DIR; + else process.env.DEVELOPER_DIR = saved; + } + expect([...seen].sort()).toEqual(['', '/a/Developer', '/b/Developer', '/daemon/Developer']); + }, +); diff --git a/src/daemon/__tests__/request-router-open.test.ts b/src/daemon/__tests__/request-router-open.test.ts index fffae59614..022617de97 100644 --- a/src/daemon/__tests__/request-router-open.test.ts +++ b/src/daemon/__tests__/request-router-open.test.ts @@ -180,6 +180,12 @@ test('open returns and creates the session state directory', async () => { path.join(String(response.data?.sessionStateDir), 'events.ndjson'), ); expect(fs.existsSync(String(response.data?.sessionStateDir))).toBe(true); + // `open` records the request through `recordEvent`, which queues the event-log + // append rather than writing it, so the path it returns can be named before the + // file exists. Every reader of that log awaits this flush first (see + // session-store.test.ts); asserting existence without it races the append and + // only passes when module loading happened to drain the queue in time. + await sessionStore.flushEvents('session-a'); expect(fs.existsSync(String(response.data?.eventLogPath))).toBe(true); } expect(inspectDeviceClaims({ udid: device.id })[0]?.claim?.stateDir).toBe( diff --git a/vitest.config.ts b/vitest.config.ts index ac655289e7..a799f3723b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -274,15 +274,20 @@ export default defineConfig({ lines: 80, }, include: ['src/**/*.ts', 'packages/*/src/**/*.ts'], + // Vitest 5 matches these against the path relative to the repo root without picomatch's + // `contains`, so a pattern reaches only the tree its anchor names: `src/**` no longer covers + // `packages/*/src/**` the way 4.1.11's did. The four that name a kind of file rather than a + // location are root-free. Written as `src/**` instead, package-side type modules and + // `__tests__` helpers join the coverage denominator and dilute it with test material. exclude: [ - 'src/**/*.test.ts', - 'src/**/__tests__/**', + '**/*.test.ts', + '**/__tests__/**', '**/*.fixtures.ts', // Test helpers are test material, not product surface: they sat under `__tests__/` roots // before @agent-device/test-kit existed and must stay out of the coverage denominator. 'packages/test-kit/**', - 'src/**/*-types.ts', - 'src/**/types.ts', + '**/*-types.ts', + '**/types.ts', 'src/sdk/**', 'src/bin.ts', 'src/client/client-types.ts',