From 68ee65c9fe68ffcb66cc51791593af47e47aed14 Mon Sep 17 00:00:00 2001 From: neverland Date: Fri, 6 Nov 2026 14:00:43 +0800 Subject: [PATCH 1/4] chore(test): use vmForks for Rstack tests --- packages/rstack/rstack.config.ts | 1 + packages/rstack/src/setup/install.ts | 5 +- .../rstack/tests/config/extends/index.test.ts | 70 ++++++++++--------- .../tests/config/extends/load-shared.ts | 24 +++++++ .../tests/config/load-config/index.test.ts | 65 +++++------------ .../tests/config/load-config/parallel-load.ts | 36 ++++++++++ .../config/reload-doc-config/index.test.ts | 7 +- .../config/reload-lib-config/index.test.ts | 7 +- 8 files changed, 130 insertions(+), 85 deletions(-) create mode 100644 packages/rstack/tests/config/extends/load-shared.ts create mode 100644 packages/rstack/tests/config/load-config/parallel-load.ts diff --git a/packages/rstack/rstack.config.ts b/packages/rstack/rstack.config.ts index 7967b7a9..ef5f3610 100644 --- a/packages/rstack/rstack.config.ts +++ b/packages/rstack/rstack.config.ts @@ -4,6 +4,7 @@ import { define } from 'rstack'; define.test(() => { return { + pool: 'vmForks', // Temporary projects and fixture projects may contain files that match // Rstest's test glob. exclude: [ diff --git a/packages/rstack/src/setup/install.ts b/packages/rstack/src/setup/install.ts index bd0bb407..ad900faf 100644 --- a/packages/rstack/src/setup/install.ts +++ b/packages/rstack/src/setup/install.ts @@ -7,6 +7,7 @@ import { writeFileSync, } from 'node:fs'; import path from 'node:path'; +import { types } from 'node:util'; import { displayPath, fail, @@ -156,7 +157,7 @@ const claimOwner = ( writeFileSync(ownerPath, `${project}\n`, { flag: 'wx' }); } catch (error) { const code = - error instanceof Error && 'code' in error ? error.code : undefined; + types.isNativeError(error) && 'code' in error ? error.code : undefined; if (code !== 'EEXIST') { throw error; } @@ -285,7 +286,7 @@ export const installHooks = ({ chmodSync(filePath, 0o755); } } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const message = types.isNativeError(error) ? error.message : String(error); return fail('write-failed', `Failed to write Git hook files: ${message}`); } diff --git a/packages/rstack/tests/config/extends/index.test.ts b/packages/rstack/tests/config/extends/index.test.ts index 89077fc5..34e09de2 100644 --- a/packages/rstack/tests/config/extends/index.test.ts +++ b/packages/rstack/tests/config/extends/index.test.ts @@ -1,50 +1,52 @@ +import { execFileSync } from 'node:child_process'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { loadRstackConfig } from '../../../src/config.ts'; import { resolveConfigLayers } from '../../../src/configLayers.ts'; const cwd = import.meta.dirname; -const params = { command: 'build', env: 'production' } as const; -test('loads all seven tools from shared configs and tracks their dependencies', async () => { - const { configs, filePath, dependencies } = await loadRstackConfig({ cwd }); - const layers = [configs]; +test('loads all seven tools from shared configs and tracks their dependencies', () => { + // Dependency tracking uses Node loader hooks, outside the test VM. + const output = execFileSync( + process.execPath, + [path.join(cwd, 'load-shared.ts')], + { encoding: 'utf8', timeout: 10_000 }, + ); + const result: unknown = JSON.parse(output); + const dependencies: unknown = expect.arrayContaining([ + path.join(cwd, 'shared.ts'), + ]); - expect({ - app: await resolveConfigLayers(layers, 'app', params), - lib: await resolveConfigLayers(layers, 'lib', params), - doc: await resolveConfigLayers(layers, 'doc'), - test: await resolveConfigLayers(layers, 'test'), - lint: await resolveConfigLayers(layers, 'lint'), - fmt: await resolveConfigLayers(layers, 'fmt'), - staged: await resolveConfigLayers(layers, 'staged'), - }).toEqual({ - app: [ - { - source: { - define: { ENV: 'production' }, + expect(result).toEqual({ + configs: { + app: [ + { + source: { + define: { ENV: 'production' }, + }, }, - }, - ], - lib: [ - { - lib: [{ format: 'esm' }], - }, - ], - doc: [{ root: 'docs' }], - test: [{ setupFiles: [path.join(cwd, 'setup.ts')] }], - lint: [ - [ + ], + lib: [ { - rules: { 'no-debugger': 'error' }, + lib: [{ format: 'esm' }], }, ], - ], - fmt: [{ semi: false, singleQuote: false, printWidth: 80 }], - staged: [{ '*.ts': 'rs lint' }], + doc: [{ root: 'docs' }], + test: [{ setupFiles: [path.join(cwd, 'setup.ts')] }], + lint: [ + [ + { + rules: { 'no-debugger': 'error' }, + }, + ], + ], + fmt: [{ semi: false, singleQuote: false, printWidth: 80 }], + staged: [{ '*.ts': 'rs lint' }], + }, + filePath: path.join(cwd, 'rstack.config.ts'), + dependencies, }); - expect(filePath).toBe(path.join(cwd, 'rstack.config.ts')); - expect(dependencies).toContain(path.join(cwd, 'shared.ts')); }); test('preserves project priority regardless of call order and across reloads', async () => { diff --git a/packages/rstack/tests/config/extends/load-shared.ts b/packages/rstack/tests/config/extends/load-shared.ts new file mode 100644 index 00000000..f4a0bb20 --- /dev/null +++ b/packages/rstack/tests/config/extends/load-shared.ts @@ -0,0 +1,24 @@ +import { loadRstackConfig } from '../../../src/config.ts'; +import { resolveConfigLayers } from '../../../src/configLayers.ts'; + +const { configs, filePath, dependencies } = await loadRstackConfig({ + cwd: import.meta.dirname, +}); +const layers = [configs]; +const params = { command: 'build', env: 'production' } as const; + +console.log( + JSON.stringify({ + configs: { + app: await resolveConfigLayers(layers, 'app', params), + lib: await resolveConfigLayers(layers, 'lib', params), + doc: await resolveConfigLayers(layers, 'doc'), + test: await resolveConfigLayers(layers, 'test'), + lint: await resolveConfigLayers(layers, 'lint'), + fmt: await resolveConfigLayers(layers, 'fmt'), + staged: await resolveConfigLayers(layers, 'staged'), + }, + filePath, + dependencies, + }), +); diff --git a/packages/rstack/tests/config/load-config/index.test.ts b/packages/rstack/tests/config/load-config/index.test.ts index 8d3f861d..810890d7 100644 --- a/packages/rstack/tests/config/load-config/index.test.ts +++ b/packages/rstack/tests/config/load-config/index.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from 'node:child_process'; import path from 'node:path'; import { afterEach, expect, test } from 'rstack/test'; import { define, loadRstackConfig } from '../../../src/config.ts'; @@ -6,33 +7,14 @@ import { loadCliRstackConfig, } from '../../../src/cli/config.ts'; -type Deferred = ReturnType>; - -type ConfigTestHooks = { - ready: Deferred; - release: Deferred; -}; - -declare global { - // rslint-disable-next-line no-var - var __rstackConfigTestHooks: ConfigTestHooks | undefined; -} - const state = getConfigState(); const configPath = (fileName: string): string => path.join(import.meta.dirname, fileName); const loadConfigFile = (fileName: string) => loadRstackConfig({ configFilePath: configPath(fileName) }); -const createTestHooks = (): ConfigTestHooks => - (globalThis.__rstackConfigTestHooks = { - ready: Promise.withResolvers(), - release: Promise.withResolvers(), - }); - afterEach(() => { delete state.configPath; - delete globalThis.__rstackConfigTestHooks; }); test('should discard a config session after loading fails', async () => { @@ -94,39 +76,28 @@ test('should search cwd without inheriting the CLI config path', async () => { ); }); -test('should isolate parallel config sessions across top-level await', async () => { - const hooks = createTestHooks(); - const firstLoad = loadConfigFile('parallel-first.config.ts'); - await hooks.ready.promise; - - const secondLoad = loadConfigFile('parallel-second.config.ts'); - const parallelLoads = Promise.all([firstLoad, secondLoad]); - - await secondLoad; - hooks.release.resolve(); - const [firstResult, secondResult] = await parallelLoads; +// Native loader hooks and the fixture's globals must share one Node realm. +test('should isolate parallel config sessions across top-level await', () => { + const output = execFileSync( + process.execPath, + [configPath('parallel-load.ts'), 'success'], + { encoding: 'utf8', timeout: 10_000 }, + ); - expect(firstResult.configs).toEqual({ - app: { root: 'first' }, - test: {}, - }); - expect(secondResult.configs).toEqual({ - app: { root: 'second' }, - lib: {}, - }); + expect(JSON.parse(output)).toEqual([ + { app: { root: 'first' }, test: {} }, + { app: { root: 'second' }, lib: {} }, + ]); }); -test('should keep a running session intact when another config fails', async () => { - const hooks = createTestHooks(); - const successfulLoad = loadConfigFile('parallel-first.config.ts'); - await hooks.ready.promise; - - await expect(loadConfigFile('parallel-failing.config.ts')).rejects.toThrow( - 'parallel config error', +test('should keep a running session intact when another config fails', () => { + const output = execFileSync( + process.execPath, + [configPath('parallel-load.ts'), 'failure'], + { encoding: 'utf8', timeout: 10_000 }, ); - hooks.release.resolve(); - expect((await successfulLoad).configs).toEqual({ + expect(JSON.parse(output)).toEqual({ app: { root: 'first' }, test: {}, }); diff --git a/packages/rstack/tests/config/load-config/parallel-load.ts b/packages/rstack/tests/config/load-config/parallel-load.ts new file mode 100644 index 00000000..7164a365 --- /dev/null +++ b/packages/rstack/tests/config/load-config/parallel-load.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { loadRstackConfig } from '../../../src/config.ts'; + +type Deferred = ReturnType>; + +declare global { + // rslint-disable-next-line no-var + var __rstackConfigTestHooks: + { ready: Deferred; release: Deferred } | undefined; +} + +const hooks = (globalThis.__rstackConfigTestHooks = { + ready: Promise.withResolvers(), + release: Promise.withResolvers(), +}); +const load = (name: string) => + loadRstackConfig({ configFilePath: path.join(import.meta.dirname, name) }); +const firstLoad = load('parallel-first.config.ts'); +await Promise.race([hooks.ready.promise, firstLoad]); + +if (process.argv[2] === 'failure') { + await assert.rejects(load('parallel-failing.config.ts'), { + message: 'parallel config error', + }); + hooks.release.resolve(); + console.log(JSON.stringify((await firstLoad).configs)); +} else { + const secondLoad = load('parallel-second.config.ts'); + const parallelLoads = Promise.all([firstLoad, secondLoad]); + await secondLoad; + hooks.release.resolve(); + console.log( + JSON.stringify((await parallelLoads).map(({ configs }) => configs)), + ); +} diff --git a/packages/rstack/tests/config/reload-doc-config/index.test.ts b/packages/rstack/tests/config/reload-doc-config/index.test.ts index ded4ea17..56443e52 100644 --- a/packages/rstack/tests/config/reload-doc-config/index.test.ts +++ b/packages/rstack/tests/config/reload-doc-config/index.test.ts @@ -1,6 +1,6 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import { getRandomPort } from '@rstackjs/test-utils'; +import { getRandomPort, waitFor } from '@rstackjs/test-utils'; import { test } from '#test-helpers'; test('should restart doc dev server when Rstack config changes', async ({ @@ -45,6 +45,8 @@ define.doc({ `doc --config test-temp-rstack.config.ts --port ${await getRandomPort()}`, ); await logHelper.expectBuildEnd(); + // The build log can arrive before the restart watcher finishes its initial scan. + await waitFor(200); logHelper.clearLogs(); await writeConfig('after config change'); @@ -53,6 +55,7 @@ define.doc({ 'restarting server as test-temp-rstack.config.ts changed', ); await logHelper.expectBuildEnd(); + await waitFor(200); logHelper.clearLogs(); await writeFile(userWatchFile, 'changed\n'); @@ -96,6 +99,7 @@ define.doc({ `doc --config test-temp-import.config.ts --port ${await getRandomPort()}`, ); await logHelper.expectBuildEnd(); + await waitFor(200); logHelper.clearLogs(); await writeFile( @@ -150,6 +154,7 @@ define.doc({ expect(await initialResponse.text()).toContain( 'before import change', ); + await waitFor(200); logHelper.clearLogs(); await writeFile( diff --git a/packages/rstack/tests/config/reload-lib-config/index.test.ts b/packages/rstack/tests/config/reload-lib-config/index.test.ts index 94694b2a..bee8d6ee 100644 --- a/packages/rstack/tests/config/reload-lib-config/index.test.ts +++ b/packages/rstack/tests/config/reload-lib-config/index.test.ts @@ -1,6 +1,6 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import { waitForFile } from '@rstackjs/test-utils'; +import { waitFor, waitForFile } from '@rstackjs/test-utils'; import { test } from '#test-helpers'; test('should restart lib watch build when Rstack config changes', async ({ @@ -44,6 +44,8 @@ define.lib({ execCliAsync('lib --watch --config test-temp-rstack.config.ts'); await logHelper.expectLog('build completed, watching for changes...'); await waitForFile(path.join(dist1, 'index.js')); + // The build log precedes the restart watcher's initial filesystem scan. + await waitFor(200); logHelper.clearLogs(); await writeConfig('dist-2'); @@ -53,6 +55,7 @@ define.lib({ ); await logHelper.expectLog('build completed, watching for changes...'); await waitForFile(path.join(dist2, 'index.js')); + await waitFor(200); logHelper.clearLogs(); await writeFile(userWatchFile, 'changed\n'); @@ -93,6 +96,7 @@ define.lib({ execCliAsync('lib --watch --config test-temp-import.config.ts'); await logHelper.expectLog('build completed, watching for changes...'); await waitForFile(path.join(dist1, 'index.js')); + await waitFor(200); logHelper.clearLogs(); await writeFile(importedFile, "export const distPath = 'dist-import-2';\n"); @@ -138,6 +142,7 @@ define.extends([sharedConfig]); execCliAsync('lib --watch --config test-temp-shared.config.ts'); await logHelper.expectLog('build completed, watching for changes...'); await waitForFile(path.join(dist1, 'index.js')); + await waitFor(200); logHelper.clearLogs(); await writeFile( From 59b5f5757c6fe447920f0c243f68b7d302b37c5e Mon Sep 17 00:00:00 2001 From: neverland Date: Fri, 6 Nov 2026 14:02:34 +0800 Subject: [PATCH 2/4] chore(ci): compare test worker pools --- .github/scripts/benchmark-pools.mjs | 101 ++++++++++++++++++++++++++++ .github/workflows/test.yml | 4 ++ 2 files changed, 105 insertions(+) create mode 100644 .github/scripts/benchmark-pools.mjs diff --git a/.github/scripts/benchmark-pools.mjs b/.github/scripts/benchmark-pools.mjs new file mode 100644 index 00000000..70a35002 --- /dev/null +++ b/.github/scripts/benchmark-pools.mjs @@ -0,0 +1,101 @@ +import { spawnSync } from 'node:child_process'; +import { appendFileSync } from 'node:fs'; +import { availableParallelism, totalmem } from 'node:os'; +import path from 'node:path'; +import { performance } from 'node:perf_hooks'; +import { fileURLToPath } from 'node:url'; + +const cwd = fileURLToPath(new URL('../../packages/rstack/', import.meta.url)); +const cli = path.join(cwd, 'bin/rs.js'); +const pools = ['forks', 'vmForks']; +const rounds = 6; +const results = []; +let expectedCounts; + +console.log( + 'POOL_BENCH_ENV', + JSON.stringify({ + node: process.version, + platform: process.platform, + arch: process.arch, + cpus: availableParallelism(), + memoryGB: totalmem() / 1024 ** 3, + nodeOptions: process.env.NODE_OPTIONS, + rounds, + }), +); + +function run(pool, round) { + const start = performance.now(); + const child = spawnSync( + process.execPath, + [cli, 'test', '--pool', pool, '--reporters', 'md'], + { + cwd, + encoding: 'utf8', + timeout: 120_000, + maxBuffer: 16 * 1024 * 1024, + }, + ); + const wallMs = Math.round(performance.now() - start); + const output = `${child.stdout ?? ''}\n${child.stderr ?? ''}`; + const match = output.match(/```json\s*([\s\S]*?)```/); + const summary = match ? JSON.parse(match[1]) : undefined; + if ( + child.status !== 0 || + summary?.status !== 'pass' || + summary.counts.testFiles !== 70 || + summary.counts.tests !== 408 || + (expectedCounts && JSON.stringify(summary.counts) !== expectedCounts) + ) { + console.error(output); + throw new Error( + `${pool} round ${round} failed validation: ${child.error ?? child.status}`, + ); + } + expectedCounts ??= JSON.stringify(summary.counts); + const result = { pool, round, wallMs, ...summary }; + console.log('POOL_BENCH_SAMPLE', JSON.stringify(result)); + if (round > 0) results.push(result); +} + +for (const pool of pools) run(pool, 0); +for (let round = 1; round <= rounds; round++) { + for (let position = 0; position < pools.length; position++) { + run(pools[(round - 1 + position) % pools.length], round); + } +} + +const stats = (values) => ({ + mean: values.reduce((sum, value) => sum + value, 0) / values.length, + min: Math.min(...values), + max: Math.max(...values), +}); +const averages = pools.map((pool) => { + const samples = results.filter((result) => result.pool === pool); + return { + pool, + samples: samples.length, + wallMs: stats(samples.map((sample) => sample.wallMs)), + totalMs: stats(samples.map((sample) => sample.durationMs.total)), + buildMs: stats(samples.map((sample) => sample.durationMs.build)), + testsMs: stats(samples.map((sample) => sample.durationMs.tests)), + }; +}); +console.log('POOL_BENCH_AVERAGES', JSON.stringify(averages)); + +if (process.env.GITHUB_STEP_SUMMARY) { + const lines = [ + '## Worker pool comparison', + '', + `${process.platform} ${process.arch}, ${process.version}, ${availableParallelism()} CPUs. Each pool: 1 warmup + ${rounds} measured runs; rotating order. Every run completed 70 files / 408 tests with matching pass/skip counts (${expectedCounts}). Default worker counts and memory limits.`, + '', + '| Pool | Mean wall time | Mean Rstest duration | Rstest min–max |', + '| --- | ---: | ---: | ---: |', + ...averages.map( + (result) => + `| ${result.pool} | ${result.wallMs.mean.toFixed(0)} ms | ${result.totalMs.mean.toFixed(0)} ms | ${result.totalMs.min}–${result.totalMs.max} ms |`, + ), + ]; + appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join('\n')}\n`); +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c7ebb5bf..a466b300 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,3 +76,7 @@ jobs: - name: Run Test (Windows) if: steps.changes.outputs.changed == 'true' && runner.os == 'Windows' run: pnpm --workspace-concurrency=1 --filter "./packages/**" test + + - name: Compare worker pools + if: steps.changes.outputs.changed == 'true' + run: node .github/scripts/benchmark-pools.mjs From 4ac39af7efadc4d4e31089630764fd8fba87e924 Mon Sep 17 00:00:00 2001 From: neverland Date: Fri, 6 Nov 2026 14:15:22 +0800 Subject: [PATCH 3/4] chore(ci): remove temporary pool benchmark --- .github/scripts/benchmark-pools.mjs | 101 ---------------------------- .github/workflows/test.yml | 4 -- 2 files changed, 105 deletions(-) delete mode 100644 .github/scripts/benchmark-pools.mjs diff --git a/.github/scripts/benchmark-pools.mjs b/.github/scripts/benchmark-pools.mjs deleted file mode 100644 index 70a35002..00000000 --- a/.github/scripts/benchmark-pools.mjs +++ /dev/null @@ -1,101 +0,0 @@ -import { spawnSync } from 'node:child_process'; -import { appendFileSync } from 'node:fs'; -import { availableParallelism, totalmem } from 'node:os'; -import path from 'node:path'; -import { performance } from 'node:perf_hooks'; -import { fileURLToPath } from 'node:url'; - -const cwd = fileURLToPath(new URL('../../packages/rstack/', import.meta.url)); -const cli = path.join(cwd, 'bin/rs.js'); -const pools = ['forks', 'vmForks']; -const rounds = 6; -const results = []; -let expectedCounts; - -console.log( - 'POOL_BENCH_ENV', - JSON.stringify({ - node: process.version, - platform: process.platform, - arch: process.arch, - cpus: availableParallelism(), - memoryGB: totalmem() / 1024 ** 3, - nodeOptions: process.env.NODE_OPTIONS, - rounds, - }), -); - -function run(pool, round) { - const start = performance.now(); - const child = spawnSync( - process.execPath, - [cli, 'test', '--pool', pool, '--reporters', 'md'], - { - cwd, - encoding: 'utf8', - timeout: 120_000, - maxBuffer: 16 * 1024 * 1024, - }, - ); - const wallMs = Math.round(performance.now() - start); - const output = `${child.stdout ?? ''}\n${child.stderr ?? ''}`; - const match = output.match(/```json\s*([\s\S]*?)```/); - const summary = match ? JSON.parse(match[1]) : undefined; - if ( - child.status !== 0 || - summary?.status !== 'pass' || - summary.counts.testFiles !== 70 || - summary.counts.tests !== 408 || - (expectedCounts && JSON.stringify(summary.counts) !== expectedCounts) - ) { - console.error(output); - throw new Error( - `${pool} round ${round} failed validation: ${child.error ?? child.status}`, - ); - } - expectedCounts ??= JSON.stringify(summary.counts); - const result = { pool, round, wallMs, ...summary }; - console.log('POOL_BENCH_SAMPLE', JSON.stringify(result)); - if (round > 0) results.push(result); -} - -for (const pool of pools) run(pool, 0); -for (let round = 1; round <= rounds; round++) { - for (let position = 0; position < pools.length; position++) { - run(pools[(round - 1 + position) % pools.length], round); - } -} - -const stats = (values) => ({ - mean: values.reduce((sum, value) => sum + value, 0) / values.length, - min: Math.min(...values), - max: Math.max(...values), -}); -const averages = pools.map((pool) => { - const samples = results.filter((result) => result.pool === pool); - return { - pool, - samples: samples.length, - wallMs: stats(samples.map((sample) => sample.wallMs)), - totalMs: stats(samples.map((sample) => sample.durationMs.total)), - buildMs: stats(samples.map((sample) => sample.durationMs.build)), - testsMs: stats(samples.map((sample) => sample.durationMs.tests)), - }; -}); -console.log('POOL_BENCH_AVERAGES', JSON.stringify(averages)); - -if (process.env.GITHUB_STEP_SUMMARY) { - const lines = [ - '## Worker pool comparison', - '', - `${process.platform} ${process.arch}, ${process.version}, ${availableParallelism()} CPUs. Each pool: 1 warmup + ${rounds} measured runs; rotating order. Every run completed 70 files / 408 tests with matching pass/skip counts (${expectedCounts}). Default worker counts and memory limits.`, - '', - '| Pool | Mean wall time | Mean Rstest duration | Rstest min–max |', - '| --- | ---: | ---: | ---: |', - ...averages.map( - (result) => - `| ${result.pool} | ${result.wallMs.mean.toFixed(0)} ms | ${result.totalMs.mean.toFixed(0)} ms | ${result.totalMs.min}–${result.totalMs.max} ms |`, - ), - ]; - appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join('\n')}\n`); -} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a466b300..c7ebb5bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,3 @@ jobs: - name: Run Test (Windows) if: steps.changes.outputs.changed == 'true' && runner.os == 'Windows' run: pnpm --workspace-concurrency=1 --filter "./packages/**" test - - - name: Compare worker pools - if: steps.changes.outputs.changed == 'true' - run: node .github/scripts/benchmark-pools.mjs From 0edd77926be8f3828c93c3a507edde91a70f4132 Mon Sep 17 00:00:00 2001 From: neverland Date: Fri, 6 Nov 2026 21:28:52 +0800 Subject: [PATCH 4/4] test: mitigate config watcher startup races --- .../rstack/tests/config/reload-doc-config/index.test.ts | 7 ++++++- .../rstack/tests/config/reload-lib-config/index.test.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/rstack/tests/config/reload-doc-config/index.test.ts b/packages/rstack/tests/config/reload-doc-config/index.test.ts index ded4ea17..01b63d19 100644 --- a/packages/rstack/tests/config/reload-doc-config/index.test.ts +++ b/packages/rstack/tests/config/reload-doc-config/index.test.ts @@ -1,6 +1,6 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import { getRandomPort } from '@rstackjs/test-utils'; +import { getRandomPort, waitFor } from '@rstackjs/test-utils'; import { test } from '#test-helpers'; test('should restart doc dev server when Rstack config changes', async ({ @@ -45,6 +45,8 @@ define.doc({ `doc --config test-temp-rstack.config.ts --port ${await getRandomPort()}`, ); await logHelper.expectBuildEnd(); + // The build log can arrive before the restart watcher finishes its initial scan. + await waitFor(100); logHelper.clearLogs(); await writeConfig('after config change'); @@ -53,6 +55,7 @@ define.doc({ 'restarting server as test-temp-rstack.config.ts changed', ); await logHelper.expectBuildEnd(); + await waitFor(100); logHelper.clearLogs(); await writeFile(userWatchFile, 'changed\n'); @@ -96,6 +99,7 @@ define.doc({ `doc --config test-temp-import.config.ts --port ${await getRandomPort()}`, ); await logHelper.expectBuildEnd(); + await waitFor(100); logHelper.clearLogs(); await writeFile( @@ -150,6 +154,7 @@ define.doc({ expect(await initialResponse.text()).toContain( 'before import change', ); + await waitFor(100); logHelper.clearLogs(); await writeFile( diff --git a/packages/rstack/tests/config/reload-lib-config/index.test.ts b/packages/rstack/tests/config/reload-lib-config/index.test.ts index 94694b2a..24d7268f 100644 --- a/packages/rstack/tests/config/reload-lib-config/index.test.ts +++ b/packages/rstack/tests/config/reload-lib-config/index.test.ts @@ -1,6 +1,6 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import { waitForFile } from '@rstackjs/test-utils'; +import { waitFor, waitForFile } from '@rstackjs/test-utils'; import { test } from '#test-helpers'; test('should restart lib watch build when Rstack config changes', async ({ @@ -44,6 +44,8 @@ define.lib({ execCliAsync('lib --watch --config test-temp-rstack.config.ts'); await logHelper.expectLog('build completed, watching for changes...'); await waitForFile(path.join(dist1, 'index.js')); + // The build log precedes the restart watcher's initial filesystem scan. + await waitFor(100); logHelper.clearLogs(); await writeConfig('dist-2'); @@ -53,6 +55,7 @@ define.lib({ ); await logHelper.expectLog('build completed, watching for changes...'); await waitForFile(path.join(dist2, 'index.js')); + await waitFor(100); logHelper.clearLogs(); await writeFile(userWatchFile, 'changed\n'); @@ -93,6 +96,7 @@ define.lib({ execCliAsync('lib --watch --config test-temp-import.config.ts'); await logHelper.expectLog('build completed, watching for changes...'); await waitForFile(path.join(dist1, 'index.js')); + await waitFor(100); logHelper.clearLogs(); await writeFile(importedFile, "export const distPath = 'dist-import-2';\n"); @@ -138,6 +142,7 @@ define.extends([sharedConfig]); execCliAsync('lib --watch --config test-temp-shared.config.ts'); await logHelper.expectLog('build completed, watching for changes...'); await waitForFile(path.join(dist1, 'index.js')); + await waitFor(100); logHelper.clearLogs(); await writeFile(