Commit Graph

17 Commits

Author SHA1 Message Date
Martin Vogel a654acbc88 fix(test-infra): grant the build-dir ACL by SID, not by name (#1532 follow-up)
The local Windows leg caught this within hours of merging #1532: on a machine
that is not domain-joined, USERDOMAIN is the literal string WORKGROUP, and
`WORKGROUP\test` is not a resolvable principal —

    WARN: build-dir DACL stamp (pre-wave) failed (user=WORKGROUP\test ...):
    WORKGROUP\test: No mapping between account names and security IDs was done.

so the grant fails outright and the tree keeps its inherited
`Authenticated Users:(M)`. The guard then correctly reports that cross-account
mutation is still possible, and the whole leg stops. Since most Windows
machines are not domain-joined, that is the common case, not the exotic one.

#1532 was fixing a real bug in the other direction (a bare name resolves
against the machine first, so a host named like its user grants to an empty
principal). Both failures are name RESOLUTION failures, so this stops resolving
names: the account is identified by SID, which is unambiguous everywhere. The
SYSTEM and Administrators grants in these same commands have always used that
form — the current user was the odd one out. Name lookup survives only as a
fallback where PowerShell is unavailable.

Applied to all three scripts carrying the stamp (run-tests-parallel, soak-test,
memlab), since they share the failure and would otherwise drift apart.

Co-Authored-By: Kiborgik <Kiborgik@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-12 11:57:35 +02:00
PETRO YAKOVYSHYN 3d613a4c9f fix(test): qualify build-dir owner with its domain
The Windows DACL stamps in run-tests-parallel.sh, soak-test.sh and
memlab.sh pass the current user to icacls by bare name. Git Bash resolves
whoami to coreutils, which prints an unqualified name, and icacls resolves
an unqualified name against the machine before the user. On a host whose
name equals the user's (COMPUTERNAME=BUILD with user build) the grant
lands on an empty principal:

    BUILD\:(OI)(CI)(F)

Combined with the /inheritance:r on the same invocation, the directory
ends up granting no usable account. run-tests-parallel.sh then cannot
write its own log directory and the wave dies with:

    build/c/test-logs/results.txt: Permission denied
    FAIL: test-runner --list-suites exited nonzero

Prefix the name with USERDOMAIN when it is set, which makes the account
unambiguous, and fall back to the bare name otherwise. SYSTEM and
Administrators in the same invocations already use SID form and are
unaffected.

Signed-off-by: PETRO YAKOVYSHYN <petroyakovyshyn@gmail.com>
2026-08-11 01:43:57 -07:00
Martin Vogel 96a25b293e fix(test-harness): run extraction in the quiet tail, not the 18-job wave
The wide-flat SCALING-RATIO guard grows the input 20x and asserts the
time grows ~20x (linear) rather than ~128x (quadratic), with the bound at
40x between them. Contention does not cancel out of that ratio: the
400k-node measurement loses far more to memory pressure and scheduling
than the 20k one, so oversubscription inflates the ratio itself.

Measured on the Windows arm64 VM, same tree and same binary:

  alone            63ms -> 1167ms   18.5x   passes
  in the 18-job wave  168ms -> 9045ms   53.8x   fails
                      163ms -> 9019ms   55.0x   fails

Reproducible 3 of 3 in the wave and 1 of 1 alone, so the verdict was a
function of the scheduler rather than of the code. The suite is ~22s;
running it alone is cheap next to a ~40min ladder.

The bound is deliberately NOT widened. The calibration note in
tests/test_extraction.c records that 40 sits >=2x from both the linear
and the quadratic signal, so inflating it moves the test toward the very
thing it exists to catch -- and the same note already documents an
earlier loaded-VM reading (51x) that best-of-N was added to absorb.
Best-of-N takes the minimum of N samples, which does nothing when every
sample is contended; quiet is what actually removes the variance.

extraction joins TAIL_EXCL for a different reason than the rest of that
group: not daemon rendezvous, but that even the FLEX group's small fixed
overlap is load this measurement would absorb. Both comments say so.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-04 04:56:32 +02:00
Martin Vogel c1425d2e27 ci: write the shard manifest before any suite runs; warn on absent upload
The manifest content (leg, slice, list hash) is fully determined at
slicing time, so write it up front: a red run's manifest is exactly as
load-bearing for the cross-shard union proof as a green one's.

With that, a missing manifest at upload time can only mean the job
died before the harness started (contract step / build failure) — that
failure is already the job's red, so the upload warns instead of
stacking a second error on top (the recurring 'No files were found
with the provided path: build/c/test-logs/shard-manifest.txt'
annotation). The shard-completeness job still gates the union.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-27 02:00:59 +02:00
Martin Vogel 62821375c4 fix: harden daemon release paths and verification
DCO / dco (push) Has been cancelled
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-24 16:08:42 +02:00
Martin Vogel f7231aa2d9 test: harden the parallel runner's stamp and completion accounting
The Windows build-dir DACL stamp resets inherited ACEs before granting,
verifies itself out loud, and re-stamps at the serial-tail boundary — the
arm image ships explicit ACEs that survived a grant-only stamp and turned
the whole suite red. Stamp verification failures now fail the run instead
of warning. A suite that exits 0 without printing its summary line is
counted as a failure rather than silently passing.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-23 17:35:40 +02:00
Martin Vogel dc5649d1e6 test: re-stamp the Windows build-dir DACL at the serial-tail boundary
On one dry run the two arm shards split: the shard whose serial tail
held the install-flow suites failed the source-directory policy minutes
after its pre-wave stamp, while its sibling — same runner image, same
stamp — passed with no install-flow suites to notice. Wave suites spawn
Cygwin-family tooling that can rewrite the build directory's DACL
behind the first stamp, so the stamp is now a function invoked both
pre-wave and at the tail boundary, where the deadline-sensitive suites
that depend on it actually run. Both invocations self-verify out loud.

Verified on the VM from a deliberately reset (inherited) build
directory through the real --par path: both stamps report clean, mini
shard green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-23 08:54:56 +02:00
Martin Vogel acce9a4582 test: make the Windows build-dir DACL stamp verify itself out loud
The dry run's windows-11-arm shard failed its install-flow tests with
the source-directory refusal the stamp exists to prevent — while the
identical commands, run from the identically dirty state on the ARM64
VM, stamp clean and pass. The difference is invisible because the stamp
was fully silenced; a silent load-bearing step cost a full CI round to
even learn whether it had run.

The stamp now reports each icacls failure with the user and directory,
re-inspects the DACL afterwards, and prints one line in either
direction: stamped clean, or the surviving cross-account grants.

Verified on the VM from a deliberately reset (inherited, Authenticated-
Users-writable) build directory through the real --par path: stamp
reports clean, suites green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-23 08:49:35 +02:00
Martin Vogel 04141465ee test: stamp the Windows build-directory DACL inside the parallel runner
The workflow pre-created build\c with a protected DACL so build
products would inherit it, but the diagnostic note from the failing CI
shard named the real actor: MSYS2's Cygwin layer writes POSIX-emulating
DACLs — including a CREATOR OWNER (S-1-3-0) mutation grant — onto
directories its tools touch, overwriting the pre-created shape during
the build. The activation transaction's source-directory policy
correctly refuses that grant, so the install-flow tests failed on CI
while the VM (whose harness stamps AFTER building) stayed green.

The stamp now runs inside run-tests-parallel.sh on Windows, after any
builder has had its say and immediately before the suites — one code
path for CI and the VM. Two idempotent steps, as before: protect the
directory, then /reset the children onto the clean inherited set.

Verified: full VM parallel run green (6669 passed, 0 failed) through
the new path.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-23 02:12:17 +02:00
Martin Vogel 568e613141 test-infra: overlap the local ladder, open the serial tail, keep container I/O off virtiofs
Local runs left most of the machine idle: the serial tail ran sixteen
suites one at a time on otherwise-idle cores, the three platform legs
were launched by hand (usually sequentially), and every container-leg
object write crossed the virtiofs bind mount.

- run-tests-parallel.sh tail scheduling in two phases: the FLEX suites
  (timing-shaped but free of the shared per-account daemon runtime
  namespace) run CBM_TAIL_JOBS-wide (default 2), then the EXCL group —
  daemon-family plus the suites that drive daemon one-shots or
  supervisor rendezvous — runs strictly sequentially on a machine
  exactly as quiet as the old fully-serial tail gave it. The wave was
  already fed longest-first by the shard dealing order, so the drain-out
  no longer ends on a heavy straggler.
- ladder.sh: one maintained entry point for the full local push gate
  with the legs overlapped — lint, the Linux container suite, and the
  Windows VM suite in the background, the macOS suite in the
  foreground, one verdict per leg, logs kept per leg. A missing
  prerequisite fails its leg loudly instead of silently skipping.
- win.sh test-par now runs through vm-run-tests.sh (--par mode): the
  full parallel harness under the CI-shaped protected temp root with
  complete output. It previously ran under the MSYS-shared /tmp and
  piped through `tail -25` — the same truncated-blindness class that
  hid 40 Windows failures from the `test` command.
- docker-compose: build artifacts and the incremental fixture cache
  move to named volumes on the container VM's native filesystem. Object
  writes over the virtiofs bind mount are the container legs' largest
  avoidable I/O cost, and the fixture cache now survives across
  container runs.
- test_mem(win): the first valid full-parallel VM run proved working-set
  trimming beats the re-touch mitigation (19 MB resident of a 256 MB
  double-touch at 18 parallel suites). The RSS probe now VirtualLocks a
  64 MB span — locked pages are exempt from trimming, making the
  measurement pressure-immune — with bounded touch-and-sample retries
  when the lock is unavailable. Red-to-green under the same 18-job load.
- cli: the portable install's staging error now appends the activation
  refusal note (predicate, SID, object) like the managed path already
  does — a bare "activation transaction I/O failed" on a CI-only
  failure is undiagnosable without it.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-23 01:33:58 +02:00
Martin Vogel d7ce789e8f ci: shard the C-suite legs, prove cross-shard coverage, warm compiler caches
PR iteration was bounded by single-job suite runs (~40-50 min critical
path) and by every first-of-ref build starting cold. Same suites, same
sanitizers, same gates — the schedule, the cache reuse, and a new
runtime coverage proof change.

- run-tests-parallel.sh accepts CBM_TEST_SHARD="i/N" and runs a
  deterministic slice of --list-suites; unset selects everything, so the
  path is inert outside CI. Known-heavy suites are dealt to shards in
  weight order first (naive modulo stacked the two slowest, store_arch
  and daemon_runtime, onto one job), the parallel wave and the serial
  tail are sliced separately so every shard keeps its own quiet tail,
  and the per-shard union guard proves each job ran exactly its slice.
- shard-completeness job: per-shard guards cannot see a mis-plumbed
  CBM_TEST_SHARD (two jobs running the same slice passes every local
  check while a slice runs nowhere). Every leg now uploads a manifest
  (leg, i/N, sha of the full suite list, its slice) and one aggregation
  job re-proves per leg that the shards agreed on the list, indices are
  exactly 1..N, and the union of slices IS the list. Runs on unsharded
  topologies too, where each leg's single manifest must cover the list.
- workflow topology (shard_suites input, off by default = byte-identical
  to the pre-shard matrix): ubuntu legs run 3 shards, Windows 2 (every
  extra Windows shard re-pays ~5 min of MSYS2 setup), macOS stays at 1
  (not the critical path; mac runner concurrency ceilings are the
  tightest). The Windows test job also pre-creates build\c with a
  protected DACL so build products inherit it — workspace drive roots
  grant Authenticated Users Modify by inheritance, which the activation
  transaction's source-directory policy correctly refuses.
- ccache: a final ref-less restore key lets a fresh PR start from the
  newest cache GitHub's scoping permits, and a nightly build-only
  cache-warm workflow keeps main-scoped caches at most a day stale —
  without it the fallback had no warm source, since nothing built in
  main's scope. The strictly-per-ref policy this replaces cost ~8-12
  minutes on every first-of-ref build for no safety gain:
  CCACHE_COMPILERCHECK=content makes a stale or foreign cache able to
  miss but never to return wrong output. Sharded legs restore-all/
  save-one (every shard builds the identical objects, so shard 1's
  cache carries the full set) — save volume stays flat and warm caches
  stop being evicted by per-shard duplicates.

- suite timeouts: daemon_runtime joins the slow tier. It measures ~610s
  solo on arm64 under ASan (10-round loop, no hang), so the 900s default
  under a loaded 4-job CI runner was a slowness kill masquerading as a
  hang detector.

Validated locally: 3-shard union over the real suite list is complete
with no duplicates, one top-heavy suite per shard, and the summed
3-shard totals reproduce the unsharded run exactly (6776 passed,
0 failed, 4 skipped).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-23 00:53:52 +02:00
Martin Vogel 0baddf7887 daemon: permanent lifecycle, daemon-backed CLI/hooks, real-Windows hardening, long-path launcher transactions
Daemon lifecycle and Windows correctness, verified on a real Windows 11
ARM64 VM through the maintained test-infrastructure/vm drivers, plus the
macOS and Linux arm64 suites and the container lint gate.

Daemon lifecycle:

- daemon start/stop/status subcommands. `daemon start` launches a
  PERMANENT daemon (spawn shape is byte-exact argv; survives idle
  periods and session ends) and reports an already-active daemon
  instead of failing. Permanence is honored at every stop latch:
  last-committed-client disconnect, host initial-client window,
  coordinator release, and application final-session close — a
  permanent daemon also keeps admitting new sessions after its last
  one closes.
- daemon stop refuses while sessions are active and lists the blocking
  peers (pid/role) that must finish first; an idle daemon drains
  through the activation-shutdown machinery with the ACK ordered after
  connection interrupts. A second stop is idempotent. The wire ops are
  no-cohort first-frame requests with peer fingerprint authentication,
  so stop/status never conflict with an exact-build admission gate.
- One-shot CLI commands now execute through the daemon (index workers
  keep their local supervised path). A cold CLI run that had to spawn a
  temporary daemon prints a hint that `daemon start` removes the
  per-command startup tax; a warm daemon is recycled silently.
- Hooks are connect-only fail-open: with no daemon present the hook
  emits a visible, rate-limited notice (Claude-dialect systemMessage
  plus stderr for other dialects) and always exits 0 — augmentation is
  never allowed to block the caller's tool use.
- Version skew: a newer-build client automatically drains an
  older-build permanent daemon (strict semantic-version triples only;
  dev builds never auto-drain) and the build-conflict message names
  `cbm daemon stop` as the manual escape hatch.

Windows IPC/runtime (real-VM verified):

- ipc(win): persistent pending overlapped ConnectNamedPipe. The accept
  path used to destroy its listening pipe instance on every 20 ms poll
  timeout; a client attaching in the teardown window was severed or left
  on an orphaned pipe object whose HELLO no server handle could ever
  read, absorbing the connect until the client's own timeout expired.
  The pending connect now survives poll timeouts and nothing is
  destroyed while a client could be attaching.
- ipc(win): drain-before-close for final responses. Closing a named-pipe
  server handle can discard a just-sent response before the peer reads
  it (POSIX stream sockets never lose buffered data on close). A bounded
  cbm_daemon_ipc_connection_drain (read-until-peer-EOF; no-op on POSIX,
  immediate on interrupted connections) now precedes close in
  runtime_worker_finish and runtime_reject_inline, so hello-conflict,
  capacity and disconnect acknowledgements reliably reach the peer.
- runtime: CLOSE_INTENT wire frame. A Windows named-pipe client has no
  transport half-close, so close_begin now announces departure with an
  explicit frame (ordered after APPLICATION_CANCEL, before the local
  interrupt); the server releases the client's admission on receipt
  instead of waiting for the handle to close. Admission-drop timing is
  now identical to POSIX shutdown() semantics on every platform.
- runtime(win): client close cancellation. close_begin serializes with
  request publication under the send lock, best-effort sends the active
  token's APPLICATION_CANCEL frame, then interrupts local I/O; the
  server cancels MCP/subprocess work promptly. Contract tests accept
  both correct outcomes (interrupted transport or decoded CANCELLED).
- runtime: activation acknowledgement ordering. The activation ACK is
  the requester's license to act on "snapshotted and draining", so every
  connection interrupt is now initiated before the ACK is sent; a
  session could previously get one more request serviced after the
  requester observed the ACK.
- service(win): deadline-bounded private-file prepare. The conflict-log
  prepare retry loop (100 x Sleep(2), which rounds up to the ~16 ms
  timer granularity) burned ~1.6 s against permanently obstructed paths,
  stalling hello rejections past the client's timeout. The retry budget
  is now a 250 ms deadline; transient share collisions still retry.
- subprocess(win): cmd.exe /C payload encoder quotes metacharacters
  correctly (root cause of the git-on-Windows failure cluster).
- watcher: SHA-256 buffer sizing (CBM_SZ_64 -> CBM_SZ_128) and a native
  Windows stop/unwatch cancellation test with exact-image verification.
- httpd: send_all writes in bounded 64 KiB slices. A single giant
  nonblocking send() on Windows is absorbed wholesale into AFD kernel
  buffering regardless of SO_SNDBUF, so send deadlines and interrupts
  could never engage against a slow peer (and the full payload was
  pinned in nonpaged pool). Slicing restores a deterministic
  backpressure point; a test hook pins SO_SNDBUF for the deadline and
  interrupt tests.
- ui/http: shutdown lifecycle — interrupt checks, response-wide send
  deadline, explicit connection states, refusal to free a server while
  a listener-owned connection is active.

Windows long-path support:

- Central path-aware wide conversion (canonicalize via GetFullPathNameW
  and prepend the extended-length prefix for absolute paths >=240) at
  the compat chokepoints (cbm_fopen/compat_fs/mkstemp/mkdtemp), sqlite
  store opens, and the daemon build-fingerprint/log paths. Deep managed
  installs (a 64-hex generation directory routinely exceeds MAX_PATH)
  now index, stage and activate correctly.
- activation transaction: its own file APIs and the component-walking
  ancestry validators now operate in the extended-length namespace;
  the launcher path is canonicalized (and prefixed when deep) once at
  entry so every downstream exact-string comparison stays
  form-consistent.
- Executable self-resolution uses the wide APIs (GetModuleFileNameW,
  GetFileAttributesW) so non-ASCII install paths survive argv[0]
  resolution.

Windows launcher install/uninstall transaction:

- FileRenameInfoEx names are NUL-terminated in an over-allocated
  buffer. FileNameLength governs per the contract, but filter drivers
  read FileName as NUL-terminated and appended adjacent heap bytes to
  created names — a flaky, garbage-suffixed rename target. Both the CLI
  and the launcher rename helpers are fixed.
- Uninstall retires state via rename-aside (.cbm ->
  .cbm-retired-v1-<tag>-<pid>) with the retired tag shortened to 16 hex
  chars so the bare rename target stays under the FileRenameInfoEx
  NT-conversion ceiling at guard depths; 64 bits still uniquely
  identify the generation.
- When the running launcher's mapped generation backings pin .cbm
  against rename, the backings are relocated to activation-<pid>-N
  .retired tombstones beside the install (a mapped image may be renamed,
  never deleted; the launcher's liveness-guarded sweep reclaims stale
  tombstones). Every relocation is recorded, and a FAILED uninstall
  reverses the moves after restoring .cbm — via MoveFileExW with
  extended-length paths on both arguments, since the deep generation
  target is beyond the handle-based rename's bare-path reach — so a
  restored install keeps its generation backings and stays runnable.
- After a committed uninstall the retired tree's backings are relocated
  out so the tree is shallow enough for the detached cleanup's rd, and
  the cleanup's working directory strips the extended-length prefix
  (CreateProcessW lpCurrentDirectory silently ignores prefixed paths).
- Files created under Administrators-default-owner directories
  (CopyFileW destinations, CREATE_NEW tombstones, probe directories)
  are explicitly owner-stamped so the exact-owner validators hold on
  runner images; guard fixtures stamp hand-built trees the same way.

Diagnostics, tests and infra:

- diagnostics: discovery is now an always-delivered JSON control record
  (new cbm_log_control) that survives CBM_LOG_LEVEL suppression and
  paths containing spaces; placement honors $TMPDIR with /tmp fallback
  via a diagnostics-local helper; the soak parser reads the JSON record;
  documented in docs/CONFIGURATION.md. Red-first coverage for suppressed
  log levels, TMPDIR-with-spaces, and native Windows output-contract
  assertions.
- tests(win): daemon_ipc/daemon_frontend fixtures now build endpoint
  parents with production-shaped ancestry (LocalAppData on Windows, via
  th_secure_runtime_parent_new) — the runtime ancestry validation
  correctly refuses temp roots whose ancestors grant mutation rights to
  Authenticated Users (C:/msys64/tmp, GitHub-runner work dirs) — and
  drive the documented startup-owner publication flow before reading
  generation-bound endpoint addresses. This turns the 26 Windows
  failures previously visible in CI's full-test job green without
  weakening any validation.
- tests(win): the launcher guard covers the full permanent-launcher
  contract including failed-uninstall restore and immediate reinstall
  after uninstall; new daemon lifecycle and reworked hook-augment
  guards run the start/recycle/stop flow end to end.
- tests: CBM_SKIP_PERF is now actually consumed by the test runner
  (it was set by CI but never read, so perf suites ran everywhere);
  four throughput/bench suites are classified as perf, the heavy
  store_arch suite moved to the slow-timeout tier, and two
  wall-clock-sensitive assertions were rewritten as invariant checks
  with coarse hang-detector backstops.
- build/test infra: build-dir safety contract, UI dev-proxy security
  contract, soak daemon-recovery contract, path-safety helper, the
  Windows VM worktree-sync contract wired into scripts/test.sh, and
  vm/win.sh guards building its clean embedded-UI product in an
  isolated BUILD_DIR so it cannot clobber the incremental test build.
  provision-windows.sh now installs Node.js for the guards UI build.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-21 15:32:15 +02:00
Martin Vogel 53d867dfc1 test-infra: per-suite timeout + arm64-safe mingw image (reliability)
Two reliability defects that bit this session: a wedged suite could
hang the runner (and the single local build slot) forever, and the
mingw image failed to build on an arm64 Docker host because its i386
Wine packages are unsatisfiable.

- run-tests-parallel.sh now caps each suite with `timeout --kill-after`
  (900s default, 3600s for the legitimately-slow incremental suite),
  where `timeout` exists; a killed suite fails loudly instead of
  blocking. Hosts without `timeout` run uncapped as before.
- Dockerfile.mingw makes Wine best-effort: the cross-COMPILE toolchain
  builds on any arch, and only the .exe-execution step needs Wine, so a
  Wine-unavailable host still gets a working compile check.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-19 00:42:19 +02:00
Martin Vogel 712d689efb fix(test): serialize the daemon-family suites in the parallel runner
The six suites that spawn coordinated worker subprocesses or bind local
endpoints (index_supervisor, daemon_application, daemon_runtime,
daemon_frontend, daemon_bootstrap, daemon_ipc) ran in the parallel wave,
where the saturated 3-4-core CI runners starve their fixed readiness
deadlines: index_supervisor waits at most 3 s for a worker marker while
the worker is a full re-exec of the ASan runner plus the admission
handshake. Both macOS legs failed the same four readiness assertions
deterministically and ubuntu-latest added four daemon_frontend and one
daemon_runtime timing failures, while an idle machine passes the same
suites 6/6 in the wave.

Moving them to the quiet serial tail follows the runner's existing rule
for this class (cli/subprocess/watcher/...) and also keeps the shared
per-account coordination namespace free of cross-suite admission
traffic. Same suites, same tests, same union-guarded totals - only the
schedule changes. Locally re-verified: 6742 passed, 0 failed, 2 skipped
(120 suites, 17 serial-tail).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 16:45:06 +02:00
Martin Vogel 95659b3955 test-par: serialize the mcp suite too (both alarm-deadline tests live there)
Flake audit: the whole corpus has exactly two wall-clock test deadlines —
the EOF-shutdown alarm(5) and the issue-832 supervisor-spawn alarm(60),
both in the mcp suite (the latter already flaked once under heavy local
load). Serializing mcp costs ~6s of tail and closes the starvable-deadline
class completely; every remaining parallel-wave suite is deadline-free.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 13:39:42 +02:00
Martin Vogel c8e07a2f9f fix(test-par): CRLF-proof the suite list, serialize deadline-sensitive suites
First CI validation round of the parallel runner, three findings:

- Windows failed ALL 104 suites with "0 passed, 1 failed" each: the CRT
  emits --list-suites lines as CRLF, so every dispatched name carried a
  trailing CR ("arena\r" is an unknown suite). The union guard caught it
  loudly, exactly as designed. The driver now strips CR when writing the
  suite list.

- The ubuntu-gcc legs had 3 real failures in the cli suite: it spawns
  subprocesses with fixed deadlines, and a fully saturated 4-core runner
  starves those deadlines into flakes. Deadline-sensitive suites (cli,
  subprocess, watcher, incremental, httpd, ui, index_resilience, and the
  stack_overflow family) now run SEQUENTIALLY after the parallel wave on a
  quiet machine — same suites, same tests, same gates, only the schedule
  differs; the union guard checks the combined result set.

- Failing suites now print every FAIL site with context (the tail-30 of a
  long suite log hid which tests actually failed on CI).

Local: totals unchanged (6,361 passed / 0 failed / 1 skipped), union guard
clean.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 12:24:07 +02:00
Martin Vogel bd939fc49a perf(test): run suites as parallel processes — same gates, ~2.5x faster
The runner executed all 104 suites sequentially (~10 min locally, the bulk
of every CI test leg). The suites are process-isolated already (per-process
mkdtemp HOME sentinel; two full parallel runs produced zero cross-suite
failures), so the serialization was pure convention.

- test-runner --list-suites: prints every registered suite, one per line,
  emitted by the SAME macro table that executes suites — the list cannot
  drift from the run set by construction.
- scripts/run-tests-parallel.sh: runs each suite as its own process
  (jobs = CPU count; CBM_TEST_PAR_JOBS overrides) under a ZERO-LOSS
  CONTRACT: a union guard fails the gate if the set of suites that
  produced a result differs from --list-suites (nothing can be silently
  dropped, a newly added suite is picked up automatically); per-suite
  pass/fail/skip are summed into the sequential runner's exact summary
  format; any suite crash, failure, or omission exits nonzero. Per-suite
  wall times are printed for balance tracking.
- make test-par: the parallel target. make test (sequential) is unchanged
  and remains the escape hatch (CBM_TEST_SEQUENTIAL=1 in test.sh).
- scripts/test.sh: builds, then routes through test-par — every CI test
  leg gets the speedup with zero workflow-topology change (same jobs,
  same gates, same billing; the legs just finish sooner).
- The stack_overflow suite is split into a/b/c (7+6+7 of its 20 tests,
  pure re-registration): as one suite it was the wall-clock critical path
  of any parallel run — every other suite finished underneath its ~4
  minutes.

Measured locally (Apple Silicon, ASan runner): sequential ~10 min vs
parallel 232 s, totals identical (6,361 passed / 0 failed / 1 skipped),
union guard clean. The TSan leg keeps its dedicated subset runner
(sequential) — out of scope here.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 11:05:17 +02:00