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:
Huanqi Cao
2026-08-12 01:11:46 +08:00
committed by Chinesezjc
parent 54cc6033a6
commit 4ed036da1c
9 changed files with 137 additions and 18 deletions

View File

@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import type { Worker } from 'node:worker_threads'
import { Context } from '@deepseek-ai/cordis'
@@ -9,6 +10,7 @@ import type { SubagentCapabilities, SubagentProvider, SubagentResult, SubagentRu
import type { WorkflowMeta, WorkflowResult, WorkflowResultInfo, WorkflowRun, WorkflowRunInfo } from '@deepseek-ai/dsh-workflow'
import * as workerEngineModule from '../src/index.ts'
import WorkerThreadWorkflowEngine, { type Config } from '../src/index.ts'
import { workerSpawnEnv } from '../src/host.ts'
import { HostToWorkerType, WorkerToHostType } from '../src/protocol.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -559,24 +561,49 @@ describe('dsh-workflow-worker-thread', () => {
expect(result.value).toBe('fine')
})
it('the worker spawns with an EMPTY environment: an escaped script finds no ambient credentials', async () => {
it('the worker spawns with a scrubbed environment: an escaped script finds no ambient credentials', async () => {
const { ctx, parent } = await setup()
// A canary in the HARNESS process's env: with an inherited environment
// the escape below would read it back (exactly how DEEPSEEK_API_KEY
// would leak); env: {} in the spawn options is what keeps it out.
// would leak); the worker env keeps every ambient variable out. Windows
// additionally receives the host temp path (TMP/TEMP) so `os.tmpdir()`
// inside the worker resolves instead of degrading to a cwd-relative
// `undefined\temp` (tsx writes its transform cache there).
process.env.WORKFLOW_ENV_CANARY = 'leak me'
try {
const result = await run(ctx, parent, scripted(`
const proc = ${ESCAPE}
return { canary: proc.env.WORKFLOW_ENV_CANARY ?? null, keys: Object.keys(proc.env).length }
return { canary: proc.env.WORKFLOW_ENV_CANARY ?? null, keys: Object.keys(proc.env).sort() }
`))
expect(result.stopReason).toBe('completed')
expect(result.value).toEqual({ canary: null, keys: 0 })
const expectedKeys = process.platform === 'win32' ? ['TEMP', 'TMP'] : []
expect(result.value).toEqual({ canary: null, keys: expectedKeys })
} finally {
delete process.env.WORKFLOW_ENV_CANARY
}
})
it('workerSpawnEnv injects the host temp path on win32 and leaves the POSIX peer empty', () => {
const tmp = tmpdir()
expect(workerSpawnEnv('win32')).toEqual({ TMP: tmp, TEMP: tmp })
expect(workerSpawnEnv('linux')).toEqual({})
})
it('workerSpawnEnv forwards TSX_TSCONFIG_PATH when the snapshot harness pins it', () => {
const tsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
vi.stubEnv('TSX_TSCONFIG_PATH', tsconfig)
try {
expect(workerSpawnEnv('linux')).toEqual({ TSX_TSCONFIG_PATH: tsconfig })
expect(workerSpawnEnv('win32')).toEqual({
TMP: tmpdir(),
TEMP: tmpdir(),
TSX_TSCONFIG_PATH: tsconfig,
})
} finally {
vi.unstubAllEnvs()
}
})
it('the unbuilt worker forwards exactly TSX_TSCONFIG_PATH through the scrub: the paths-map pin survives, secrets do not', async () => {
const { ctx, parent } = await setup()
// The ACP snapshot harness runs the parent with its cwd OUTSIDE the
@@ -589,10 +616,13 @@ describe('dsh-workflow-worker-thread', () => {
try {
const result = await run(ctx, parent, scripted(`
const proc = ${ESCAPE}
return { keys: Object.keys(proc.env), tsconfig: proc.env.TSX_TSCONFIG_PATH }
return { keys: Object.keys(proc.env).sort(), tsconfig: proc.env.TSX_TSCONFIG_PATH }
`))
expect(result.stopReason).toBe('completed')
expect(result.value).toEqual({ keys: ['TSX_TSCONFIG_PATH'], tsconfig })
const expectedKeys = process.platform === 'win32'
? ['TEMP', 'TMP', 'TSX_TSCONFIG_PATH']
: ['TSX_TSCONFIG_PATH']
expect(result.value).toEqual({ keys: expectedKeys, tsconfig })
} finally {
delete process.env.TSX_TSCONFIG_PATH
delete process.env.WORKFLOW_ENV_CANARY