From f45ca2d9acdbb295e13b6e7eac7daa717ef50470 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 10 Sep 2026 17:36:15 +0800 Subject: [PATCH] test: drop two host-paced assumptions the hosted image exposes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ACP plugin-config case drove a real dispose with 150ms graces. On the hosted image the SIGKILL escalation landed while the managed scope could not take the signal — systemctl failed the kill ("Failed to send signal SIGKILL to auxiliary processes: Invalid argument") — so the run reported a teardown failure the configuration never asked for. The case asserts that config graces reach the real run, so the values only need to clear the host's scope handling. Its mock refuses stdin EOF and SIGTERM by design, so the case waits out both graces and carries its own budget for that fixed cost. Both illegal-UTF-8 residual cases relied on a plain sched_yield leaving each write as its own read chunk. A starved reader coalesces hundreds of bytes into one chunk, which inflates the measured peak for correct accounting too: the hosted image measured 2563 against the 2048 bound. Pace with a real sleep so each byte is its own chunk everywhere, and keep each payload above the bound (3200 bytes; 1100 CESU-8 sequences) so a raw-byte undercount still flushes past it instead of only at EOF. --- .../code-runtime-python/tests/runtime.spec.ts | 29 ++++++++++++++----- .../subagent-acp/tests/subagent-acp.spec.ts | 20 +++++++++---- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/experimental/code-runtime-python/tests/runtime.spec.ts b/packages/experimental/code-runtime-python/tests/runtime.spec.ts index 4fa4f225d4..b89ec692b0 100644 --- a/packages/experimental/code-runtime-python/tests/runtime.spec.ts +++ b/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 diff --git a/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts b/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts index 1de31c00ae..cea3e4536f 100644 --- a/packages/subagent/subagent-acp/tests/subagent-acp.spec.ts +++ b/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 [