Skip to content

test(e2e): exercise MCP logs cutoff and typed failures against real Compose - #341

Merged
roodboi merged 5 commits into
nextfrom
wf/f13-logs-e2e-b16
Oct 20, 2026
Merged

roodboi merged 5 commits into
nextfrom
wf/f13-logs-e2e-b16

Conversation

@roodboi

@roodboi roodboi commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

HACK-1134 (unit F13) and HACK-1155 (P11). Test and guidance change only; no product code changes.

Final behavior

mcp-logs-compose (docker tier, optional docker-e2e job)

The scenario starts hack mcp serve over stdio with the MCP SDK client. Every tool call runs the CLI under test through HACK_MCP_COMMAND (the compiled dist/hack in CI). The project is a real Compose project:

  • cached alpine:3.20 with pull_policy: never
  • a private internal network and no published ports
  • Hack DNS/TLS turned off, and both log backends set to compose

It has three services:

  • ticker prints a boot marker, then a numbered line every 0.2 s.
  • quiet prints one line.
  • bulk prints 1500 lines of 2048 four-byte emoji (about 12 MiB), then idles.

The scenario requires these results:

Case Required result
Complete A quiet snapshot: ok, cutoff: null, exit 0, its one line and an end event with reason eof
Time cutoff, quiet A quiet tail with maxMs: 2000: {bound: "time", limit: 2000}, stop_reason: timeout, only the one replayed line
Time cutoff, live A ticker tail with maxMs: 3000: time cutoff, at least 5 live lines in strict order, duration_ms from 3000 up to 30000
Event cutoff A ticker tail with maxEvents: 5: {bound: "events", limit: 5}, exactly 5 log events, stop_reason: max_events
Byte cutoff A bulk snapshot: {bound: "bytes", limit: 8388608}, outputTruncated, an ordered prefix of whole 2048-emoji lines (line 1 onward, no BULK-DONE)
UTF-8 No tool result payload in the whole run may contain U+FFFD
backend_error A second MCP session with DOCKER_HOST set to a missing socket. Snapshot and tail both return ok: false, isError, exactly the closed {E_UNEXPECTED, backend_error, fixed message} and cutoff: null. The tail ends at eof well before maxMs
Restart docker restart on the exact owned ticker container during a 10 s tail. The tail must end in one typed outcome (time cutoff, complete or backend_error) within its bound, with at least 2 ordered boot-1 lines. The container must keep its ID with a new StartedAt (positive control). The later history snapshot holds BOOT 1 then BOOT 2 once each, with strictly ordered lines. A new tail returns 3 lines, all from boot 2
Reconnect A new MCP session's tail never returns a line older than the previous session's last line
P11 on real Compose hack run quiet -- false exits 1. sh -c 'printf p11-partial; exit 7' exits 7 with its output. The MCP hack.project.run result has ok: false, isError and exitCode: 7. A true child gives ok: true (positive control). No one-off container is left behind

Cleanup removes only containers, networks and volumes with the fixture's owner label. It is registered in ALL_SCENARIOS with an estimated 60 s shard hint.

Lines repeated across a restart are logged, not asserted. Compose may reattach to the restarted container from a coarse timestamp, so this is backend behavior, not an MCP contract violation. docs/reference/mcp-logs.md now says so.

tests/run-child-exit-status.test.ts (required test job, no Docker)

The source CLI runs with a private HOME and HACK_HOME (privateCliEnv) against a recording docker stub. The stub plays compose run by running the argv after the service on the host. The test checks four things:

  • hack run app -- false exits 1 with no stdout.
  • exit 7 exits 7 and keeps its output.
  • true exits 0.
  • --json after -- reaches the child unchanged (recorded argv).

Pipe negative control in bash: hack run … -- false | cat gives pipeline=0 hack=1 without pipefail and pipeline=1 hack=1 with it. The filter hides the failure only from the pipeline, never from hack's own status.

The in-process MCP hack.project.run envelope reports ok: false / exitCode: 1, ok: false / exitCode: 7 with stdout partial, and ok: true / exitCode: 0.

HACK-P11 pattern card

The card now cites the agent-surface-freeze E2E negative control (#296) under cleanup. Readiness says the hack run exit status and the MCP run result's exitCode are the child's own. The missing contract is narrowed to the two items still open: a freeze that refuses explicit guidance updates while an agent runs, and a typed terminal result for resumed background operations. The plugin bundle is regenerated with bun run generate:agent-plugins. Only plugins/hack/skills/hack-cli/references/patterns.md changed. No integration-ownership fingerprint or instruction content revision changed: the catalogue is not a fingerprinted surface, and those tests pass.

Validation (local, macOS, Bun 1.4.3, no Docker)

  • bun test tests/run-child-exit-status.test.ts: 3 pass, 3 consecutive runs. An earlier flaky 143 was Bun's 5 s default test timeout killing the CLI. Each test now sets a 90 s timeout.
  • bun test tests/pattern-catalogue.test.ts tests/pattern-examples.test.ts tests/integration-ownership.test.ts tests/agent-instruction-source.test.ts tests/agent-plugin-generation.test.ts tests/cli-spawn-env-isolation.test.ts tests/e2e-shard.test.ts: 78 pass, 0 fail.
  • bun run --cwd packages/cli typecheck: exit 0. bun run --cwd packages/cli check: 0 errors, 34 warnings, the same as the base. bunx ultracite check on the changed files: clean.
  • bun tests/e2e/run.ts --list lists mcp-logs-compose. The shard partition test passes. bun run privacy:check: ok.
  • MCP plumbing check, scratch only and not committed: the scenario's session, payload parsing, byte-cut and backend_error helpers ran against a scripted HACK_MCP_COMMAND stub. The real hack mcp serve returned the byte cutoff (506 whole lines, about 50 MB result over stdio), an events cutoff and a closed backend_error. This run found and fixed a non-u emoji regex in the scenario.

Hosted docker-e2e (real Compose)

mcp-logs-compose ran on docker-e2e shard 4/4 (Blacksmith Ubuntu 24.04, compiled dist/hack).

  • Final head 706e9567: PASS in 25.0 s (job). The required test job also passed on this head (job). The scenario is unchanged since 889cd62a.
  • Head 889cd62a: PASS in 25.3 s (job). Observed results:
    • Quiet snapshot complete. Quiet tail: time cutoff at 2009 ms with 1 line.
    • Ticker tail: time cutoff at 3006 ms with 15 lines. Event tail: exactly 5 log events (3 replayed + 2 live) in 521 ms.
    • Bulk snapshot: bytes cutoff with 501 whole lines and 8388606 stdout bytes kept, no U+FFFD.
    • Unreachable engine: snapshot and tail both returned the closed backend_error (tail eof at 259 ms).
    • Tail across restart: time cutoff, boots 1 and 2 seen, no repeated lines this run. History holds BOOT 1 then BOOT 2.
    • Post-restart tail: 3 boot-2 events. Reconnected session B: 2 events.
    • hack.project.run: exit 7 gave ok: false / exitCode: 7, and true gave ok: true.
  • The first two hosted runs failed on real assertion mismatches in the scenario itself, and both were fixed:
    • Head 22f1d477: the event tail replayed 50 lines including the older boot marker, so only 4 of the 5 counted events were ticks. It now replays 3.
    • Head 711e29dd: docker ps -q printed short IDs, which the exact-ID check rejected. It now uses --no-trunc.
    • In both runs every earlier step passed, and owned cleanup succeeded.

Hosted test job (macOS)

The test job failed on 889cd62a with 207 failures. Two of them were in the new P11 test:

  • hack run stdout carried CoreDNS/Caddy reachability warnings. The fixture now turns internal DNS/TLS off, so no probe runs.
  • bash was missing from PATH. The test now calls /bin/bash.

The rest of the 207 were a suite-wide environment cascade. bun, bash and docker were missing from PATH, and HOME-based paths were wrong. It started with the known nondeterministic native-authored-project-start failure ("Startup fixture settlement unconfirmed; roots retained"), so it is not attributed to this change. test passed on 706e9567.

Finding, not fixed here: hack run writes those Hack warnings to stdout, mixed with the child's output. That is a P11 diagnostics/results separation gap for HACK-1155.

Negative controls (mutations, each restored)

  • composeRuntimeBackend.run collapses any non-zero status to 1: all 3 P11 tests fail (exit 7 read as 1).
  • composeRuntimeBackend.run returns 0: all 3 fail.
  • The run backend drops --json from the child argv: the argv test fails.
  • MCP buildToolResult uses ok = exitCode < 2: the envelope test fails.
  • The scenario's U+FFFD guard, against the stub with the logs collector's codePointSafe: false: the bulk snapshot fails with "must return valid UTF-8". With the real collector it passes.

Remaining gaps

  • The scenario has one hosted pass. No hosted mutation run was made, to avoid extra docker-e2e runs on the shared fleet. Its U+FFFD guard was mutation-checked only against the stub backend. Docker was not run locally.
  • The byte-cut position varies from run to run, so one live pass does not prove that this run cut inside a code point. The deterministic mid-code-point case stays in tests/mcp-logs-contract.test.ts.
  • hack run writes Hack's own warnings to stdout (see above). This is not fixed here.
  • Not covered: the Loki backend, the native/authored logs path, and Compose's own duplicate behavior across a restart (logged only).
  • The P11 freeze for explicit hack setup during execution, and a typed terminal result for resumed background operations, remain open.

🤖 Generated with Claude Code

roodboi and others added 2 commits October 19, 2026 11:50
…ompose

HACK-1134 (unit F13), HACK-1155 (P11).

Add the docker-tier `mcp-logs-compose` scenario. It drives `hack mcp serve`
over stdio against a real Compose project and checks the logs contract:
complete, time cutoff (quiet and live), event cutoff, byte cutoff with a
UTF-8-safe partial result, closed backend_error with an unreachable engine,
a tail across a container restart, and no replay after reconnect. On the same
stack it checks that `hack run <svc> -- false` / `exit 7` and the MCP
`hack.project.run` envelope report the child's exit status.

Add tests/run-child-exit-status.test.ts: the source CLI against a recording
docker stub proves the exit status, argv after `--`, the pipe-filter negative
control and the MCP envelope without a Docker engine.

Update the HACK-P11 pattern card to cite the agent-surface-freeze negative
control (#296) and narrow the missing contract; regenerate the plugin bundle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first hosted run replayed 50 lines, which included the older boot
marker, so only 4 of the 5 counted log events were ticks. Replay 3 ticks
and let 2 live ones reach the bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

roodboi and others added 2 commits October 19, 2026 12:48
docker ps -q prints 12-character IDs, so the exact-ID check refused the
ticker container before the restart step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…state

The hosted macOS test job printed CoreDNS/Caddy reachability warnings on
hack run stdout, which the exact stdout assertion rejected. Turn internal
DNS/TLS off in the fixture so no probe runs, and call /bin/bash directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve plugins/hack/skills/hack-cli/references/patterns.md by
regenerating it (bun run generate:agent-plugins) from the merged
src/agents/pattern-catalogue.ts, which carries both this branch's
HACK-P11 wording and the pattern changes from #336.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

@roodboi
roodboi marked this pull request as ready for review October 20, 2026 17:58
@roodboi
roodboi merged commit d92774d into next Oct 20, 2026
30 of 33 checks passed
@roodboi
roodboi deleted the wf/f13-logs-e2e-b16 branch October 20, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant