Files
ironclaw/tools
firat.sertgoz 30ae2d50f6 feat(stress): scripted tool-call workload with durable write read-back (#7360) (#7382)
* feat(stress): scripted tool-call workload with durable write read-back (#7360)

Phase 1 of issue #7360: teach the stress harness to drive real builtin
and memory tool calls through the production capability path and verify
their durable side effects.

The api-user-capacity mock LLM sidecar learns a deterministic scripted
state machine: the driver embeds an `ironclaw-stress-tool` marker in the
user message, the sidecar emits the scripted tool call for a tool
advertised in the request, the server executes it through the real
capability host, and the driver verifies the read-back verdict in the
final assistant message. Verdicts: confirmed / contended (same-user CAS
race, counted) / leak (cross-user isolation, hard failure) / missing
(write lost, hard failure) / undisclosed (tool never advertised).

Scripts: write_file_roundtrip (write_file + read_file of a unique
workspace path), memory_roundtrip / memory_grow / memory_mixed
(ironclaw.memory.write/replace-append + read of the shared
stress/shared.md target — every run doubles as a same-relative-path
isolation check). --api-scripted-doc-sizes cycles 4 KiB..1 MiB
documents with per-size buckets and submit-to-tool-visible /
submit-to-finalize stage latencies; --api-hot-writers spawns concurrent
same-user writers for hot-document CAS contention. Gated tools are
exercised through the per-user Tools auto-approve setting enabled during
setup via the production settings API.

Wired as a nightly leg in the hosted-single-tenant Postgres job (the
existing server stays up; the leg rebinds the mock sidecar on the same
port). Unit coverage: marker parsing, per-op step sequencing, tool-name
resolution (encoded/dotted/bare), verdict computation incl. leak
precedence, disclosure fallback, timeline helpers, per-size summary
buckets, and flag validation.

* fix(stress): hot writers on distinct user threads, size floor, CI server lifecycle

Review fixes for the #7360 Phase 1 scripted workload:

- Hot writers now run on distinct threads of the first user instead of
  sharing one thread, so concurrent operations exercise real per-user
  memory-document CAS contention rather than per-thread turn
  serialization. setup_users creates and records one extra thread per
  hot writer for user 0; run_hot_writer picks its own thread.
- Scripted document sizes are floored at 4 KiB (the token-dominated
  region below is meaningless and the issue's workloads start there);
  enforced in marker parsing and --api-scripted-doc-sizes validation.
- The CI scripted leg runs inside the server's run block so the trap
  does not kill the server before it starts; artifacts upload together.
- Wire-shape tests: mock_tool_call_response deserializes as the rig
  OpenAI CompletionResponse (stringified arguments, finish_reason
  tool_calls) and streaming tool-call chunks carry indexed delta
  tool_calls.

* fix(stress): hot-writer client action ids collide with the primary writer

A hot writer and the first user's regular writer shared the same user
label and operation index, so their client_action_id values were
identical and the server rejected the second submit with a 409
duplicate conflict. Include the scripted op prefix (h{k}-) in the
operation ref so concurrent writers always submit distinct action ids.

Found by a full local E2E run of the scripted leg against a real
hosted-single-tenant server: after the fix, memory_roundtrip with one
hot writer runs 9/9 clean (6 confirmed + 3 contended, 0 leaks) and
memory_grow runs 8/8 confirmed.

* fix(stress): address coderabbit review — verdict integrity, op-scoped tool counts, typed script key (#7382)

- compute_verdict: verdict comes from read steps only (write echoes can no
  longer mask missing/contended)
- timeline tool evidence: count tool results by sequence above the op's
  baseline instead of subtracting page-limited absolute counts
- timeline verdict match: delimit prefix by trailing space so op 1 cannot
  terminate on op 10's message; parse_result_verdict aligns on substring
- background users namespace markers as b{index} so cross-cohort leaks
  cannot read back as their own token
- hot writers drain in a dedicated JoinSet (no run_virtual_user refills)
- fraction chunks derive from cumulative boundaries so split writes persist
  exactly the configured size (regression test at 4097)
- ScriptKey derives clap::ValueEnum: CLI, marker wire format, and parsing
  share one string mapping; --api-hot-writers rejects write_file_roundtrip
- parse_marker bounds identity grammar; poisoned mutex recovery; sorted
  stage latencies; single conversation parse per completion request;
  CLI-level scripted validation test and doc-size bound coverage

* test(stress): cover --api-wait-for-assistant gate in CLI-level scripted test (#7382)
2026-08-08 16:57:36 +00:00
..