@agentsfleet/orly 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.githooks/pre-commit +42 -0
- package/.githooks/pre-push +67 -0
- package/AGENTS.md +348 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/SOUL.md +90 -0
- package/audits/agents-md.md +497 -0
- package/audits/agents-md.sh +353 -0
- package/audits/cross-tier-rates.sh +105 -0
- package/audits/data.sh +185 -0
- package/audits/deinit-pairs.sh +224 -0
- package/audits/design-tokens.sh +143 -0
- package/audits/doc-read.sh +183 -0
- package/audits/error-codes.sh +221 -0
- package/audits/fixtures/clean.diff +15 -0
- package/audits/fixtures/dirty.diff +16 -0
- package/audits/lifecycle-anchors.sh +22 -0
- package/audits/logging.sh +217 -0
- package/audits/msid-ui.sh +144 -0
- package/audits/parity-dispatch.sh +51 -0
- package/audits/rule-ledger-lib.sh +264 -0
- package/audits/rule-ledger.sh +225 -0
- package/audits/rule-paths.sh +108 -0
- package/audits/spec-template.sh +258 -0
- package/audits/sql-mod.sh +102 -0
- package/audits/ufs.sh +303 -0
- package/bin/orly +26 -0
- package/core/operating-model.md +256 -0
- package/dispatch/edit_rules.md +69 -0
- package/dispatch/lib.sh +213 -0
- package/dispatch/lifecycle.md +187 -0
- package/dispatch/name_architecture.md +93 -0
- package/dispatch/verify.md +111 -0
- package/dispatch/write_any.md +740 -0
- package/dispatch/write_any.sh +49 -0
- package/dispatch/write_auth.md +36 -0
- package/dispatch/write_changelog.md +64 -0
- package/dispatch/write_documentation.md +45 -0
- package/dispatch/write_go.md +24 -0
- package/dispatch/write_http.md +45 -0
- package/dispatch/write_mdx.md +7 -0
- package/dispatch/write_python.md +8 -0
- package/dispatch/write_rust.md +38 -0
- package/dispatch/write_shell.md +8 -0
- package/dispatch/write_spec.md +105 -0
- package/dispatch/write_sql.md +112 -0
- package/dispatch/write_sql.sh +36 -0
- package/dispatch/write_ts_adhere_bun.md +493 -0
- package/dispatch/write_ts_adhere_bun.sh +37 -0
- package/dispatch/write_zig.md +852 -0
- package/dispatch/write_zig.sh +40 -0
- package/docs/CHANGELOG_VOICE.md +18 -0
- package/docs/DISPATCH_ARCHITECTURE.md +664 -0
- package/docs/DISPATCH_REVIEW_DISPOSITION.md +65 -0
- package/docs/DOCUMENTATION_RULES.md +209 -0
- package/docs/EXECUTE_DOC_READS.md +29 -0
- package/docs/HARNESS_VERIFY_OUTPUT.md +40 -0
- package/docs/LIFECYCLE_PATTERNS.md +316 -0
- package/docs/LOGGING_STANDARD.md +397 -0
- package/docs/ORLY_ARCHITECTURE.md +129 -0
- package/docs/REST_API_DESIGN_GUIDELINES.md +783 -0
- package/docs/RULE_ENFORCEMENT.md +41 -0
- package/docs/SCHEMA_CONVENTIONS.md +93 -0
- package/docs/TEMPLATE.md +411 -0
- package/docs/VERIFY_TIERS.md +57 -0
- package/docs/greptile-learnings/README.md +51 -0
- package/docs/greptile-learnings/RULES.md +1045 -0
- package/fixtures/registry-invalid.json +1 -0
- package/fixtures/registry-valid.json +18 -0
- package/fixtures/unclosed-pack-block.md +8 -0
- package/package.json +63 -0
- package/packs/language/go/rules.md +24 -0
- package/packs/language/mdx/rules.md +7 -0
- package/packs/language/python/rules.md +8 -0
- package/packs/language/rust/rules.md +38 -0
- package/packs/language/shell/rules.md +8 -0
- package/registry.json +405 -0
- package/schemas/evidence.schema.json +43 -0
- package/schemas/profile.schema.json +12 -0
- package/schemas/registry.schema.json +12 -0
- package/skills/orly-babysit-prs/SKILL.md +326 -0
- package/skills/orly-spec-new/SKILL.md +157 -0
- package/skills/orly-write-integration-test/SKILL.md +470 -0
- package/skills/orly-write-unit-test/SKILL.md +520 -0
- package/src/cli.test.ts +70 -0
- package/src/cli.ts +287 -0
- package/src/config.test.ts +226 -0
- package/src/config.ts +288 -0
- package/src/criteria.test.ts +137 -0
- package/src/criteria.ts +166 -0
- package/src/criteria_spec.ts +127 -0
- package/src/criteria_support.ts +53 -0
- package/src/gates.test.ts +183 -0
- package/src/gates.ts +169 -0
- package/src/gates_spec.test.ts +98 -0
- package/src/gates_test_support.ts +194 -0
- package/src/git_env.ts +20 -0
- package/src/install.test.ts +514 -0
- package/src/install.ts +447 -0
- package/src/install_nested.test.ts +32 -0
- package/src/model.test.ts +31 -0
- package/src/model.ts +245 -0
- package/src/references.test.ts +47 -0
- package/src/references.ts +147 -0
- package/src/render.test.ts +60 -0
- package/src/render.ts +94 -0
- package/src/surfaces.test.ts +43 -0
- package/src/surfaces.ts +89 -0
- package/src/validation.test.ts +11 -0
- package/src/validation.ts +87 -0
- package/src/verify.test.ts +13 -0
- package/src/verify.ts +59 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: orly-write-integration-test
|
|
3
|
+
description: >
|
|
4
|
+
Service-layer integration tests against real dependencies (Postgres, Redis,
|
|
5
|
+
full HTTP router + middleware) with deterministic failure injection,
|
|
6
|
+
per-test isolation, drain + leak audits, and 100-or-more-connection parallelism
|
|
7
|
+
proofs. Sister of orly-write-unit-test. Use for handlers, repos, services, or
|
|
8
|
+
any real-I/O module-boundary crossing. Not for browser E2E (gstack /qa) or
|
|
9
|
+
pure logic (orly-write-unit-test).
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Write Integration Test
|
|
13
|
+
|
|
14
|
+
Service-layer tests that prove real wiring works under real failure modes. Sits between unit tests (`make test`) and browser E2E (gstack `/qa`, `/e2e-qa-playwright`).
|
|
15
|
+
|
|
16
|
+
> **What this guarantees / what it doesn't.** Integration tests don't prove "zero bugs
|
|
17
|
+
> in production" — they prove the *seams* hold under *real* deps and *named* failure
|
|
18
|
+
> modes. Three production-safety proofs are gated on **deterministic** evidence
|
|
19
|
+
> (counters over clocks · exhaustive injection · barrier-synced contention · pinned
|
|
20
|
+
> baselines): **zero leaks over the real request lifecycle (T6), correctness *and*
|
|
21
|
+
> parallelism at ≥100 connections (T5), and a complexity/latency budget under load**.
|
|
22
|
+
> When a super-linear term or a serialization point (one global lock) shows up, the
|
|
23
|
+
> rule is **propose the structural refactor, surface it for decision — never silently
|
|
24
|
+
> band-aid** (`AGENTS.md` Decomposition).
|
|
25
|
+
|
|
26
|
+
## Test quality bar
|
|
27
|
+
|
|
28
|
+
Every test answers three questions. Delete it if it answers none.
|
|
29
|
+
|
|
30
|
+
1. **What integration bug would this catch that no unit test could?**
|
|
31
|
+
2. **Which real dependency is exercised — and would mocking it hide the bug?**
|
|
32
|
+
3. **What failure mode does this prove the system survives?**
|
|
33
|
+
|
|
34
|
+
If a test answers (1) but mocks the dep, demote it to a unit test.
|
|
35
|
+
|
|
36
|
+
## What belongs here
|
|
37
|
+
|
|
38
|
+
A test belongs in the integration suite (`make test-integration` / `pytest -m integration` / `cargo test --test 'integration_*'`) if **any** of:
|
|
39
|
+
|
|
40
|
+
- **Real Postgres** — schema applied, real connection pool, real extensions if used
|
|
41
|
+
- **Real Redis** — actual pub/sub, streams, KV, lease semantics
|
|
42
|
+
- **Full HTTP lifecycle** — request enters via router; auth/rate-limit/CORS/logging middleware runs; handler executes; response serialises
|
|
43
|
+
- **Cross-module wiring** — handler → service → repo → DB. Mocking any internal layer disqualifies.
|
|
44
|
+
- **Real serialiser** — request/response through the actual codec, not constructed structs
|
|
45
|
+
|
|
46
|
+
A test does **NOT** belong here if it:
|
|
47
|
+
|
|
48
|
+
- Tests pure logic / codec / parser → `orly-write-unit-test`
|
|
49
|
+
- Drives a browser / asserts UI → gstack `/qa` or `/e2e-qa-playwright`
|
|
50
|
+
- Mocks the DB or Redis → demote to unit
|
|
51
|
+
- Hits a deployed environment (`api-dev.agentsfleet.net`) → that's a probe/canary
|
|
52
|
+
|
|
53
|
+
## Truth hierarchy
|
|
54
|
+
|
|
55
|
+
When sources disagree, trust in this order:
|
|
56
|
+
|
|
57
|
+
1. **Production runtime behaviour** — logs, incidents, traces
|
|
58
|
+
2. **Spec / OpenAPI / API rules**
|
|
59
|
+
3. **Code**
|
|
60
|
+
|
|
61
|
+
Spec asserts "503 on Redis down", code returns 200 → test the spec, flag the code as a defect.
|
|
62
|
+
|
|
63
|
+
## First steps
|
|
64
|
+
|
|
65
|
+
1. **Read the spec** — Failure Modes, Error Contracts, Concurrency Contracts, Resource Limits, Streaming Contracts tables are authoritative.
|
|
66
|
+
2. **Read `docs/greptile-learnings/RULES.md`** — every rule maps to a regression test.
|
|
67
|
+
3. **Bring up real deps** — `make up` (or stack equivalent). Verify health before writing tests.
|
|
68
|
+
4. **Run the existing integration suite** — establish green baseline; rule out flakes before adding tests.
|
|
69
|
+
5. **Map the ordered request path** — list every dependency interaction in execution order, including repeated acquisitions of the same resource, plus every `catch`/`orelse`/`except`/`Err` in the chain. That list seeds T4 and its partial-completion matrix.
|
|
70
|
+
|
|
71
|
+
## Three execution modes
|
|
72
|
+
|
|
73
|
+
| Mode | When | Required tiers |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **Smoke** | New Create, Read, Update, Delete (CRUD) endpoint backed by existing patterns; trivial schema add | T1 + T2 + T3 |
|
|
76
|
+
| **Standard** | New service method, new Redis stream/key, schema change with logic | + T4 + T5 + T6 |
|
|
77
|
+
| **Hardening** | Auth / payment / lease / migration / streaming / anything in the data-loss radius | + T7 (if applicable) + T8 + chaos pass |
|
|
78
|
+
|
|
79
|
+
Auto-detect from diff: `src/auth/**`, `src/agent/leases/**`, `src/runner/**` + `src/agentsfleetd/fleet/**` (the lease/reclaim/fence + client-daemon surface → T9), schema migrations, streaming handlers → Hardening. CRUD-only against existing schema → Smoke. Default → Standard.
|
|
80
|
+
|
|
81
|
+
Any operation that touches multiple systems or acquires the same resource more than once is **Standard at minimum** and must run T4, even when the endpoint is otherwise CRUD-only.
|
|
82
|
+
|
|
83
|
+
**A client daemon (no router, no datastore — e.g. `agentsfleet-runner`) is always Hardening + T9**, regardless of diff size: its public surface *is* the process lifecycle (kill / restart / reclaim / fence), and that is exactly the surface a happy-path loop test misses.
|
|
84
|
+
|
|
85
|
+
## Risk-weighted priority
|
|
86
|
+
|
|
87
|
+
Within a mode, write tests in this order. Stop when the surface is covered.
|
|
88
|
+
|
|
89
|
+
1. **State-changing happy path with real deps** (T1 + T2 + T3 together)
|
|
90
|
+
2. **Each downstream-failure branch** (T4) — every `catch`/`orelse`/`except`/`Err` in handler/service/repo; test release-then-reacquire windows first
|
|
91
|
+
3. **Concurrency under contention** (T5) — same row, same lease, same idempotency key
|
|
92
|
+
4. **Resource lifecycle** (T6) — drain after every query, leak across N requests, pool exhaustion
|
|
93
|
+
5. **Edge-of-spec rule checks** (T8) — OpenAPI breaking-change
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Nine integration tiers
|
|
98
|
+
|
|
99
|
+
### T1 — Real-dependency wiring
|
|
100
|
+
|
|
101
|
+
Prove the seams hold:
|
|
102
|
+
- Real PG pool initialised; schema/migrations applied; idempotent on re-run
|
|
103
|
+
- Real Redis connected; pub/sub round-trips; streams append/consume
|
|
104
|
+
- Real router resolves the route; method matched; path params parsed
|
|
105
|
+
- Full middleware chain executes in order — auth → rate-limit → logging → CORS
|
|
106
|
+
- Zero internal modules mocked. External edges (Stripe, OpenAI, SES) **may** be mocked at the HTTP boundary, never at the SDK call site
|
|
107
|
+
|
|
108
|
+
### T2 — Request lifecycle behaviour
|
|
109
|
+
|
|
110
|
+
Full HTTP request → response, asserting:
|
|
111
|
+
- Specific status code (`201`, not "2xx")
|
|
112
|
+
- Response headers — `content-type`, `x-request-id`, `cache-control`, custom
|
|
113
|
+
- Body shape validated against OpenAPI/protobuf schema (not just `assert response["id"]`)
|
|
114
|
+
- Pagination headers / cursors when applicable
|
|
115
|
+
- HEAD/OPTIONS behaviour when API rules require
|
|
116
|
+
|
|
117
|
+
### T3 — State assertions (success and injected failure)
|
|
118
|
+
|
|
119
|
+
After the request, assert side-effects:
|
|
120
|
+
- **DB rows** — queried back, content matches; `updated_at` advanced; FK rows created; soft-delete flag set
|
|
121
|
+
- **Redis events** — published to right stream, payload schema valid, consumer-group lag = 0 after process
|
|
122
|
+
- **Lease state** — acquired and released; no orphan keys
|
|
123
|
+
- **Outbox** — outgoing event row written if applicable
|
|
124
|
+
- **Logs** — structured log line emitted with right level + fields (use a capturing logger; assert structured fields, not string match)
|
|
125
|
+
- **Metrics** — counter incremented (where exposed in test mode)
|
|
126
|
+
|
|
127
|
+
A test asserting only the response body is half-done. After an injected failure,
|
|
128
|
+
assert the residual state: which durable rows survive, which external state changed,
|
|
129
|
+
what the user sees, and whether a retry heals the operation.
|
|
130
|
+
|
|
131
|
+
### T4 — Failure injection per dependency
|
|
132
|
+
|
|
133
|
+
Vague claim "503 on Redis failure" → real test with deterministic injection. Every `catch`/`orelse`/`except`/`Err` in the request path needs ≥1 injection-driven test.
|
|
134
|
+
|
|
135
|
+
**Partial completion.** This applies whenever one operation touches more than one
|
|
136
|
+
system or acquires the same resource more than once. Enumerate interactions in
|
|
137
|
+
execution order and inject failure at **each selected pair of workflow and boundary-call ordinals**, not only the first.
|
|
138
|
+
Draining a resource pool proves the first acquisition and nothing after it. If code
|
|
139
|
+
acquires twice, the test needs a seam that can fail acquisition 1, then acquisition
|
|
140
|
+
2, with the same exhaustive shape as `std.testing.checkAllAllocationFailures`.
|
|
141
|
+
|
|
142
|
+
Record both ordinals. The workflow ordinal locates the interaction in the whole
|
|
143
|
+
operation; the boundary-call ordinal selects a repeated call at one dependency.
|
|
144
|
+
Scope the failpoint to the test and operation or request identifier, reset it per
|
|
145
|
+
test, make its counter concurrency-safe, and assert it fired exactly once.
|
|
146
|
+
|
|
147
|
+
Write the matrix before the tests:
|
|
148
|
+
|
|
149
|
+
| Workflow N | Boundary call N | Dependency / resource | Interaction | Prior completed actions | Injection seam | Operation / request scope | Response | Failure timing / remote outcome | Durable residual state | External residual state | User-visible state | Retry outcome | Design finding |
|
|
150
|
+
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
151
|
+
| 1..N | 1..M | named boundary | exact call | side effects already committed | ordinal-selecting mechanism | test plus operation ID | exact surface | before effect / remote committed, acknowledgement lost / after durable write | rows and transactions | remote effects | next user read | heals or stays broken | ordering defect or none |
|
|
152
|
+
|
|
153
|
+
For every row, assert all residual-state columns. A response-only assertion is an
|
|
154
|
+
incomplete T4 test. Every external mutation gets rows for failure before the effect,
|
|
155
|
+
remote commit with acknowledgement-loss, and failure after the following durable
|
|
156
|
+
write. For acknowledgement-loss, inspect remote state or a provider status query,
|
|
157
|
+
assert the idempotency or reconciliation record, and prove retry reaches exactly one
|
|
158
|
+
effect. Durable intent or an outbox makes repair observable; it does not by itself
|
|
159
|
+
prove a retry cannot reissue the remote mutation. Treat **release → network or external action → reacquire** as the
|
|
160
|
+
highest-risk injection window: the later acquisition competes with work admitted
|
|
161
|
+
during the gap and is commonly the least tested. Classify each external interaction
|
|
162
|
+
as a read-only prerequisite, reversible reservation, idempotent mutation, or
|
|
163
|
+
irreversible side effect. A read-only prerequisite is not an ordering defect. For a
|
|
164
|
+
mutation, prefer durable intent or an outbox before the effect when possible. If the
|
|
165
|
+
effect can complete before any durable record makes it observable and repairable,
|
|
166
|
+
report a design defect. When required sequencing puts the mutation first, require
|
|
167
|
+
idempotency, compensation, reconciliation, and failure tests for each residual state.
|
|
168
|
+
|
|
169
|
+
| Failure | Injection technique | What to assert |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| PG: connection drop mid-tx | `SELECT pg_terminate_backend(pid)` from sibling conn | 503 (not 500), tx rolled back, no orphan row |
|
|
172
|
+
| PG: deadlock | Two concurrent tx grabbing rows in opposite order | One retries, other succeeds, no data loss |
|
|
173
|
+
| PG: lock timeout | `SET lock_timeout = '50ms'` on test conn | Specific error code, no busy-loop |
|
|
174
|
+
| PG: first acquisition pool exhausted | `pool_size=1`, hold conn from sibling test | 503 or queue, not crash; proves acquisition 1 only |
|
|
175
|
+
| PG: chosen acquisition unavailable | Operation-scoped pool failpoint fails the selected boundary call | Residual rows and external effects match that matrix row; retry outcome asserted |
|
|
176
|
+
| Redis: down | `toxiproxy` disable, or `docker pause` redis | Fallback executes; no busy-loop on reset |
|
|
177
|
+
| Redis: slow | `toxiproxy` latency 5s | Timeout path triggers, request fails fast |
|
|
178
|
+
| Redis: partition mid-stream | `toxiproxy` cut after N bytes | Reconnect with `Last-Event-ID`, no duplicate processing |
|
|
179
|
+
| External 5xx (Stripe/OpenAI) | Mock HTTP server returns 502 | Retry budget honoured, eventual surface to user |
|
|
180
|
+
| External timeout | Mock HTTP server delays past `timeout_ms` | Caller times out, doesn't propagate hang |
|
|
181
|
+
| External acknowledgement lost | Mock server commits then drops the response | Remote status confirms one effect; idempotency or reconciliation fences retry |
|
|
182
|
+
| TCP half-open | Raw socket `SO_LINGER 0` close | Detected within `SO_RCVTIMEO`, not silent hang |
|
|
183
|
+
| Disk: ENOSPC on tmp | tmpfs with size cap | Surface error, no partial-write corruption |
|
|
184
|
+
| Clock skew | Fake clock past lease TTL | Lease re-issued, no stale-holder state |
|
|
185
|
+
|
|
186
|
+
**Distinguish expected vs fatal.** Timeout (expected) and connection-reset (fatal) must take different paths. A function returning null/fallback for both causes busy-loops.
|
|
187
|
+
|
|
188
|
+
### T5 — Concurrency & isolation
|
|
189
|
+
|
|
190
|
+
- **Concurrent identical requests** — N callers, same idempotency key → one effect, N OK responses
|
|
191
|
+
- **Concurrent conflicting writes** — two callers update same row → one wins cleanly, no corruption
|
|
192
|
+
- **Lease contention** — N callers race for same lease → one acquires, others queue or fail predictably
|
|
193
|
+
- **Transaction isolation** — assert no dirty reads, no phantom reads at chosen level
|
|
194
|
+
- **Randomised execution order** — suite passes under `--shuffle` / `pytest -p randomly` / equivalent. Ordered-only pass = hidden coupling, fix isolation.
|
|
195
|
+
- **Per-test isolation is the test's responsibility** — txn rollback, unique IDs, namespaced keys, `TRUNCATE`, or Redis `FLUSHDB` scoped to the test. Suite-level resets (drop+migrate between full runs, schema teardown scripts) are setup hygiene, **not** a substitute for per-test isolation: they only make the *first* run from clean state correct. Tests run inside one process must not see each other's rows.
|
|
196
|
+
|
|
197
|
+
**Parallelism at scale (≥100 connections) — proves there's no hidden global lock.**
|
|
198
|
+
A single global `Thread.Mutex` keeps every correctness test green while serialising
|
|
199
|
+
every request; this is the regression that proof exists to catch.
|
|
200
|
+
- **≥100 concurrent connections** against real PG + Redis, released together off a
|
|
201
|
+
**barrier** so contention is real and reproducible (not staggered by spawn latency).
|
|
202
|
+
- **Correctness invariant (deterministic hard gate):** same row / idempotency key / lease
|
|
203
|
+
under the 100-way race → **exactly one effect**, no lost update.
|
|
204
|
+
- **Parallelism assertion (counter, deterministic):** peak simultaneous in-flight ≥ a
|
|
205
|
+
threshold, or pool lock-wait ≈ 0. Wall-clock fallback: 100 concurrent ops finish in
|
|
206
|
+
**< R×** a single op (R≈10 with margin — a global lock blows past ~100×), median-of-K.
|
|
207
|
+
- **Verdict invariance:** pass K consecutive runs under randomised order; race detector /
|
|
208
|
+
`loom` (Rust) clean where the stack supports it.
|
|
209
|
+
- **A serialization bottleneck is a *structural* finding** — propose the concurrent
|
|
210
|
+
redesign (shard the lock · per-key/per-shard locks · lock-free CAS), surface it; the
|
|
211
|
+
≥100-connection parallelism counter is the acceptance proof of the redesign. Don't hack
|
|
212
|
+
around the lock.
|
|
213
|
+
|
|
214
|
+
### T6 — Resource lifecycle
|
|
215
|
+
|
|
216
|
+
Proofs the system doesn't bleed:
|
|
217
|
+
- **Drain audit** — `make check-pg-drain` (or equivalent) green after suite. Every `conn.query()` paired with `.drain()` before `deinit()`.
|
|
218
|
+
- **Connection leak** — open count before == after across N requests
|
|
219
|
+
- **Memory leak** — Zig: `std.testing.allocator` over full request lifecycle. Rust: `dhat`/`heaptrack` over N iters. Python: `objgraph`/`gc` over N iters.
|
|
220
|
+
- **FD leak** — `lsof -p $$ | wc -l` before/after holds
|
|
221
|
+
- **Pool sizing** — load N>pool_size concurrent → backpressure works, no crash
|
|
222
|
+
- **Graceful shutdown** — SIGTERM mid-request → in-flight completes, new requests rejected with 503
|
|
223
|
+
|
|
224
|
+
**Zero leaks over the *real* request lifecycle, error paths included (deterministic).**
|
|
225
|
+
"0 leaks" means every path frees — including the ones that only run on failure.
|
|
226
|
+
- Run the leak audit over the **full** request lifecycle (init → route → middleware →
|
|
227
|
+
handler → deinit / arena reset), not a handler unit, so cross-thread and cross-request
|
|
228
|
+
leaks surface.
|
|
229
|
+
- **Zig error-path proof:** wrap allocating handlers/repos under
|
|
230
|
+
`std.testing.checkAllAllocationFailures` (or a `FailingAllocator` loop) — it fails each
|
|
231
|
+
allocation site in turn and asserts no leak on the resulting error return. This is the
|
|
232
|
+
deterministic proof every `errdefer` is correct; exhaustive over alloc sites ⇒ invariant.
|
|
233
|
+
- **No cross-request growth:** drive N requests and assert outstanding-bytes / high-water
|
|
234
|
+
does not grow monotonically (a slow leak shows as linear growth).
|
|
235
|
+
- **Gate:** `make memleak` → **0 leaks** + `make check-pg-drain` clean, both pasted into PR
|
|
236
|
+
Session Notes; cross-compile both targets before declaring done.
|
|
237
|
+
|
|
238
|
+
**Complexity / latency budget under load (counters over clocks).**
|
|
239
|
+
- **N+1 detector:** assert DB/Redis round-trip count per request is **constant or linear**
|
|
240
|
+
in payload size n — count at n and 10n; multiplicative growth = an N+1 to fix.
|
|
241
|
+
- **Complexity ladder:** measure a work counter (round-trips, rows scanned, allocations)
|
|
242
|
+
at n, 2n, 4n, 8n; assert the growth matches the claimed O-bound (noise-free verdict).
|
|
243
|
+
- **Latency under the ≥100-conn load:** p95/p99 within budget vs a **pinned baseline file**,
|
|
244
|
+
median-of-K — single samples banned.
|
|
245
|
+
- **Super-linear or serialization finding ⇒ propose the structural refactor**, surface it
|
|
246
|
+
for decision (root cause + target design + patch alternative + recommendation); the test
|
|
247
|
+
pins the **target** bound so the refactor is provably met. Don't band-aid.
|
|
248
|
+
|
|
249
|
+
### T7 — Streaming / transport (only if SSE / chunked / WebSocket / gRPC streaming present)
|
|
250
|
+
|
|
251
|
+
Spec claim "real-time" → assert *bytes arrive incrementally*, not just that the parser handles pre-buffered data.
|
|
252
|
+
|
|
253
|
+
- **Incremental delivery** — consumer reads N bytes, asserts more not yet flushed, advances time/clock, reads next chunk
|
|
254
|
+
- **Backpressure** — slow consumer → producer pauses, no unbounded buffer growth
|
|
255
|
+
- **Reconnect with `Last-Event-ID`** — kill stream mid-flight, reconnect, only missed events replay
|
|
256
|
+
- **Heartbeat** — idle stream emits keepalive within SLA (use fake clock to verify)
|
|
257
|
+
- **Mid-stream failure** — producer dies mid-frame → consumer sees error, not silent truncation
|
|
258
|
+
- **Cancel propagation** — client disconnects → producer stops work within bounded time
|
|
259
|
+
|
|
260
|
+
### T8 — API rules & schema
|
|
261
|
+
|
|
262
|
+
- Request and response validated against OpenAPI/protobuf in-test (not just at server startup)
|
|
263
|
+
- `oasdiff breaking origin/main..HEAD` exits 0 (PR gate)
|
|
264
|
+
- Generated SDK/client compiles against the spec change
|
|
265
|
+
- Deprecation headers present for deprecated fields/endpoints
|
|
266
|
+
- Versioning: API path/header version matches handler
|
|
267
|
+
|
|
268
|
+
### T9 — Client-daemon & process supervision (split-architecture services)
|
|
269
|
+
|
|
270
|
+
For a service that is **not** an HTTP server but a **client daemon** holding no datastore — e.g. the `agentsfleet-runner`: register → heartbeat → long-poll lease → fork a sandboxed child → report — the router/request tiers don't map. Integration testing it means driving its protocol loop against a real (or harness) control plane and exercising the process lifecycle that *is* its public surface.
|
|
271
|
+
|
|
272
|
+
- **Protocol loop against a real control plane** — stand up the control-plane HTTP server (real handlers, real PG/Redis behind it); the daemon registers, leases a seeded event, executes, reports; assert the durable rows the report writes (T3 state assertions), not just that the loop ran.
|
|
273
|
+
- **Sudden kill (SIGKILL, not SIGTERM) mid-lease** — the cattle-not-pets case, distinct from graceful drain (T6). Kill the daemon while a lease is in flight; assert the lease expires at its deadline, the reclaim sweep re-issues it with a higher fencing token to another holder, the event is processed **exactly once**, and the dead holder's late report is **fenced** (stale-token reject), never a double-write.
|
|
274
|
+
- **Restart / re-register** — bring the daemon back; assert it re-registers and resumes leasing with no duplicate processing of in-flight work and no orphan lease left held.
|
|
275
|
+
- **Flaky control-plane link (client side)** — the daemon is an HTTP *client* over an unreliable link: lease long-poll drops, heartbeat fails, report fails *after* the child already ran. Assert backoff-without-crash, the un-acked lease redelivers, and report retry is idempotent (a second delivery of the same outcome is fenced/deduped, not double-applied).
|
|
276
|
+
- **Forked-child lifecycle** — the child is always reaped (no orphaned processes) and the sandbox/cgroup scope always destroyed (idempotent) on every exit path including timeout and crash; a child past its deadline is killed and the outcome **classified** (timeout vs OOM vs crash), not hung.
|
|
277
|
+
|
|
278
|
+
Injection: `kill -9 <pid>` / `docker kill` for sudden death; `toxiproxy` or a pause on the control-plane endpoint for the flaky link; a fake clock past the lease TTL for reclaim.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Determinism & harness hygiene (the test infra is a test too)
|
|
283
|
+
|
|
284
|
+
A flaky harness fails good code and trains the team to re-run until green — the opposite of what integration tests are for. The harness gets the same rigor as the system. Two failure classes, **both observed in this repo**, are suite-blocking and non-negotiable:
|
|
285
|
+
|
|
286
|
+
- **Port binding: bind-and-hold, never alloc-then-rebind.** A harness that asks the OS for a free port, closes the socket, then has the server re-`bind()` that number races every other test doing the same — the port is grabbed in the gap and the server's `listen()` aborts (`EADDRINUSE`). **Rule:** the harness binds the listener **once** and hands the *already-bound* socket/fd to the server (no close-reopen window); if a number must be pre-allocated, bind with `SO_REUSEADDR` + retry on conflict and treat a bind failure as **retryable, never a panic**. A harness `listen()` that can `panic` on a bind race is a defect in the harness, not a flaky test.
|
|
287
|
+
- **Reset/teardown must be catalog-derived, never a hand-maintained list.** A teardown that drops a hardcoded set of schemas/tables silently rots the moment a migration adds a schema — the new schema survives teardown, the "0 objects remain" assertion trips, and the *whole suite can't start* (observed: a `fleet` schema added by a migration, absent from the static drop list). **Rule:** derive the drop/truncate set from the live catalog (`information_schema.schemata` / `pg_tables`) or from the migration set; OR add a test that fails when a schema the migrations create is missing from the teardown. The teardown is **verified against** the schema, not assumed to match it.
|
|
288
|
+
|
|
289
|
+
Both make every downstream test a lie — fix them before adding coverage. A green suite on a stale teardown or a racy harness is worth less than no suite.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Hard rules
|
|
294
|
+
|
|
295
|
+
- **Real deps or it's not integration.** Mocking PG, Redis, or any internal module disqualifies. External-edge HTTP may be mocked at the HTTP boundary, not at the SDK call site.
|
|
296
|
+
- **Assert state, not just response.** Body assertion alone is half a test.
|
|
297
|
+
- **Per-test isolation is the test's job.** Txn rollback, unique IDs, namespaced keys, or scoped truncation — pick one and apply per test. Suite-level resets (drop+migrate before the run, fixture teardown scripts) are setup hygiene; they don't isolate tests from each other inside one run. Suite passes shuffled.
|
|
298
|
+
- **Tier may be env-gated, not binary-gated.** Many projects ship one test binary that runs both unit and integration tests, with env vars / build tags / pytest markers / Zig comptime flags switching the live-deps tier on. That's fine — but the env interface that flips the gate must be documented in the suite README and surfaced in Continuous Integration (CI), not folklore.
|
|
299
|
+
- **Specific error contracts.** Status code + error code + message + structured fields. Bare `expect 500` is a smell.
|
|
300
|
+
- **Failure injection is deterministic.** If you can't reproduce a failure on demand, the test is fiction.
|
|
301
|
+
- **Inject at the selected ordinals, not only the first interaction.** Exhausting a resource pool tests the first acquisition and nothing after it. Repeated acquisitions require a seam that selects both the workflow ordinal and boundary-call ordinal, exhaustively.
|
|
302
|
+
- **Instrument real dependencies without replacing them.** An operation-scoped failpoint may wrap a real client or pool only when every unselected call delegates to the real dependency, the selected call fails before acquisition, and a separate real-dependency test proves production error mapping. Replacing the pool or client with synthetic behavior disqualifies the integration proof.
|
|
303
|
+
- **Partial failure proves residual state.** For every injected failure, assert durable rows, external state, what the user sees next, and whether retry heals it. A status code alone cannot complete T4.
|
|
304
|
+
- **Unsafe external mutation ordering is a design defect.** Read-only prerequisites are valid. If a mutation can complete before any durable intent or repair record, report it; prefer durable intent first, or require idempotency, compensation, and reconciliation when sequencing cannot change.
|
|
305
|
+
- **Acknowledgement loss needs a remote-state proof.** For every external mutation, make the remote effect succeed while its acknowledgement is lost; inspect remote state, assert the idempotency or reconciliation record, and prove retry reaches exactly one effect. A durable record alone is not an exactly-once proof.
|
|
306
|
+
- **Record the partial-completion matrix.** Paste the completed rows into PR Session Notes. A checked box without the rows is indistinguishable from a skipped proof.
|
|
307
|
+
- **Drain + leak proofs are required, not optional.** Once per suite, recorded in PR Session Notes.
|
|
308
|
+
- **No hitting deployed environments.** Integration tests run against `make up` containers, never `api-dev`/`api`.
|
|
309
|
+
- **Test names read as documentation.** `should_503_when_redis_down`, `should_release_lease_on_handler_panic`, `should_not_double_charge_on_idempotency_replay`.
|
|
310
|
+
- **Cover the scenario axis per surface.** Each surface gets, explicitly: a **positive** path, a **negative** path (rejected input → specific error shape), an **invalid** case (oversized / wrong-typed / unknown-enum / bad-token / malformed payload), a **flaky-connection** case (T4 for a server, T9 for a client daemon), a **kill-and-restart** case wherever a process holds state (T9), and a **concurrency** case (T5). A surface missing one axis is under-covered, not done — name the missing axis in the coverage report rather than declaring green.
|
|
311
|
+
- **Guard the harness as a test (Determinism & harness hygiene).** Server binds-and-holds its port; teardown/reset is catalog-derived. A suite on a racy harness or a stale teardown is a false signal — fix it before trusting any result.
|
|
312
|
+
|
|
313
|
+
## Anti-patterns (reject in review)
|
|
314
|
+
|
|
315
|
+
- ❌ Mocking the DB or Redis "for speed" — that's a unit test, label it correctly
|
|
316
|
+
- ❌ Asserting only the HTTP response body
|
|
317
|
+
- ❌ Tests that pass only when run in a specific order
|
|
318
|
+
- ❌ Bare `expect 500` / `assert.throws` with no error-code shape
|
|
319
|
+
- ❌ Hand-wavy "handles failure" with no injection mechanism named
|
|
320
|
+
- ❌ Sharing state across tests via module-level fixtures
|
|
321
|
+
- ❌ Hitting deployed environments from the integration suite
|
|
322
|
+
- ❌ Skipping drain/leak audits because "it's just a test"
|
|
323
|
+
- ❌ Asserting on log lines via string matching instead of structured fields
|
|
324
|
+
- ❌ Calling the handler function directly, bypassing router + middleware
|
|
325
|
+
- ❌ Draining an entire pool and calling it pool-exhaustion coverage for later acquisitions
|
|
326
|
+
- ❌ Asserting a failure status without asserting what the operation left behind and whether retry heals it
|
|
327
|
+
- ❌ Treating a durable intent row as proof that acknowledgement-loss cannot reissue a remote effect
|
|
328
|
+
|
|
329
|
+
## CI execution strategy
|
|
330
|
+
|
|
331
|
+
| Stage | What runs | Why |
|
|
332
|
+
|---|---|---|
|
|
333
|
+
| **PR** | Smoke mode on changed surface + randomised order | Fast feedback, catches obvious wiring breaks |
|
|
334
|
+
| **Pre-merge** | Standard mode on changed module + 1× full suite from clean (`make down && make up && make test-integration`) | Detects state pollution between tests |
|
|
335
|
+
| **Nightly** | Hardening mode + chaos pass (random failure injection across deps) | Catches rare-path bugs |
|
|
336
|
+
| **Pre-release** | Full Hardening + `make memleak` + `make bench` + cross-platform | Final sign-off |
|
|
337
|
+
|
|
338
|
+
Suite passes shared but fails clean = state pollution, not a bug. Fix isolation before re-running.
|
|
339
|
+
|
|
340
|
+
## Spec integration
|
|
341
|
+
|
|
342
|
+
When the spec has these tables, treat them as authoritative test sources:
|
|
343
|
+
|
|
344
|
+
| Spec table | Generates |
|
|
345
|
+
|---|---|
|
|
346
|
+
| Failure Modes | One T4 test per row with deterministic injection |
|
|
347
|
+
| Partial Completion | One matrix row per ordered failure point, with workflow-ordinal and boundary-call-ordinal injection plus residual-state assertions |
|
|
348
|
+
| Error Contracts | One T2 test per row with specific status + error code |
|
|
349
|
+
| Concurrency Contracts | T5 tests for stated isolation level + idempotency keys |
|
|
350
|
+
| Resource Limits | T6 tests for pool size, timeout, rate-limit |
|
|
351
|
+
| Streaming Contracts | T7 tests for incremental delivery + reconnect |
|
|
352
|
+
| Recovery / Failover (lease, daemon) | T9 sudden-kill → reclaim + re-fence + exactly-once; restart → resume |
|
|
353
|
+
|
|
354
|
+
If no such tables, build a claim-tracing table from spec prose / OpenAPI:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
| Claim | Tier | Injection | Test? |
|
|
358
|
+
| "503 on Redis down" | T4 | toxiproxy disable | ❌ |
|
|
359
|
+
| "lease released on handler panic" | T6 | force panic in test | ❌ |
|
|
360
|
+
| "idempotency replay returns same id" | T5 | duplicate request | ❌ |
|
|
361
|
+
| "SSE bytes arrive within 100ms" | T7 | partial-read reader | ❌ |
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Every claim → ≥1 test. Untestable claims → flag `needs infra`, never silently skip.
|
|
365
|
+
|
|
366
|
+
## Stack appendix
|
|
367
|
+
|
|
368
|
+
| Stack | Suite runner | Real PG | Real Redis | Drain/leak | Concurrency |
|
|
369
|
+
|---|---|---|---|---|---|
|
|
370
|
+
| Zig (agentsfleet) | `make test-integration` | `make up` container | `make up` container | `std.testing.allocator` + `make check-pg-drain` | `std.Thread.spawn` |
|
|
371
|
+
| Zig runner (client daemon) | `make test-integration` (protocol loop vs a harness/real control plane) | via the control plane (runner holds none) | via the control plane | `std.testing.allocator` over parent + forked child | `fork` + `kill -9` / fake-clock past lease TTL for reclaim |
|
|
372
|
+
| Python | `pytest -m integration` | `testcontainers-python` | `testcontainers-python` | `gc` + `objgraph` over N iters | `asyncio.gather` / `ThreadPoolExecutor` |
|
|
373
|
+
| Rust | `cargo test --test 'integration_*'` | `testcontainers-rs` / `sqlx::test` | `testcontainers-rs` | `dhat-rs`; `Drop` checks | `tokio::spawn` + `loom` for races |
|
|
374
|
+
| Node/Bun | `bun test --integration` | `testcontainers-node` | `testcontainers-node` | manual handle counting | `Promise.all` |
|
|
375
|
+
|
|
376
|
+
Cross-stack failure-injection tools:
|
|
377
|
+
|
|
378
|
+
| Need | Tool |
|
|
379
|
+
|---|---|
|
|
380
|
+
| HTTP fault between services | `toxiproxy` |
|
|
381
|
+
| PG mid-tx kill | `pg_terminate_backend(pid)` from sibling conn |
|
|
382
|
+
| Redis down | `toxiproxy` disable; or `docker pause redis` |
|
|
383
|
+
| Network partition | `toxiproxy` down; `iptables`/`pfctl` block |
|
|
384
|
+
| Slow / latency | `toxiproxy` latency toxic |
|
|
385
|
+
| Partial read | wrapper `Reader` returning short |
|
|
386
|
+
| Clock skew | fake clock injection at app boundary |
|
|
387
|
+
| Sudden process death (no graceful drain) | `kill -9 <pid>` / `docker kill <ctr>` mid-lease |
|
|
388
|
+
| Free-port without a rebind race | bind once + hand the bound fd to the server (`SO_REUSEADDR` + retry on conflict) |
|
|
389
|
+
|
|
390
|
+
Stack-specific must-knows:
|
|
391
|
+
|
|
392
|
+
- **Zig** — `conn.query()` paired with `.drain()` in the same fn before `deinit()`; verify with the project's drain-audit target. Per-request arena, not per-app. `std.testing.allocator` over the full request lifecycle, not just handler unit, to catch cross-thread leaks. If integration tests share the binary with unit tests, an env var / build flag should gate live-deps mode — assert that gate is set in CI, not assumed. Cross-compile (`x86_64-linux`, `aarch64-linux`) before declaring done.
|
|
393
|
+
- **Python** — `pytest-asyncio` event loop scope = `function`, not `session` (avoid leakage). `respx`/`responses` for external HTTP only.
|
|
394
|
+
- **Rust** — `#[sqlx::test]` for per-test schema; `tokio::test(flavor = "multi_thread")` for real concurrency. `loom` for races, not raw threads.
|
|
395
|
+
- **Node** — `--isolate` per test if framework supports; otherwise reset module registry.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Output format
|
|
400
|
+
|
|
401
|
+
For each test write:
|
|
402
|
+
|
|
403
|
+
1. Name (per stack convention, reads as documentation)
|
|
404
|
+
2. Tier(s) it satisfies
|
|
405
|
+
3. The integration bug it catches that a unit test could not
|
|
406
|
+
4. Failure-injection mechanism (required for T4 / T7)
|
|
407
|
+
5. Residual-state and retry assertions (required for T4 partial-completion rows)
|
|
408
|
+
6. The test code
|
|
409
|
+
|
|
410
|
+
After the suite, produce:
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
## Integration Coverage Report: <module>
|
|
414
|
+
| Tier | Required | Met | Status |
|
|
415
|
+
| T1 Real-dependency wiring | yes | 4/4 | ✅ |
|
|
416
|
+
| T2 Request lifecycle | yes | 6/6 | ✅ |
|
|
417
|
+
| T3 State assertions | yes | 5/5 | ✅ |
|
|
418
|
+
| T4 Failure injection | yes | 3/8 | 🔴 |
|
|
419
|
+
| T5 Concurrency & isolation | yes | 2/2 | ✅ |
|
|
420
|
+
| T6 Resource lifecycle | yes | 1/2 | 🟡 |
|
|
421
|
+
| T7 Streaming / transport | n/a | — | 🟣 |
|
|
422
|
+
| T8 API rules & schema | yes | 1/1 | ✅ |
|
|
423
|
+
| T9 Client-daemon supervision | n/a | — | 🟣 |
|
|
424
|
+
Mode: Smoke | Standard | Hardening
|
|
425
|
+
Scenario axis: positive ✅ · negative ✅ · invalid 🟡 · flaky-conn ✅ · kill/restart ✅ · concurrency ✅
|
|
426
|
+
Harness: ✅ port bind-and-hold · ✅ teardown catalog-derived (survives a new schema)
|
|
427
|
+
DoD: 9/12 — return to EXECUTE before declaring done
|
|
428
|
+
Drain audit: ✅ make check-pg-drain clean
|
|
429
|
+
Leak audit: 🔴 std.testing.allocator reported 2 leaks across 100 requests
|
|
430
|
+
Shuffle: ✅ pass under --shuffle
|
|
431
|
+
Clean run: ✅ pass after make down && make up
|
|
432
|
+
100-conn: ✅ exactly-once + parallel (peak in-flight 64/100, lock-wait 3ms)
|
|
433
|
+
Complexity: ✅ round-trips constant n→10n; p95 42ms < 50ms budget vs baseline
|
|
434
|
+
Refactor proposals surfaced: none (or "1 — <one-line summary>")
|
|
435
|
+
Partial completion: ✅ <rows complete>/<ordered failure points> · residual state + retry asserted per row
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Legend: `✅` met · `🟡` partially met (specify gaps) · `🔴` missing required (block) · `🟣` not applicable (state why)
|
|
439
|
+
|
|
440
|
+
## Definition of Done
|
|
441
|
+
|
|
442
|
+
Block the change if any are missing on touched surface:
|
|
443
|
+
|
|
444
|
+
- [ ] Every changed handler/service/repo has ≥1 T1+T2+T3 happy-path test
|
|
445
|
+
- [ ] Every `catch`/`orelse`/`except`/`Err` in the request path has a T4 injection test
|
|
446
|
+
- [ ] Every concurrent-write or idempotency claim has a T5 test
|
|
447
|
+
- [ ] Drain audit (`make check-pg-drain`) clean post-suite
|
|
448
|
+
- [ ] Leak audit clean post-suite (Zig: `std.testing.allocator`; Rust: `dhat`; Python: `objgraph`)
|
|
449
|
+
- [ ] Suite passes under randomised execution order
|
|
450
|
+
- [ ] Suite passes from clean state (`make down && make up && make test-integration`)
|
|
451
|
+
- [ ] No mocking of DB, Redis, or internal modules
|
|
452
|
+
- [ ] Test names read as documentation
|
|
453
|
+
- [ ] OpenAPI / protobuf breaking-change check (`oasdiff` / `buf breaking`) clean
|
|
454
|
+
- [ ] If streaming touched: T7 incremental-delivery test present
|
|
455
|
+
- [ ] Failure-injection mechanism named for every T4/T7 test (no fiction)
|
|
456
|
+
- [ ] **Partial completion:** every operation touching multiple systems or acquiring the same resource more than once carries a matrix with one row per ordered failure point; each row selects workflow and boundary-call ordinals, failure timing / remote outcome, scopes a concurrency-safe failpoint to one operation, and asserts durable, external, user-visible, and retry residual state; every external mutation proves acknowledgement-loss remote state plus exactly one effect on retry; the completed matrix is pasted into PR Session Notes
|
|
457
|
+
- [ ] PR Session Notes paste final `make test-integration` + `make memleak` lines
|
|
458
|
+
- [ ] **≥100-connection** test proves exactly-once + parallelism (peak-concurrency/lock-wait counter, or wall-time < R×), passes K runs
|
|
459
|
+
- [ ] **Zig error-path leak:** allocating handlers/repos proven by `std.testing.checkAllAllocationFailures`; no cross-request high-water growth over N requests
|
|
460
|
+
- [ ] **Complexity under load:** N+1 round-trips ruled out (count at n vs 10n); work-counter ladder asserts the O-bound; p95 within budget vs pinned baseline
|
|
461
|
+
- [ ] **Refactor escalation:** any super-linear or serialization finding carries a refactor proposal (root cause + target design + patch alternative + recommendation) surfaced for decision — not silently band-aided
|
|
462
|
+
- [ ] **Scenario axis** covered per surface: positive · negative · invalid · flaky-connection · kill/restart (stateful processes) · concurrency — gaps named in the coverage report
|
|
463
|
+
- [ ] **Harness hygiene:** server binds-and-holds its port (no rebind race, no `listen()` panic); teardown/reset is catalog-derived (proven by adding a throwaway schema and re-running teardown — it must still reach zero)
|
|
464
|
+
- [ ] **Client daemon (if present, T9):** sudden SIGKILL mid-lease → reclaim + re-fence + exactly-once + stale report fenced; restart → re-register + resume, no duplicate, no orphan lease; flaky control-plane link → backoff without crash + un-acked redelivery + idempotent report retry; forked child always reaped + scope always destroyed on every exit path
|
|
465
|
+
|
|
466
|
+
100% required before declaring `make test-integration` done.
|
|
467
|
+
|
|
468
|
+
## Validation gate
|
|
469
|
+
|
|
470
|
+
Run `make test-integration` (or stack equivalent) twice — once shared, once from clean (`make down && make up`). Run once more under randomised order. Report pass/fail/skip counts, drain audit, leak audit, the coverage table above, and the DoD checklist. Do not declare done until DoD is 100%.
|