Fix review findings: validate the hooks cap, integer read caps, doc drift, config plumb-through test

A Codex review pass on the draft caught four real gaps and two solid
suggestions; all addressed except one pushed back on the merits:

- hooks-claude/hooks-codex: stderrSummaryMaxChars was the one new knob
  with NO range validation — a negative/NaN cap would silently
  misbehave inside slice(). Both bridges now assert a positive integer
  at the TOP of apply() (before the config-file parse's early return,
  so a bad value fails the load loudly), with rejection tests.
- tool-fs: the read caps count lines/chars/bytes, so positive-FINITE
  was too loose (a fractional readLimit would flow into windowing
  arithmetic and the schema description). All four now require a
  positive integer, matching tool-web's cap.
- Doc drift the gates cannot catch: tool-web's README tools table
  still named WEB_SEARCH_MAX_RESULTS as the mechanism; compact-basic's
  README/module doc and the compaction-capability-seam RFC still
  described estimation as fixed char/4 rather than the charsPerToken
  default.
- subagent-acp: the dispose graces were tested only at the
  startAcpRun level, so a regression that stopped threading plugin
  config into AcpRunSpec would have survived. A provider-path test now
  drives the trap-escalation scenario through ctx.subagents.start with
  small config graces and bounds dispose at 4s.

Pushed back on: converting compact-basic's charsPerToken to a
schemastery field. The package's whole config is deliberately
hand-rolled (resolveConfig, every threshold REQUIRED with no default —
a documented design posture); one schemastery field beside it would be
incoherent. The knob is cordis.yml-reachable, defaulted, and validated,
which is what the convention requires; migrating the package to
schemastery wholesale is pre-existing config-surface hygiene out of
this change's scope.
This commit is contained in:
Tianyi Cui
2026-07-04 18:06:35 +08:00
parent 774d460889
commit 48d25cdd44
11 changed files with 90 additions and 17 deletions

View File

@@ -398,6 +398,37 @@ describe('dsh-subagent-acp', () => {
await run.dispose()
})
it('plugin-config dispose graces reach the run (SIGKILL escalation through the provider)', async () => {
// Same trap scenario as the direct startAcpRun escalation test, but the
// graces arrive via the PLUGIN CONFIG through the registered provider — so a
// regression that stops threading config into AcpRunSpec (falling back to
// the 6s/3s defaults) blows past the 4000ms bound and fails loud.
const tmp = mkdtempSync(join(tmpdir(), 'acp-cfg-trap-'))
const ready = join(tmp, 'trap-armed')
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(acp, {
providerName: 'acp',
command: process.execPath,
args: ['--import', tsxLoader, mockServer],
permission: 'reject',
env: { MOCK_TRAP_SIGTERM: '1', MOCK_TEXT: 'x', MOCK_READY_FILE: ready, TSX_TSCONFIG_PATH: repoTsconfig },
disposeEofGraceMs: 150,
disposeGraceMs: 150,
})
const run = ctx.subagents.start('acp', { prompt: [{ type: 'text', text: 'p' }], parent: fakeParent })
await waitForFile(ready)
await expect(Promise.race([
run.dispose(),
new Promise((_r, reject) => { setTimeout(() => { reject(new Error('dispose did not return — config graces not threaded to the run')) }, 4000) }),
])).resolves.toBeUndefined()
await ctx.fiber.dispose()
} finally {
rmSync(tmp, { recursive: true, force: true })
}
})
it('rejects a non-positive dispose grace at load', async () => {
for (const bad of [{ disposeEofGraceMs: 0 }, { disposeGraceMs: -1 }, { disposeEofGraceMs: Number.NaN }]) {
const ctx = new Context()