mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix: Windows-native CI findings on latest master
Local run of check:ci:windows-complete (the windows-native gate) on latest master surfaced five Windows-only failures, all unreachable by current CI because the native windows job is disabled and the wine gate only covers build+site. - install-lefthook/translation-pairing-merge specs junctioned the real scripts/ and tsx package into fixtures; Windows recursive deletion (Node rmSync and git worktree remove) follows MOUNT_POINT junctions and deleted the repository's own directories mid-run. Fixtures now unlink their reparse points before any recursive removal (shared helper in scripts/test-fixture-cleanup.ts). - workflow-workerthread spawned its worker with an empty env; on Windows os.tmpdir() then degrades to the literal relative path undefined\temp, so tsx wrote its transform cache into a cwd-relative undefined/ directory inside the repo. The worker env now injects the host temp path on win32 (workerSpawnEnv, platform-parameterized and unit-tested on both arms). - workspace-context spec did not stub USERPROFILE (win32 homedir) or a set DSH_HOME, leaking the developer machine's real ~/.dsh/AGENTS.md into discovery. - ui-trajectory client-bundle spec mounted the built artifact without the remote/settingsScope provides the locale plugin needs, so the plugin never activated and no view registered. - subagent temp-fixture cleanup lacked the maxRetries Windows handle release needs under load (EPERM); added retries to the three affected specs and the fixture-cleanup helper.
This commit is contained in:
@@ -16,6 +16,7 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { removeFixtureSafely, unlinkFixtureLinks } from './test-fixture-cleanup.ts'
|
||||
|
||||
const installer = fileURLToPath(new URL('./install-lefthook.mjs', import.meta.url))
|
||||
const pairingMergeDriver = 'scripts/merge-translation-pairing-driver.sh %O %A %B %P'
|
||||
@@ -40,7 +41,7 @@ interface CommandResult {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const fixture of fixtures.splice(0)) rmSync(fixture, { recursive: true, force: true })
|
||||
for (const fixture of fixtures.splice(0)) removeFixtureSafely(fixture)
|
||||
})
|
||||
|
||||
function commandResult(command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv): CommandResult {
|
||||
@@ -282,6 +283,10 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
expect(gitResult(fixture, fixture.main, ['config', '--file', commonConfig, '--get', 'core.bare']).status).toBe(1)
|
||||
|
||||
const mainHookBeforeRemoval = readFileSync(join(mainHooks, 'pre-commit'), 'utf8')
|
||||
// Windows Git follows the fixture's MOUNT_POINT junctions into their real
|
||||
// targets while removing a worktree; unlink them first so the removal
|
||||
// cannot delete the repository's scripts/ or tsx package.
|
||||
unlinkFixtureLinks(fixture.linked)
|
||||
git(fixture, fixture.main, ['worktree', 'remove', '--force', fixture.linked])
|
||||
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBeforeRemoval)
|
||||
expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy hook\n')
|
||||
|
||||
46
scripts/test-fixture-cleanup.ts
Normal file
46
scripts/test-fixture-cleanup.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Junction-safe fixture cleanup for Windows. Test fixtures junction the REAL
|
||||
* `scripts/`, `node_modules`, and tsx package directories so installer probes
|
||||
* resolve through them; Windows recursive deletion — both Node's `rmSync` and
|
||||
* Git's `worktree remove` — follows MOUNT_POINT junctions into their targets
|
||||
* and would delete the repository's own directories. POSIX `unlink`/`rm`
|
||||
* already remove symlinks without following them, so the walk is a no-op
|
||||
* there.
|
||||
*/
|
||||
|
||||
import { lstatSync, readdirSync, rmSync, unlinkSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
/**
|
||||
* Recursively unlink every symbolic link (junction) under `path`.
|
||||
* @param path - the fixture tree whose reparse points are unlinked.
|
||||
*/
|
||||
export function unlinkFixtureLinks(path: string): void {
|
||||
const visit = (entry: string): void => {
|
||||
let stat: ReturnType<typeof lstatSync>
|
||||
try {
|
||||
stat = lstatSync(entry)
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
||||
throw error
|
||||
}
|
||||
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
||||
if (stat.isSymbolicLink()) unlinkSync(entry)
|
||||
return
|
||||
}
|
||||
for (const child of readdirSync(entry)) visit(join(entry, child))
|
||||
}
|
||||
visit(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one fixture tree after its junctions are unlinked (see
|
||||
* {@link unlinkFixtureLinks}). Retries the removal: Windows releases child
|
||||
* process and antivirus file handles asynchronously, and an unretried
|
||||
* `rmSync` fails immediately with EPERM under load.
|
||||
* @param path - the fixture tree to remove.
|
||||
*/
|
||||
export function removeFixtureSafely(path: string): void {
|
||||
unlinkFixtureLinks(path)
|
||||
rmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
/** Integration coverage for automatic and explicit pairing-record conflict resolution. */
|
||||
|
||||
import { execFileSync, spawnSync } from 'node:child_process'
|
||||
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import {
|
||||
chmodSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -15,6 +22,7 @@ import {
|
||||
renderTranslationPairingRecord,
|
||||
translationPairPaths,
|
||||
} from './translation-pairing-record.ts'
|
||||
import { removeFixtureSafely } from './test-fixture-cleanup.ts'
|
||||
|
||||
const driver = fileURLToPath(new URL('./merge-translation-pairing.ts', import.meta.url))
|
||||
const driverLauncher = fileURLToPath(new URL('./merge-translation-pairing-driver.sh', import.meta.url))
|
||||
@@ -28,7 +36,7 @@ interface Fixture {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const fixture of fixtures.splice(0)) rmSync(fixture, { recursive: true, force: true })
|
||||
for (const fixture of fixtures.splice(0)) removeFixtureSafely(fixture)
|
||||
})
|
||||
|
||||
function git(fixture: Fixture, args: string[]): string {
|
||||
|
||||
Reference in New Issue
Block a user