From bfd25ea0236b2d8fe368ff8c3978797a7a838b59 Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Sun, 18 Oct 2026 18:13:58 -0700 Subject: [PATCH 1/2] fix: make Windows unit and integration test suites pass under test:pr - Spawn TypeScript 7 tsgo from dirname(root) during the initial updateSnapshot in withNativeCompiler while keeping --cwd set to root, so Windows does not lock root while tsgo closes its stdio pipe after api.close(). - Configure GIT_CEILING_DIRECTORIES, TERM=xterm-256color, and Git for Windows bin PATH in packages/intent/vitest.config.ts so temporary test fixtures do not inherit a parent git repository or WSL bash.exe. - Use path.delimiter, #!/usr/bin/env node, and pathToFileURL(preload) in reusable-workflows.test.ts and review-workflow.test.ts. - Spawn Node directly with verdaccio/bin/verdaccio and await process close before deleting storageDir in integration tests, and invoke npm pack via execSync in packed-release.test.ts. --- .changeset/windows-tsgo-close-wait.md | 5 ++++ packages/intent/src/validate/blocks.ts | 12 +++++++++- .../integration/load-integration.test.ts | 6 ++--- .../tests/integration/packed-release.test.ts | 9 ++++---- packages/intent/tests/integration/scaffold.ts | 23 +++++++++++-------- .../integration/scanner-integration.test.ts | 6 ++--- .../intent/tests/reusable-workflows.test.ts | 21 ++++++++++------- packages/intent/tests/review-workflow.test.ts | 4 ++-- packages/intent/vitest.config.ts | 14 +++++++++++ 9 files changed, 68 insertions(+), 32 deletions(-) create mode 100644 .changeset/windows-tsgo-close-wait.md diff --git a/.changeset/windows-tsgo-close-wait.md b/.changeset/windows-tsgo-close-wait.md new file mode 100644 index 00000000..390a7abe --- /dev/null +++ b/.changeset/windows-tsgo-close-wait.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Spawn the TypeScript 7 `tsgo` compiler process from the parent directory in `withNativeCompiler` while keeping `--cwd` set to the project root, so Windows callers can remove or rename the checked directory immediately after `intent validate` or `intent repair` without hitting `EPERM`. diff --git a/packages/intent/src/validate/blocks.ts b/packages/intent/src/validate/blocks.ts index 5d06a5f9..31faaa49 100644 --- a/packages/intent/src/validate/blocks.ts +++ b/packages/intent/src/validate/blocks.ts @@ -214,13 +214,23 @@ async function withNativeCompiler( return await run(async (sources, compilerOptions) => { // A new configuration per call opens a new project that lists only // this call's files, whatever the compiler process kept from earlier. + const first = projects === 0 const config = `${virtualDir}/tsconfig-${projects++}.json` for (const [path, code] of sources) files.set(path, code) files.set( config, JSON.stringify({ compilerOptions, files: [...sources.keys()] }), ) - const snapshot = await api.updateSnapshot({ openProjects: [config] }) + // Spawn tsgo from the parent directory so Windows does not lock root + // while the child process closes its stdio pipe after api.close(). + const cwd = process.cwd() + if (first) process.chdir(dirname(root)) + let snapshot: NativeApi.Snapshot + try { + snapshot = await api.updateSnapshot({ openProjects: [config] }) + } finally { + if (first) process.chdir(cwd) + } const project = snapshot.getProject(config) if (!project) throw new Error(`could not open ${config}`) return project diff --git a/packages/intent/tests/integration/load-integration.test.ts b/packages/intent/tests/integration/load-integration.test.ts index 9ea70a98..913a6d0b 100644 --- a/packages/intent/tests/integration/load-integration.test.ts +++ b/packages/intent/tests/integration/load-integration.test.ts @@ -19,10 +19,10 @@ const tempDirs: Array = [] beforeAll(async () => { registry = await startRegistry() publishFixtures(registry.url) -}, 30_000) +}, 60_000) -afterAll(() => { - registry?.stop() +afterAll(async () => { + await registry?.stop() for (const dir of tempDirs) { rmSync(dir, { recursive: true, force: true }) } diff --git a/packages/intent/tests/integration/packed-release.test.ts b/packages/intent/tests/integration/packed-release.test.ts index 7706c412..0f474378 100644 --- a/packages/intent/tests/integration/packed-release.test.ts +++ b/packages/intent/tests/integration/packed-release.test.ts @@ -1,4 +1,4 @@ -import { execFileSync, spawnSync } from 'node:child_process' +import { execFileSync, execSync, spawnSync } from 'node:child_process' import { existsSync, mkdirSync, @@ -37,9 +37,8 @@ function run(args: Array) { beforeAll(() => { root = realpathSync(mkdtempSync(join(tmpdir(), 'intent-packed-release-'))) const packed = JSON.parse( - execFileSync( - 'npm', - ['pack', '--ignore-scripts', '--json', '--pack-destination', root], + execSync( + `npm pack --ignore-scripts --json --pack-destination=${JSON.stringify(root)}`, { cwd: packageRoot, encoding: 'utf8', @@ -348,7 +347,7 @@ describe('packed release', () => { expect(synced.status, synced.stderr).toBe(0) expect(run(['maintainer', 'check']).status).toBe(1) const packed = JSON.parse( - execFileSync('npm', ['pack', '--dry-run', '--ignore-scripts', '--json'], { + execSync('npm pack --dry-run --ignore-scripts --json', { cwd, encoding: 'utf8', timeout, diff --git a/packages/intent/tests/integration/scaffold.ts b/packages/intent/tests/integration/scaffold.ts index 60fe2775..e17db6d6 100644 --- a/packages/intent/tests/integration/scaffold.ts +++ b/packages/intent/tests/integration/scaffold.ts @@ -36,7 +36,7 @@ function getFreePort(): Promise { export interface Registry { url: string - stop: () => void + stop: () => Promise } export async function startRegistry(): Promise { @@ -70,26 +70,23 @@ export async function startRegistry(): Promise { ].join('\n'), ) - const isWindows = process.platform === 'win32' const verdaccioBin = join( thisDir, '..', '..', 'node_modules', - '.bin', + 'verdaccio', + 'bin', 'verdaccio', ) return new Promise((resolve, reject) => { const child = spawn( - isWindows ? `"${verdaccioBin}"` : verdaccioBin, - isWindows - ? ['--config', `"${configPath}"`, '--listen', `127.0.0.1:${port}`] - : ['--config', configPath, '--listen', `127.0.0.1:${port}`], + process.execPath, + [verdaccioBin, '--config', configPath, '--listen', `127.0.0.1:${port}`], { stdio: ['ignore', 'pipe', 'pipe'], detached: false, - shell: isWindows, }, ) @@ -109,8 +106,14 @@ export async function startRegistry(): Promise { clearTimeout(timeout) resolve({ url: `http://127.0.0.1:${port}`, - stop: () => { - child.kill('SIGTERM') + stop: async () => { + if (child.exitCode === null) { + const closed = new Promise((done) => + child.once('close', () => done()), + ) + child.kill('SIGTERM') + await closed + } rmSync(storageDir, { recursive: true, force: true }) }, }) diff --git a/packages/intent/tests/integration/scanner-integration.test.ts b/packages/intent/tests/integration/scanner-integration.test.ts index 6db17907..489d82ba 100644 --- a/packages/intent/tests/integration/scanner-integration.test.ts +++ b/packages/intent/tests/integration/scanner-integration.test.ts @@ -56,10 +56,10 @@ function expectLeafCoreSkill(parsed: CliResult['parsed']): void { beforeAll(async () => { registry = await startRegistry() publishFixtures(registry.url) -}, 30_000) +}, 60_000) -afterAll(() => { - registry?.stop() +afterAll(async () => { + await registry?.stop() for (const dir of tempDirs) { rmSync(dir, { recursive: true, force: true }) } diff --git a/packages/intent/tests/reusable-workflows.test.ts b/packages/intent/tests/reusable-workflows.test.ts index 6eba054b..f56f42fc 100644 --- a/packages/intent/tests/reusable-workflows.test.ts +++ b/packages/intent/tests/reusable-workflows.test.ts @@ -9,7 +9,8 @@ import { writeFileSync, } from 'node:fs' import { tmpdir } from 'node:os' -import { dirname, join } from 'node:path' +import { delimiter, dirname, join } from 'node:path' +import { pathToFileURL } from 'node:url' import { afterEach, beforeEach, expect, it } from 'vitest' import { parse } from 'yaml' @@ -37,7 +38,7 @@ function run(script: string, env: Record = {}) { encoding: 'utf8', env: { ...process.env, - PATH: `${join(root, 'bin')}:${process.env.PATH}`, + PATH: [join(root, 'bin'), process.env.PATH].join(delimiter), GITHUB_OUTPUT: join(root, 'output'), GITHUB_PATH: join(root, 'path'), RUNNER_TEMP: join(root, 'temp'), @@ -78,7 +79,7 @@ esac expect(installed.status, installed.stdout + installed.stderr).toBe(0) const intentPath = readFileSync(join(root, 'path'), 'utf8').trim() const invoked = run('intent validate --github-summary', { - PATH: `${intentPath}:${join(root, 'bin')}:${process.env.PATH}`, + PATH: [intentPath, join(root, 'bin'), process.env.PATH].join(delimiter), }) expect(invoked.status, invoked.stderr).toBe(0) expect(readFileSync(join(root, 'invoked'), 'utf8')).toBe( @@ -173,7 +174,7 @@ it.each(['', 'planning records/$(touch injected)'])( const trace = join(root, 'arguments.json') writeFileSync( join(root, 'intent'), - `#!${process.execPath}\nrequire('node:fs').writeFileSync(${JSON.stringify(trace)}, JSON.stringify(process.argv.slice(2)))\n`, + `#!/usr/bin/env node\nrequire('node:fs').writeFileSync(${JSON.stringify(trace)}, JSON.stringify(process.argv.slice(2)))\n`, { mode: 0o755 }, ) const check = workflow( @@ -186,7 +187,9 @@ it.each(['', 'planning records/$(touch injected)'])( encoding: 'utf8', env: { ...process.env, - PATH: `${root}:${process.env.PATH}`, + PATH: [root, dirname(process.execPath), process.env.PATH].join( + delimiter, + ), INTENT_REVIEW_BASE: 'base-sha', INTENT_ARTIFACTS: artifacts, }, @@ -243,7 +246,7 @@ it.each([ const argumentsPath = join(root, 'arguments.jsonl') writeFileSync( join(bin, 'intent'), - `#!${process.execPath} + `#!/usr/bin/env node const fs = require('node:fs'); const args = process.argv.slice(2); fs.appendFileSync(${JSON.stringify(argumentsPath)}, JSON.stringify(args) + '\\n'); @@ -262,7 +265,9 @@ if (args.includes('--patch')) process.stdout.write('suggested patch\\n'); encoding: 'utf8', env: { ...process.env, - PATH: `${bin}:${process.env.PATH}`, + PATH: [bin, dirname(process.execPath), process.env.PATH].join( + delimiter, + ), RUNNER_TEMP: temporary, GITHUB_OUTPUT: output, INTENT_ARTIFACTS: 'planning records/$(touch injected)', @@ -357,7 +362,7 @@ function publish( ) const result = spawnSync( process.execPath, - ['--import', preload, '--input-type=module'], + ['--import', pathToFileURL(preload).href, '--input-type=module'], { input: script.split("<<'NODE'\n")[1]!.replace(/\nNODE\s*$/, ''), encoding: 'utf8', diff --git a/packages/intent/tests/review-workflow.test.ts b/packages/intent/tests/review-workflow.test.ts index 632cdc88..044a16ce 100644 --- a/packages/intent/tests/review-workflow.test.ts +++ b/packages/intent/tests/review-workflow.test.ts @@ -8,7 +8,7 @@ import { writeFileSync, } from 'node:fs' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { delimiter, join } from 'node:path' import { fileURLToPath } from 'node:url' import { parse } from 'yaml' import { afterEach, beforeEach, expect, it, vi } from 'vitest' @@ -261,7 +261,7 @@ it('runs the PR gate for maintainer instructions before any review state exists' cwd: root, env: { ...process.env, - PATH: `${join(root, 'bin')}:${process.env.PATH}`, + PATH: [join(root, 'bin'), process.env.PATH].join(delimiter), INTENT_REVIEW_BASE: 'fixture-base', }, } diff --git a/packages/intent/vitest.config.ts b/packages/intent/vitest.config.ts index 3aff6f76..ae456bdb 100644 --- a/packages/intent/vitest.config.ts +++ b/packages/intent/vitest.config.ts @@ -1,5 +1,14 @@ +import { existsSync, realpathSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { delimiter } from 'node:path' import { defineConfig } from 'vitest/config' +const gitBin = 'C:\\Program Files\\Git\\bin' +const pathEnv = + process.platform === 'win32' && existsSync(gitBin) + ? `${gitBin}${delimiter}${process.env.PATH ?? ''}` + : process.env.PATH + export default defineConfig({ test: { name: 'intent', @@ -7,6 +16,11 @@ export default defineConfig({ // Packed-release coverage separately checks the artifact's own metadata. env: { INTENT_WORKFLOW_REF: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # v9.9.9', + GIT_CEILING_DIRECTORIES: [ + ...new Set([tmpdir(), realpathSync(tmpdir())]), + ].join(delimiter), + TERM: 'xterm-256color', + ...(pathEnv ? { PATH: pathEnv } : {}), }, include: ['tests/**/*.test.ts'], }, From d882106be6bd20c71b6ad2f3c105ceb4f5ad42f3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:14:38 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .changeset/windows-tsgo-close-wait.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/windows-tsgo-close-wait.md b/.changeset/windows-tsgo-close-wait.md index 390a7abe..c4f000ee 100644 --- a/.changeset/windows-tsgo-close-wait.md +++ b/.changeset/windows-tsgo-close-wait.md @@ -1,5 +1,5 @@ ---- -'@tanstack/intent': patch ---- - -Spawn the TypeScript 7 `tsgo` compiler process from the parent directory in `withNativeCompiler` while keeping `--cwd` set to the project root, so Windows callers can remove or rename the checked directory immediately after `intent validate` or `intent repair` without hitting `EPERM`. +--- +'@tanstack/intent': patch +--- + +Spawn the TypeScript 7 `tsgo` compiler process from the parent directory in `withNativeCompiler` while keeping `--cwd` set to the project root, so Windows callers can remove or rename the checked directory immediately after `intent validate` or `intent repair` without hitting `EPERM`.