Browse Source

Merge pull request #3926 from deepseek-harness/ci/blacksmith-hosted-image-fixes

test/ci: remove the host-paced assumptions the hosted coverage lanes hit
Chinesezjc 1 week ago
parent
commit
1050091d82

+ 6 - 0
.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/testing/2026-09-10-hosted-image-test-assumptions.md
+2026-09-10-hosted-image-test-assumptions.md: e6cd1ef37dce318920d75545f816006a98989076
+2026-09-10-hosted-image-test-assumptions.zh.md: e60910aebdc2952a2bfb6ab25963a973ea41ac87

+ 39 - 0
.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.md

@@ -0,0 +1,39 @@
+# Agent Note: Hosted-image assumptions in the coverage suite
+
+Status: implemented
+
+English | [中文](2026-09-10-hosted-image-test-assumptions.zh.md)
+
+## Problem
+
+The [failover leg](../process/2026-09-09-blacksmith-failover-leg.md) runs this suite on pools this repository does not own — Blacksmith's ephemeral images, and the in-house `vm-backup` and `dsh-win-ci` standbys. On the hosted image the coverage lanes failed on host properties their cases never named: whether the host offered a usable user-systemd scope decided which containment a mocked PTY exit raced; the wall-clock grace a managed scope needed before it could take a `SIGKILL` was below what a loaded image provides; a starved reader coalesced writes the illegal-UTF-8 residual cases assumed arrived as separate chunks; and a Windows Server image refuses `CoCreateInstance(CLSID_FileOpenDialog)` outright.
+
+## Decision
+
+Every case names the host property it depends on, so the same revision reports the same verdict on the in-house pool and on a hosted image.
+
+Terminal cases that drive a mocked PTY exit pin the containment they need (`internals = { platform: 'darwin' }` in `packages/subprocess/subprocess-local/tests/local.spec.ts`); under the host's native scope the mocked exit races the scope bootstrap and fails as `terminal scope exited before its bootstrap consumed the launch request`. Mocking the `linux-scope.ts` probes to reach the same path was removed: the platform pin skips both probes, so the mock could not change the selected path.
+
+`disposal contains a spawn-failure rejection that races teardown` asserts the settlement contract instead of one winner of the race: a bootstrap that published its pre-exec failure rejects with that failure, and a teardown that stopped the bootstrap first settles as the requested `SIGTERM`. Only the Linux scope records the stopped arm, because the win32 job owner turns a cancelled start into a rejection and the fallback launcher rejects the missing directory.
+
+`plugin-config dispose graces reach the real ACP run` configures 5000ms dispose graces. At 150ms the hosted image escalated while the scope could not take the signal — `systemctl` failed the kill (`Failed to send signal SIGKILL to auxiliary processes: Invalid argument`) and the teardown reported a failure the configuration never asked for. Its mock refuses stdin EOF and `SIGTERM` by design, so the case waits out both graces (~10s) and carries a 30s case budget, above the 5000ms default the local unit entry grants.
+
+Both illegal-UTF-8 residual cases in `packages/experimental/code-runtime-python/tests/runtime.spec.ts` pace their writes with `time.sleep(0.001)`: `os.sched_yield()` lets a loaded reader coalesce the writes into one chunk, and the coalesced chunk is what the wrapped `Buffer.concat` measures (the hosted image measured 2563 against the 2048 bound with a correct implementation). Their payloads stay above that bound — 3200 bytes for the `0xFF` case and 1100 `ED A0 80` sequences, 3300 raw bytes, for the CESU-8 case, past the 3072-byte budget a raw-byte undercount reaches — so the undercount still flushes above 2048. Each carries a 20s case budget for the paced writes plus the interpreter start.
+
+The Linux coverage lane grants `DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'`, matching the Windows coverage lane, because the disposal cases in `subprocess-local` and `bash-sandbox` exceed the 5000ms default when the lane's partitions, workers, and sibling gates share one host.
+
+The Windows folder-dialog smoke probes `CoCreateInstance(CLSID_FileOpenDialog)` through PowerShell instead of gating on `process.platform`. An image that answers `CLASS_E_CLASSNOTAVAILABLE` (0x80040111) runs the clean-rejection case and skips the real-dialog case, so `win32-dialog.ts` keeps its file coverage without a host that can open a dialog. Every exception from that activation reads as refusal, so a host failing the probe for another reason only loses the real-dialog case; a probe that cannot run at all keeps the win32 assumption.
+
+## Alternatives considered
+
+**Excluding the coverage lanes from the hosted leg.** Rejected: the leg exists to run the same suite on another pool, and the failures named real host dependencies rather than a suite the pool cannot support.
+
+**Raising only the lane's per-test budget.** Rejected: a wider budget does not change the cases whose cost or behaviour is deterministic — a trapped ACP child still waits out both graces, and a coalesced reader still inflates the measured peak.
+
+**Keeping the `linux-scope.ts` probe mocks.** Rejected as inert: the platform pin selects the fallback path before either probe is called, so the mock changed no execution path.
+
+**Cutting the illegal-UTF-8 payloads to keep the cases fast.** Rejected: below the 2048 bound the assertion can no longer fail for the undercount it names, which leaves the regression unguarded.
+
+## Consequences
+
+The suite's verdict no longer depends on which pool served the lane, at the cost of fixtures pinned to one containment choice: the `linux-scope` and win32-job paths keep their own dedicated cases instead of being reached through these ones. The ACP dispose case costs about 10s of wall clock per run and each residual case about 3.5s, all deterministic rather than host-paced. Hosted-image evidence: [run 34449848541](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34449848541) failed on these cases, [run 34457655892](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34457655892) is green with this diff plus the 90000ms lane budget, and `windows node 24 / coverage` is green on five consecutive hosted runs, where the probe reports the refusal (`clsid-probe=refused`).

+ 39 - 0
.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.zh.md

@@ -0,0 +1,39 @@
+# Agent Note: coverage 套件里的托管镜像假设
+
+Status: implemented
+
+[English](2026-09-10-hosted-image-test-assumptions.md) | 中文
+
+## 问题
+
+[故障切换支路](../process/2026-09-09-blacksmith-failover-leg.zh.md)会把这套测试跑在本仓库不拥有的池上——Blacksmith 的临时镜像,以及自有的 `vm-backup` 与 `dsh-win-ci` 备用池。在托管镜像上,coverage 各通道的失败来自用例从未点明的宿主属性:宿主是否提供可用的用户级 systemd scope,决定了被 mock 的 PTY 退出会与哪种 containment 竞争;托管 scope 接受 `SIGKILL` 之前所需的墙钟宽限,低于负载镜像实际提供的量;读端被抢占时会把非法 UTF-8 残余用例假定为独立分块的写入合并成一个分块;以及 Windows Server 镜像直接拒绝 `CoCreateInstance(CLSID_FileOpenDialog)`。
+
+## 决策
+
+每个用例都点明它依赖的宿主属性,因此同一份修订在自有池与托管镜像上给出同样的结论。
+
+驱动被 mock 的 PTY 退出的终端用例钉死自己需要的 containment(`packages/subprocess/subprocess-local/tests/local.spec.ts` 中的 `internals = { platform: 'darwin' }`);在宿主的原生 scope 下,被 mock 的退出会与 scope 的 bootstrap 竞争,并以 `terminal scope exited before its bootstrap consumed the launch request` 失败。为走到同一路径而 mock `linux-scope.ts` 的探针已被删除:平台钉死会让两个探针都不被调用,因此该 mock 无法改变选中的路径。
+
+`disposal contains a spawn-failure rejection that races teardown` 断言结算契约,而不是这场竞争的某一方获胜:已经发布其 pre-exec 失败的 bootstrap 以该失败 reject,先停住 bootstrap 的 teardown 则以被请求的 `SIGTERM` 结算。只有 Linux scope 会记录停止这一支,因为 win32 job owner 会把被取消的启动转成 rejection,fallback 启动器则因目录缺失而 reject。
+
+`plugin-config dispose graces reach the real ACP run` 配置 5000ms 的 dispose 宽限。在 150ms 时,托管镜像在 scope 还无法接受信号时就升级了信号——`systemctl` 的 kill 失败(`Failed to send signal SIGKILL to auxiliary processes: Invalid argument`),teardown 上报了一个配置从未要求的失败。该用例的 mock 按设计既拒绝 stdin EOF 也拒绝 `SIGTERM`,因此用例会等满两个宽限(约 10s),并自带 30s 的用例预算,高于本地单测入口授予的 5000ms 默认值。
+
+`packages/experimental/code-runtime-python/tests/runtime.spec.ts` 的两个非法 UTF-8 残余用例都用 `time.sleep(0.001)` 控制写入节奏:`os.sched_yield()` 会让被抢占的读端把多次写入合并成一个分块,而被包裹的 `Buffer.concat` 测量的正是该分块(在正确实现下,托管镜像测得 2563,超过了 2048 的界)。两个用例的载荷都保持在该界之上——`0xFF` 用例 3200 字节,CESU-8 用例 1100 个 `ED A0 80` 序列(3300 原始字节,超过按原始字节计费会触及的 3072 字节预算)——因此少计仍然会在 2048 之上触发 flush。两者各自带有 20s 的用例预算,容纳带节奏的写入与解释器启动。
+
+Linux coverage 通道授予 `DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'`,与 Windows coverage 通道一致,因为当该通道的分区、worker 与同级门禁共用一个宿主时,`subprocess-local` 与 `bash-sandbox` 的处置用例会超过 5000ms 默认值。
+
+Windows 文件夹对话框冒烟测试改为通过 PowerShell 探测 `CoCreateInstance(CLSID_FileOpenDialog)`,而不再按 `process.platform` 分流。回答 `CLASS_E_CLASSNOTAVAILABLE`(0x80040111)的镜像会跑干净的拒绝用例并跳过真实对话框用例,因此 `win32-dialog.ts` 在没有可开对话框的宿主上仍保有文件覆盖率。该激活过程抛出的任何异常都按拒绝解读,因此因其它原因探测失败的宿主只会失去真实对话框用例;完全无法运行的探测则保留 win32 假设。
+
+## 备选方案
+
+**把 coverage 通道排除出托管支路。** 否决:该支路的意义就是把同一套测试跑在另一个池上,而这些失败点出的是真实的宿主依赖,不是一个该池无法支撑的套件。
+
+**只抬高通道的每用例预算。** 否决:更宽的预算改变不了那些成本或行为确定的用例——被 trap 的 ACP 子进程仍然会等满两个宽限,被合并的读端仍然会抬高测得的峰值。
+
+**保留 `linux-scope.ts` 的探针 mock。** 因无效而否决:平台钉死会在任一探针被调用前就选中 fallback 路径,因此该 mock 没有改变任何执行路径。
+
+**削减非法 UTF-8 的载荷以让用例更快。** 否决:低于 2048 的界之后,断言再也无法为它所点名的少计而失败,等于让该回归失去守护。
+
+## 后果
+
+套件的结论不再取决于哪个池服务了这条通道,代价是被钉在某一 containment 选择上的 fixture:`linux-scope` 与 win32-job 两条路径仍由各自的专用用例覆盖,而不是经由这些用例抵达。ACP 处置用例每次运行约 10s 墙钟,每个残余用例约 3.5s,且都是确定成本而非随宿主浮动。托管镜像证据:[run 34449848541](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34449848541) 在这些用例上失败,[run 34457655892](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34457655892) 在本改动加 90000ms 通道预算下转绿,`windows node 24 / coverage` 在连续五次托管运行中为绿,其中探针报告拒绝(`clsid-probe=refused`)。

+ 8 - 0
.github/workflows/ci.yml

@@ -121,6 +121,14 @@ jobs:
       DSH_COVERAGE_MAX_WORKERS: '6'
       DSH_COVERAGE_PARTITIONS: '4'
       DSH_GATE_CONCURRENCY: '3'
+      # Managed-scope teardown cases run past the default per-test budget when
+      # this lane's partitions, workers, and sibling gates share one host: the
+      # disposal cases in subprocess-local and bash-sandbox exceeded 5000ms on
+      # the hosted image (run 34449848541) while the same commit stayed inside
+      # the budget on the in-house pool; the lane is green with this value
+      # (run 34457655892). The Windows coverage lane grants the same budget for
+      # the same reason.
+      DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'
       # A gate failure aborts the sibling gate instead of waiting out its
       # multi-minute instrumented run.
       DSH_GATE_FAIL_FAST: '1'

+ 21 - 8
packages/experimental/code-runtime-python/tests/runtime.spec.ts

@@ -1517,10 +1517,14 @@ describe('PythonCodeRuntime — programs and bindings', () => {
       const { runtime } = await setup({ maxLogBytes: 3072, maxWallMs: 30_000 })
       result = await runtime.run({
         program: [
-          'import os',
-          'for _ in range(6000):',
+          'import os, time',
+          // One byte per chunk on every host: a plain yield lets a loaded
+          // reader coalesce, and the coalesced chunk is what the bound below
+          // measures. The payload stays above the 2048 discriminator, so a
+          // raw-byte undercount still flushes the whole residual at EOF.
+          'for _ in range(3200):',
           '    os.write(1, b"\\xff")',
-          '    os.sched_yield()',
+          '    time.sleep(0.001)',
           'return None',
         ].join('\n'),
         bindings: [],
@@ -1534,7 +1538,10 @@ describe('PythonCodeRuntime — programs and bindings', () => {
     // merged buffer stays well under 2048. A raw-byte undercount would let it
     // reach ~3072 before flushing, so 2048 discriminates.
     expect(maxConcat).toBeLessThan(2048)
-  })
+    // The paced payload costs ~3.2s deterministically, which is above the
+    // 5000ms default the local unit entry grants, so the case carries its own
+    // bound instead of relying on the lane to widen it.
+  }, 20_000)
 
   it('charges a structurally-valid but illegal UTF-8 sequence its U+FFFD-decoded cost', async () => {
     // A CESU-8 lone surrogate `ED A0 80` is structurally well-formed (a 3-byte
@@ -1559,12 +1566,15 @@ describe('PythonCodeRuntime — programs and bindings', () => {
       const { runtime } = await setup({ maxLogBytes: 3072, maxWallMs: 30_000 })
       result = await runtime.run({
         program: [
-          'import os',
+          'import os, time',
           'seq = (0xed, 0xa0, 0x80)',
-          'for _ in range(2000):',
+          // 1100 sequences are 3300 raw bytes, past the 3072-byte budget a
+          // raw-byte undercount reaches, so the undercount flushes above the
+          // 2048 discriminator instead of only at EOF.
+          'for _ in range(1100):',
           '    for b in seq:',
           '        os.write(1, bytes((b,)))',
-          '        os.sched_yield()',
+          '        time.sleep(0.001)',
           'return None',
         ].join('\n'),
         bindings: [],
@@ -1579,7 +1589,10 @@ describe('PythonCodeRuntime — programs and bindings', () => {
     // largest merged buffer stays well under 2048. Charging the structural width
     // 3 would need ~1024 raw bytes, tripling the peak past 2048.
     expect(maxConcat).toBeLessThan(2048)
-  })
+    // The paced payload costs ~3.3s deterministically, which is above the
+    // 5000ms default the local unit entry grants, so the case carries its own
+    // bound instead of relying on the lane to widen it.
+  }, 20_000)
 
   it('charges a lone surrogate its full six escaped bytes, not three', async () => {
     // A forged `log` frame carrying `\ud800` escapes materializes lone

+ 35 - 9
packages/host/directory-picker-native/tests/win32-dialog.spec.ts

@@ -1,11 +1,12 @@
 /**
  * Driver tests: the child-process message protocol mapped onto the promise,
  * the WM_CLOSE abort service (including the show-race retry and the kill
- * last resort) against fakes, plus the real spawn plumbing — POSIX hosts
- * prove the default path rejects cleanly (koffi cannot load ole32 there),
- * and win32 hosts briefly open and auto-abort a real dialog.
+ * last resort) against fakes, plus the real spawn plumbing: hosts that cannot
+ * open the folder dialog prove the default path rejects cleanly and hosts
+ * that can briefly open and auto-abort a real one.
  */
 
+import { spawnSync } from 'node:child_process'
 import { EventEmitter } from 'node:events'
 import { describe, expect, it, vi } from 'vitest'
 import { pickWin32Directory, type Win32DialogInternals, type Win32DialogWorkerLike } from '../src/win32-dialog.ts'
@@ -41,6 +42,28 @@ function harness(overrides: Partial<Win32DialogInternals> = {}): Harness {
 
 const live = (): AbortSignal => new AbortController().signal
 
+/** CLSID_FileOpenDialog, spelled out here so the probe below owns its own copy. */
+const CLSID_FILE_OPEN_DIALOG = 'dc1c5a9c-e88a-4dde-a5a1-60f82a20aef7'
+
+/**
+ * Whether this host can open the shell's folder dialog. Some Windows Server
+ * images answer `CoCreateInstance(CLSID_FileOpenDialog)` with
+ * CLASS_E_CLASSNOTAVAILABLE (0x80040111) instead of an object, so no dialog
+ * can ever appear there. PowerShell activates the class outside the code
+ * under test, which keeps a GUID or vtable regression in `src` failing the
+ * smoke below instead of silently skipping it. Every exception from that
+ * activation reads as refusal, so a host that fails the probe for another
+ * reason only loses the real-dialog case; a probe that cannot run at all —
+ * no `powershell.exe` — keeps the win32 assumption.
+ */
+const opensRealDialog = process.platform === 'win32' && spawnSync('powershell.exe', [
+  '-NoLogo',
+  '-NoProfile',
+  '-NonInteractive',
+  '-Command',
+  `try { [void][Activator]::CreateInstance([Type]::GetTypeFromCLSID([Guid]'${CLSID_FILE_OPEN_DIALOG}')) } catch { exit 1 }`,
+], { encoding: 'utf8' }).status !== 1
+
 describe('pickWin32Directory', () => {
   it('resolves the selected path and the cancellation null', async () => {
     const first = harness()
@@ -145,15 +168,18 @@ describe('pickWin32Directory', () => {
     expect(close.mock.calls.length).toBeGreaterThan(10)
   })
 
-  // POSIX hosts exercise the REAL default plumbing end to end: the tsx-bootstrapped
-  // worker spawns, loads koffi, fails to load ole32.dll, and reports the error.
-  it.skipIf(process.platform === 'win32')('rejects through the real worker where the Win32 surface is unavailable', async () => {
+  // Hosts without the folder dialog exercise the REAL default plumbing end to
+  // end: the tsx-bootstrapped worker spawns, loads koffi, and reports the
+  // refusal — ole32.dll fails to load on POSIX, and a Windows Server image
+  // that refuses the dialog class reports the same rejection.
+  it.skipIf(opensRealDialog)('rejects through the real worker where the Win32 surface is unavailable', async () => {
     await expect(pickWin32Directory(live())).rejects.toThrow('win32 folder dialog failed')
   }, 30_000)
 
-  // win32 hosts run the true COM smoke instead: a real dialog opens briefly
-  // and the abort service closes it (the same lever a disconnecting client pulls).
-  it.skipIf(process.platform !== 'win32')('opens and abort-closes a real dialog', async () => {
+  // Hosts with the dialog run the true COM smoke instead: a real dialog opens
+  // briefly and the abort service closes it (the same lever a disconnecting
+  // client pulls).
+  it.skipIf(!opensRealDialog)('opens and abort-closes a real dialog', async () => {
     const controller = new AbortController()
     setTimeout(() => {
       controller.abort()

+ 15 - 5
packages/subagent/subagent-acp/tests/subagent-acp.spec.ts

@@ -1412,17 +1412,27 @@ describe('dsh-subagent-acp', () => {
       args: [mockServer],
       permission: 'reject',
       env: { MOCK_TRAP_SIGTERM: '1', MOCK_TEXT: 'x', MOCK_READY_FILE: ready },
-      disposeEofGraceMs: 150,
-      disposeGraceMs: 150,
+      // The graces are wall-clock budgets for the managed scope's teardown on a
+      // shared host. At 150ms the hosted image escalated while the scope could
+      // not take the signal and systemctl failed the kill ("Failed to send
+      // signal SIGKILL to auxiliary processes: Invalid argument"), surfacing as
+      // a teardown failure the configuration never asked for. The case asserts
+      // that config graces reach the real run, so the value only has to clear
+      // the host's scope handling.
+      disposeEofGraceMs: 5_000,
+      disposeGraceMs: 5_000,
     })
     const run = await ctx.subagents.start('acp', request())
     expect(start).toHaveBeenCalledExactlyOnceWith(expect.anything(), expect.objectContaining({
-      disposeEofGraceMs: 150,
-      disposeGraceMs: 150,
+      disposeEofGraceMs: 5_000,
+      disposeGraceMs: 5_000,
     }))
     await waitForFile(ready, task.timeout)
     await expect(run.dispose()).resolves.toBeUndefined()
-  })
+    // The trapped child refuses stdin EOF, so the ladder waits out the EOF
+    // grace and then the SIGTERM grace (10s) before the SIGKILL settles it.
+    // That fixed cost is above the 5000ms default the local unit entry grants.
+  }, 30_000)
 
   it('rejects a dispose grace outside the Node timer range at load', async () => {
     for (const bad of [

+ 22 - 2
packages/subprocess/subprocess-local/tests/local.spec.ts

@@ -393,6 +393,8 @@ describe('LocalSubprocessRuntime', () => {
       const ctx = new Context()
       const fiber = await ctx.plugin(IsolatedLocalSubprocessRuntime)
       const service = ctx.subprocess as InstanceType<typeof IsolatedLocalSubprocessRuntime>
+      // Pins the containment choice: with the host's native scope a mocked PTY
+      // exit races the scope bootstrap.
       service.internals = { platform: 'darwin' }
       const handle = await ctx.subprocess.spawnTerminal({
         argv: ['shell'], cwd: process.cwd(), rows: 24, cols: 80, graceMs: 1,
@@ -600,6 +602,8 @@ describe('LocalSubprocessRuntime', () => {
       ctx.logger.error = ((error: unknown) => { disposalErrors.push(error) }) as typeof ctx.logger.error
       const fiber = await ctx.plugin(IsolatedLocalSubprocessRuntime)
       const alive = new Set([124])
+      // Pins the containment choice: with the host's native scope a mocked PTY
+      // exit races the scope bootstrap.
       ;(ctx.subprocess as InstanceType<typeof IsolatedLocalSubprocessRuntime>).internals = { platform: 'darwin' }
       ;(ctx.subprocess as InstanceType<typeof IsolatedLocalSubprocessRuntime>).terminalInspector = {
         foregroundPgid: () => 123,
@@ -878,10 +882,26 @@ describe('LocalSubprocessRuntime', () => {
     const ctx = new Context()
     const fiber = await ctx.plugin(LocalSubprocessRuntime)
     // Dispose before the rejection continuation removes the handle from the
-    // live set, so teardown itself must swallow the rejected done.
+    // live set, so teardown itself must swallow the rejected done. Two
+    // settlements are valid and the winner is a race: a bootstrap that
+    // publishes its pre-exec failure rejects with that failure, and a teardown
+    // that stops the bootstrap first settles as the requested termination —
+    // the recorded failure only outranks the stop when it was published before
+    // the stop landed.
     const handle = ctx.subprocess.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-subprocess-test' }))
     await fiber.dispose()
-    await expect(handle.done).rejects.toThrow()
+    const settlement = await handle.done.then(
+      outcome => ({ kind: 'stopped' as const, outcome }),
+      (error: unknown) => ({ kind: 'failed' as const, error }),
+    )
+    if (settlement.kind === 'failed') {
+      expect(settlement.error).toBeInstanceOf(Error)
+    } else {
+      // Only the Linux scope records a stop this way: the win32 job owner
+      // rejects a cancelled start and the fallback launcher rejects the ENOENT,
+      // so neither can produce the stopped branch.
+      expect(settlement.outcome.signal).toBe('SIGTERM')
+    }
   })
 
   it('loading a second implementation throws (one processes service per context — cordis standard)', async () => {