mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge pull request #3115 from deepseek-harness/fix/remove-transform-corpus
fix(ci): Windows pnpm setup isolation and spawn budget alignment
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.md
|
||||
2026-07-29-pnpm-setup-runner-isolation.md: c7c076f34dcd4b905a6bb54411538d6cf61bc1d0
|
||||
2026-07-29-pnpm-setup-runner-isolation.zh.md: 2dd866404a5a799fe33e8b9c70c17787dfed0c2a
|
||||
2026-07-29-pnpm-setup-runner-isolation.md: 40a34460171777a1cf2a8d2fb940d74012113c1c
|
||||
2026-07-29-pnpm-setup-runner-isolation.zh.md: 4c307bc00e980975d35a1a0c3e776d5fe371cec9
|
||||
|
||||
@@ -10,9 +10,9 @@ English | [中文](2026-07-29-pnpm-setup-runner-isolation.zh.md)
|
||||
|
||||
## Decision
|
||||
|
||||
Every `pnpm/action-setup` step in [the primary CI workflow](../../../../.github/workflows/ci.yml) and [the master workflow](../../../../.github/workflows/ci-master.yml) sets `dest: ${{ runner.temp }}/setup-pnpm`. Each runner service owns its temporary directory, so one setup cannot replace another runner's install directory. Persistent store reuse remains separate through `PNPM_CONFIG_STORE_DIR`, as established by the [pnpm provisioning decision](../process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md).
|
||||
Every non-Windows `pnpm/action-setup` step in [the primary CI workflow](../../../../.github/workflows/ci.yml) and [the master workflow](../../../../.github/workflows/ci-master.yml) sets `dest: ${{ runner.temp }}/setup-pnpm`. Each runner service owns its temporary directory, so one setup cannot replace another runner's install directory. The Windows native jobs and the [python SDK exe build](../../../../.github/workflows/build-exe-for-python-sdk.yml) use a separate pnpm executable under `setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}` (not `standalone: true`; the destination keeps that executable apart): the run/attempt/job suffix gives every job a fresh directory even when sequential jobs land on the same self-hosted runner and a previous job leaves a locked @reflink native module. Persistent store reuse remains separate through `PNPM_CONFIG_STORE_DIR`, as established by the [pnpm provisioning decision](../process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md).
|
||||
|
||||
[The workflow regression test](../../../../scripts/ci-workflow.spec.ts) discovers every `pnpm/action-setup` step in `ci.yml` and `ci-master.yml` and rejects one without the runner-private destination. This keeps newly added jobs inside the same isolation boundary.
|
||||
[The workflow regression test](../../../../scripts/ci-workflow.spec.ts) discovers every `pnpm/action-setup` step in `ci.yml`, `ci-master.yml`, and `build-exe-for-python-sdk.yml` and rejects one without the runner-private destination. This keeps newly added jobs inside the same isolation boundary.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
[主 CI 工作流](../../../../.github/workflows/ci.yml)与 [CI master 工作流](../../../../.github/workflows/ci-master.yml)中的每个 `pnpm/action-setup` 步骤都设置 `dest: ${{ runner.temp }}/setup-pnpm`。每个 runner 服务独占自己的临时目录,因此一个设置过程无法替换另一个 runner 的安装目录。持久 store 的复用仍由 `PNPM_CONFIG_STORE_DIR` 独立处理,遵循 [pnpm 配置决策](../process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md)。
|
||||
[主 CI 工作流](../../../../.github/workflows/ci.yml)与 [CI master 工作流](../../../../.github/workflows/ci-master.yml)中的每个**非 Windows** `pnpm/action-setup` 步骤都设置 `dest: ${{ runner.temp }}/setup-pnpm`。每个 runner 服务独占自己的临时目录,因此一个设置过程无法替换另一个 runner 的安装目录。Windows 原生作业与 [python SDK exe 构建](../../../../.github/workflows/build-exe-for-python-sdk.yml)在 `setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}` 下使用独立的 pnpm 可执行文件(非 `standalone: true`,目录本身起分离作用):run/attempt/job 后缀让每次作业都使用全新目录,即使顺序作业落到同一自托管 runner、且前一作业留下被锁定的 @reflink 原生模块。持久 store 的复用仍由 `PNPM_CONFIG_STORE_DIR` 独立处理,遵循 [pnpm 配置决策](../process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md)。
|
||||
|
||||
[工作流回归测试](../../../../scripts/ci-workflow.spec.ts)会找出 `ci.yml` 与 `ci-master.yml` 中的每个 `pnpm/action-setup` 步骤,并拒绝缺少 runner 专属目标目录的步骤。这可确保后续新增的作业也处于同一隔离边界内。
|
||||
[工作流回归测试](../../../../scripts/ci-workflow.spec.ts)会找出 `ci.yml`、`ci-master.yml` 与 `build-exe-for-python-sdk.yml` 中的每个 `pnpm/action-setup` 步骤,并拒绝缺少 runner 专属目标目录的步骤。这可确保后续新增的作业也处于同一隔离边界内。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
|
||||
|
||||
- name: Enable Windows Developer Mode (symlink support)
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
@@ -421,16 +421,9 @@ jobs:
|
||||
run: >-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
|
||||
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
|
||||
# Best-effort: a torn-down job on this self-hosted pool can leave a
|
||||
# locked @reflink native module under the action's install destination,
|
||||
# and pnpm/action-setup's self-installer then fails its unlink with
|
||||
# EPERM. Clearing the destination gives every attempt fresh state.
|
||||
- name: Clear stale pnpm setup state
|
||||
shell: pwsh
|
||||
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
@@ -453,7 +446,7 @@ jobs:
|
||||
env:
|
||||
DSH_COVERAGE_MAX_WORKERS: '6'
|
||||
DSH_COVERAGE_PARTITIONS: '4'
|
||||
DSH_COVERAGE_TEST_TIMEOUT_MS: '30000'
|
||||
DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'
|
||||
DSH_GATE_CONCURRENCY: '3'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -464,13 +457,9 @@ jobs:
|
||||
run: >-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
|
||||
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
|
||||
# Best-effort stale pnpm-destination cleanup; rationale on windows-build's copy.
|
||||
- name: Clear stale pnpm setup state
|
||||
shell: pwsh
|
||||
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
@@ -502,13 +491,9 @@ jobs:
|
||||
run: >-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
|
||||
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
|
||||
# Best-effort stale pnpm-destination cleanup; rationale on windows-build's copy.
|
||||
- name: Clear stale pnpm setup state
|
||||
shell: pwsh
|
||||
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
@@ -520,7 +505,7 @@ jobs:
|
||||
run: >-
|
||||
pnpm exec vitest run
|
||||
--no-file-parallelism
|
||||
--testTimeout 30000
|
||||
--testTimeout 90000
|
||||
packages/shell/tool-pwsh/tests/loader.spec.ts
|
||||
packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts
|
||||
packages/workflow/tool-ralph/tests/integration.spec.ts
|
||||
@@ -548,13 +533,9 @@ jobs:
|
||||
run: >-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
|
||||
/t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
|
||||
# Best-effort stale pnpm-destination cleanup; rationale on windows-build's copy.
|
||||
- name: Clear stale pnpm setup state
|
||||
shell: pwsh
|
||||
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js
|
||||
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.PRIMARY_NODE_VERSION }}
|
||||
|
||||
@@ -19,6 +19,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
/** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */
|
||||
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
// The dsh built bin cold-starts slowly on the contended self-hosted Windows pool; the
|
||||
// execa deadline, its error text, the outer vitest case budget, and waitForFile all
|
||||
// share this value so a widening cannot leave a stale 25s diagnostic behind.
|
||||
const SPAWN_TIMEOUT_MS = 60_000
|
||||
// The release version, including a prerelease such as 0.0.1-rc.1: `--version`
|
||||
// prints what this manifest carries, so no test may pin it to a literal.
|
||||
const cliVersion = (JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }).version
|
||||
@@ -36,7 +40,7 @@ async function runBuiltBin(
|
||||
)
|
||||
const result = await execa(process.execPath, [dshBin, ...args], {
|
||||
input: '',
|
||||
timeout: 25_000,
|
||||
timeout: SPAWN_TIMEOUT_MS,
|
||||
killSignal: 'SIGKILL',
|
||||
reject: false,
|
||||
env: childEnv,
|
||||
@@ -44,13 +48,13 @@ async function runBuiltBin(
|
||||
...cwd === undefined ? {} : { cwd },
|
||||
})
|
||||
if (result.timedOut) {
|
||||
throw new Error(`dsh built bin did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
|
||||
throw new Error(`dsh built bin did not exit within ${SPAWN_TIMEOUT_MS / 1_000}s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
|
||||
}
|
||||
return { stdout: result.stdout, code: result.exitCode ?? -1, stderr: result.stderr }
|
||||
}
|
||||
|
||||
async function waitForFile(file: string): Promise<void> {
|
||||
const deadline = Date.now() + 20_000
|
||||
const deadline = Date.now() + SPAWN_TIMEOUT_MS
|
||||
while (!existsSync(file)) {
|
||||
if (Date.now() >= deadline) throw new Error(`dsh profile lifecycle marker did not appear: ${file}`)
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
@@ -146,6 +150,8 @@ function startProfileLifecycle(fixture: ProfileLifecycleFixture, args: readonly
|
||||
return execa(process.execPath, [dshBin, '--profile', 'lifecycle', ...args], {
|
||||
cwd: fixture.home,
|
||||
input: '',
|
||||
timeout: SPAWN_TIMEOUT_MS,
|
||||
killSignal: 'SIGKILL',
|
||||
reject: false,
|
||||
env: {
|
||||
DSH_HOME: fixture.home,
|
||||
@@ -310,7 +316,7 @@ function startStartupProfile(fixture: StartupFixture, args: readonly string[]) {
|
||||
cwd: fixture.home,
|
||||
input: '',
|
||||
reject: false,
|
||||
timeout: 25_000,
|
||||
timeout: SPAWN_TIMEOUT_MS,
|
||||
killSignal: 'SIGKILL',
|
||||
env: {
|
||||
DSH_HOME: fixture.home,
|
||||
@@ -335,7 +341,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
const result = await runBuiltBin(removed)
|
||||
expect(result.code).toBe(1)
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS * 3 + 30_000)
|
||||
|
||||
it('routes help and usage errors without activating startup-dependent rows', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-app-help-'))
|
||||
@@ -392,7 +398,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, process.platform === 'win32' ? 60_000 : 30_000)
|
||||
}, SPAWN_TIMEOUT_MS * 3 + 30_000)
|
||||
|
||||
it('reports SDK startup failure when stdin reaches EOF first', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-built-sdk-startup-failure-'))
|
||||
@@ -416,14 +422,14 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('serves the SDK protocol through the sdk profile and exits after shutdown', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-built-sdk-'))
|
||||
const child = execa(process.execPath, [dshBin, '--profile', 'sdk'], {
|
||||
cwd: home,
|
||||
reject: false,
|
||||
timeout: 25_000,
|
||||
timeout: SPAWN_TIMEOUT_MS,
|
||||
killSignal: 'SIGKILL',
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -471,7 +477,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
await child
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('runs a mock-backed ACP turn through the acp profile and exits on disconnect', async () => {
|
||||
const apiKey = 'built-acp-profile-key'
|
||||
@@ -484,7 +490,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
const child = execa(process.execPath, [dshBin, '--profile', 'acp'], {
|
||||
cwd: home,
|
||||
reject: false,
|
||||
timeout: 25_000,
|
||||
timeout: SPAWN_TIMEOUT_MS,
|
||||
killSignal: 'SIGKILL',
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -555,7 +561,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
await server.close()
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('runs the headless profile through its app-owned task positional', async () => {
|
||||
const apiKey = 'built-dsh-headless-key'
|
||||
@@ -583,7 +589,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
await server.close()
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('does not load a project environment for --version', async () => {
|
||||
const project = mkdtempSync(join(tmpdir(), 'dsh-version-project-'))
|
||||
@@ -606,7 +612,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('uses the launching endpoint and managed credential through the published entry', async () => {
|
||||
const apiKey = 'built-home-layer-key'
|
||||
@@ -646,7 +652,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
rmSync(project, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('reports a patch-overlay boot failure without hanging', async () => {
|
||||
// The HMR main watcher's initial scan once refreshed the include
|
||||
@@ -666,7 +672,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('lets a profile without a parser ignore app arguments and dispose on a startup-time signal', async () => {
|
||||
const fixture = createProfileLifecycleFixture()
|
||||
@@ -682,7 +688,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
child.kill('SIGKILL')
|
||||
rmSync(fixture.home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('fully settles a custom profile, hot-reloads its patch layer with removal reverting, and disposes on a signal', async () => {
|
||||
const fixture = createProfileLifecycleFixture()
|
||||
@@ -734,7 +740,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
child.kill('SIGKILL')
|
||||
rmSync(fixture.home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('hands the app arguments to the profile, which applies them before its rows start', async () => {
|
||||
const fixture = createStartupFixture()
|
||||
@@ -750,7 +756,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
child.kill('SIGKILL')
|
||||
rmSync(fixture.home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('starts a consumer on its composed value when the invocation carries no app arguments', async () => {
|
||||
const fixture = createStartupFixture()
|
||||
@@ -764,7 +770,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
child.kill('SIGKILL')
|
||||
rmSync(fixture.home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('keeps the app arguments across a user patch reload', async () => {
|
||||
// A live edit recomposes every row while the provider service remains
|
||||
@@ -798,7 +804,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
child.kill('SIGKILL')
|
||||
rmSync(fixture.home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it("prints the app's own help, starts none of its rows, and exits", async () => {
|
||||
const fixture = createStartupFixture()
|
||||
@@ -811,7 +817,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
} finally {
|
||||
rmSync(fixture.home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('anchors a relative add spec to the invoking directory, not the profile', async () => {
|
||||
// `dsh plugin --profile x add .` from a plugin checkout must install THAT
|
||||
@@ -829,7 +835,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
const result = await execa(process.execPath, [dshBin, 'plugin', '--profile', 'anchor', 'add', '.'], {
|
||||
cwd: checkout,
|
||||
input: '',
|
||||
timeout: 60_000,
|
||||
timeout: SPAWN_TIMEOUT_MS,
|
||||
killSignal: 'SIGKILL',
|
||||
reject: false,
|
||||
env: { DSH_HOME: home },
|
||||
@@ -860,7 +866,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
rmSync(checkout, { recursive: true, force: true })
|
||||
}
|
||||
}, 90_000)
|
||||
}, SPAWN_TIMEOUT_MS * 2 + 30_000)
|
||||
|
||||
it('activates a dependency that gained dsh.bundle in a later update', async () => {
|
||||
// Reconcile runs against the INSTALLED state on every successful pnpm
|
||||
@@ -897,7 +903,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS * 2 + 30_000)
|
||||
|
||||
describe('config dump', () => {
|
||||
let home: string
|
||||
@@ -913,7 +919,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
expect(stdout).toContain('# == @deepseek-ai/dsh-base')
|
||||
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
|
||||
expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(false)
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('prints the headless profile without Host or browser layers', async () => {
|
||||
const { stdout, code, stderr } = await runBuiltBin(
|
||||
@@ -926,7 +932,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-host-/)
|
||||
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-web-app'")
|
||||
expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-client-/)
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS + 30_000)
|
||||
|
||||
it('prints the exact standalone sdk-minimal tree without dsh-base', async () => {
|
||||
const { stdout, code, stderr } = await runBuiltBin(
|
||||
@@ -959,7 +965,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
expect(stdout).toContain('# == @deepseek-ai/dsh-sdk-minimal')
|
||||
expect(stdout).not.toContain('@deepseek-ai/dsh-base')
|
||||
expect(stdout).not.toContain('@deepseek-ai/dsh-web-app')
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS * 2 + 30_000)
|
||||
|
||||
it('composes the profile user layer and a --patch overlay in order', async () => {
|
||||
// Auto-init the web profile first, then write its user layer.
|
||||
@@ -998,6 +1004,6 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
// Both layers patched the row; the comment lists them in application order.
|
||||
expect(stdout).toContain(`patched by ${profilePatch}, ${overlay}`)
|
||||
expect(stderr).toContain('patch: entry "absent-row" not found')
|
||||
}, 30_000)
|
||||
}, SPAWN_TIMEOUT_MS * 2 + 30_000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@ async function mountRalph(script: MockScript, config: toolRalph.Config) {
|
||||
}
|
||||
|
||||
describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => {
|
||||
it('uses distinct empty-seed children, shared cwd, and only the prior bounded handoff', { timeout: 30_000 }, async () => {
|
||||
it('uses distinct empty-seed children, shared cwd, and only the prior bounded handoff', { timeout: 90_000 }, async () => {
|
||||
const firstReport = {
|
||||
status: 'continue',
|
||||
summary: 'ROUND_ONE_HANDOFF',
|
||||
@@ -115,7 +115,7 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => {
|
||||
await parentHandle.dispose()
|
||||
})
|
||||
|
||||
it('reports the failed round and last good handoff when a child fails', { timeout: 30_000 }, async () => {
|
||||
it('reports the failed round and last good handoff when a child fails', { timeout: 90_000 }, async () => {
|
||||
const firstReport = {
|
||||
status: 'continue',
|
||||
summary: 'ROUND_ONE_HANDOFF',
|
||||
@@ -235,7 +235,7 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => {
|
||||
await parentHandle.dispose()
|
||||
})
|
||||
|
||||
it('cancels the real worker and fresh child to quiescence', { timeout: 20_000 }, async () => {
|
||||
it('cancels the real worker and fresh child to quiescence', { timeout: 90_000 }, async () => {
|
||||
const { ctx, parent, parentHandle } = await mountRalph(['hang'], { maxRounds: 2 })
|
||||
const children: Agent[] = []
|
||||
const outcomes: string[] = []
|
||||
|
||||
@@ -787,7 +787,7 @@ describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
|
||||
expect(result.error).toContain('raced the completion')
|
||||
expect(narration).toEqual(['started'])
|
||||
await handle.dispose()
|
||||
}, 15_000)
|
||||
}, 90_000)
|
||||
|
||||
it('cancel() force-settles a script parked on a promise no hook owns, and TERMINATES its worker', async () => {
|
||||
const { ctx, parent } = await setup({ config: { provider: 'stub', disposeGraceMs: 50 } })
|
||||
@@ -1027,7 +1027,7 @@ describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
|
||||
expect(provider.runs[0]!.disposeCalls).toBe(1)
|
||||
await handle.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
}, 15_000)
|
||||
}, 90_000)
|
||||
|
||||
it('dispose() on a wedged worker host-drives child disposal inside the grace: it returns with the children DISPOSED, not with their teardown still in flight', async () => {
|
||||
const { ctx, parent, provider } = await setup({
|
||||
@@ -1062,7 +1062,7 @@ describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
|
||||
expect(provider.runs[0]!.disposed).toBe(true)
|
||||
const result = await handle.result
|
||||
expect(result.stopReason).toBe('cancelled')
|
||||
}, 15_000)
|
||||
}, 90_000)
|
||||
|
||||
it('a live child disposed by the dispose() drive is disposed ONCE, and the worker\'s late dispose RPC still gets its ack (the script settles, not the grace)', async () => {
|
||||
const { ctx, parent, provider } = await setup({ manual: true })
|
||||
@@ -1127,7 +1127,7 @@ describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
|
||||
// can finalize its state at run-end without dangling agents.
|
||||
expect(order.indexOf('run-end')).toBe(order.length - 1)
|
||||
await handle.dispose()
|
||||
}, 15_000)
|
||||
}, 90_000)
|
||||
|
||||
it('graceful cancellation keeps pairing worker-authored: exactly one agent-end per start, nothing synthesized on top', async () => {
|
||||
const { ctx, parent, provider } = await setup({ manual: true })
|
||||
@@ -1305,7 +1305,7 @@ describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
|
||||
await Promise.resolve()
|
||||
expect(result.stopReason).toBe('error')
|
||||
await handle.dispose()
|
||||
}, 15_000)
|
||||
}, 90_000)
|
||||
|
||||
it('an uncaught exception inside the worker surfaces as an error result and reaps the in-flight child', async () => {
|
||||
const { ctx, parent, provider } = await setup({ manual: true })
|
||||
@@ -1331,7 +1331,7 @@ describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
|
||||
expect(provider.runs[0]!.disposed).toBe(true)
|
||||
}, 1000)
|
||||
await handle.dispose()
|
||||
}, 15_000)
|
||||
}, 90_000)
|
||||
|
||||
it('a worker death pairs every stranded start: the synthesized cancelled agent-end precedes the error workflow/end', async () => {
|
||||
const { ctx, parent, provider } = await setup({ manual: true })
|
||||
@@ -1366,7 +1366,7 @@ describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
|
||||
])
|
||||
expect(order.indexOf('run-end')).toBe(order.length - 1)
|
||||
await handle.dispose()
|
||||
}, 15_000)
|
||||
}, 90_000)
|
||||
|
||||
it('a dispose ack racing the worker death is dropped, not crashed (post after exit)', async () => {
|
||||
// Slow child disposal: the ack resolves only AFTER the worker died, so
|
||||
@@ -1395,7 +1395,7 @@ describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
|
||||
// tight explicit bound (see the helper's doc comment).
|
||||
await waitFor(() => { expect(provider.runs[0]!.disposed).toBe(true) }, 1000)
|
||||
await handle.dispose()
|
||||
}, 15_000)
|
||||
}, 90_000)
|
||||
|
||||
it('a worker death AFTER a cancel reports cancelled, not error', async () => {
|
||||
const { ctx, parent } = await setup({ config: { provider: 'stub', disposeGraceMs: 60_000 } })
|
||||
@@ -1421,7 +1421,7 @@ describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => {
|
||||
expect(result.stopReason).toBe('cancelled')
|
||||
expect(result.error).toContain('stop it')
|
||||
await handle.dispose()
|
||||
}, process.platform === 'win32' ? 30_000 : 15_000)
|
||||
}, process.platform === 'win32' ? 90_000 : 15_000)
|
||||
})
|
||||
|
||||
describe('service API', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const runnerPrivatePnpmDestination = '${{ runner.temp }}/setup-pnpm'
|
||||
const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js'
|
||||
const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}'
|
||||
|
||||
describe('CI workflow', () => {
|
||||
it('isolates every pnpm action setup destination per runner', () => {
|
||||
@@ -36,6 +36,25 @@ describe('CI workflow', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('isolates the python SDK exe pnpm setup destination per job', () => {
|
||||
const workflow: unknown = yaml.load(readFileSync(resolve(root, '.github/workflows/build-exe-for-python-sdk.yml'), 'utf8'))
|
||||
if (!isRecord(workflow) || !isRecord(workflow.jobs)) throw new TypeError('build-exe-for-python-sdk.yml must define jobs')
|
||||
const setups: Array<{ step: unknown }> = []
|
||||
for (const job of Object.values(workflow.jobs)) {
|
||||
if (!isRecord(job) || !Array.isArray(job.steps)) continue
|
||||
for (const step of job.steps) {
|
||||
if (!isRecord(step) || typeof step.uses !== 'string' || !step.uses.startsWith('pnpm/action-setup@')) continue
|
||||
setups.push({ step })
|
||||
}
|
||||
}
|
||||
expect(setups.length).toBeGreaterThan(0)
|
||||
for (const { step } of setups) {
|
||||
expect(step).toMatchObject({
|
||||
with: { dest: nativeWindowsPnpmDestination },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps required Wine and split native Windows jobs with failover, plus a master-only standby', () => {
|
||||
const workflow = loadWorkflow('.github/workflows/ci.yml')
|
||||
const masterWorkflow = loadWorkflow('.github/workflows/ci-master.yml')
|
||||
@@ -115,7 +134,7 @@ describe('CI workflow', () => {
|
||||
))
|
||||
const nativeTestCommand = nativeTestCommands.map(step => step.run).join('\n')
|
||||
expect(nativeTestCommand).toContain('--no-file-parallelism')
|
||||
expect(nativeTestCommand).toContain('--testTimeout 30000')
|
||||
expect(nativeTestCommand).toContain('--testTimeout 90000')
|
||||
expect(nativeTestCommand).toContain('tool-pwsh/tests/loader.spec.ts')
|
||||
expect(nativeTestCommand).toContain('workflow-worker-thread.spec.ts')
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ probePromise()
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
}, 20_000)
|
||||
}, 90_000)
|
||||
|
||||
it('runs JavaScript compatibility and nursery rules', async () => {
|
||||
const suffix = randomUUID()
|
||||
@@ -152,7 +152,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
}, 20_000)
|
||||
}, 90_000)
|
||||
|
||||
it('keeps the complete stylistic contract in Oxlint', async () => {
|
||||
const oxlintPath = join(repositoryRoot, '.oxlintrc.json')
|
||||
@@ -252,7 +252,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
}, 20_000)
|
||||
}, 90_000)
|
||||
|
||||
it('accepts an ignored-only staged selection', () => {
|
||||
const result = runOxlint([
|
||||
@@ -371,6 +371,6 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
},
|
||||
20_000,
|
||||
90_000,
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user