@@ -174,6 +174,128 @@ verifiable so that tampering is detectable.*
174174
175175---
176176
177+ #### Audit Hash Chain Integrity (new)
178+
179+ ** File:** ` test_audit_chain.py ` (integration)
180+
181+ ** Story (P1):** * As a security lead, I want each persisted audit event to carry
182+ a SHA-256 hash of the previous event so that any tampering is
183+ cryptographically detectable.*
184+
185+ | Assertion | Details |
186+ | ---| ---|
187+ | Clean log verifies OK | ` verify_audit_chain() ` returns ` valid=True ` |
188+ | First event has ` prev_hash="genesis" ` | Chain anchor constant |
189+ | Second event's ` prev_hash ` equals first's ` hash ` | Sequential linkage |
190+ | Content tampering detected | Mutating ` event_type ` breaks the hash |
191+ | Event insertion detected | Foreign event with wrong ` prev_hash ` rejected |
192+ | Event deletion detected | Gap in chain detected on line 3 |
193+ | Multi-run log verifies OK | Two runs in one JSONL all pass |
194+
195+ ---
196+
197+ #### Plugin Loader (new)
198+
199+ ** File:** ` test_plugins.py ` (integration)
200+
201+ ** Story (P1):** * As a third-party author, I want to register tools via
202+ ` teaagent.tools ` entry-points so that ` pip install ` adds tools with no manual
203+ wiring.*
204+
205+ | Assertion | Details |
206+ | ---| ---|
207+ | No plugins → empty result | ` loaded == [] ` , ` failed == [] ` |
208+ | Single plugin registers tool | Tool appears in ` ToolRegistry ` |
209+ | Multiple plugins all loaded | 3 entry-points → 3 tools |
210+ | Failing plugin isolated | Bad plugin in ` failed ` , others unaffected |
211+ | ` ep.load() ` error isolated | ` ImportError ` quarantined |
212+ | Custom group respected | ` group= ` parameter forwarded to ` entry_points() ` |
213+ | ` PLUGIN_GROUP == 'teaagent.tools' ` | Canonical group constant |
214+
215+ ---
216+
217+ #### A2A Circuit Breaker (new)
218+
219+ ** File:** ` test_a2a_circuit_breaker.py ` (integration)
220+
221+ ** Story (P1):** * As a platform engineer, I want unhealthy remote agents to be
222+ skipped automatically so that one bad endpoint does not block the entire
223+ federation refresh.*
224+
225+ | Assertion | Details |
226+ | ---| ---|
227+ | Circuit opens after ` failure_threshold ` | ` circuit_state() ` returns ` 'open' ` |
228+ | Open circuit skips endpoint | Second refresh URL not in call log |
229+ | Success resets failure count | ` circuit_state() ` returns ` 'closed' ` |
230+ | Circuit resets after ` reset_timeout_seconds ` | Half-open → retry → closed |
231+ | No ` circuit_breaker ` → retries every time | Backward-compatible default |
232+ | Unknown endpoint is ` 'closed' ` | ` circuit_state('http://unknown') ` = ` 'closed' ` |
233+ | Healthy endpoints still return cards | Even when one is open |
234+
235+ ---
236+
237+ #### Config Layering + Workspace Profile (new)
238+
239+ ** File:** ` test_config_loader.py ` (integration)
240+
241+ ** Story (P2 — AC-NEW-3):** * As a developer, I want my workspace
242+ ` .teaagent/config.json ` to set default permission-mode and iteration limits so
243+ that I don't have to repeat CLI flags on every run.*
244+
245+ | Assertion | Details |
246+ | ---| ---|
247+ | ` ResolvedConfig.get() ` returns value | By key name |
248+ | ` ResolvedConfig.source() ` returns layer | ` ConfigLayer.WORKSPACE ` etc. |
249+ | ` show() ` lists key=value [ layer] lines | Human-readable |
250+ | Workspace config loaded | ` .teaagent/config.json ` values in ` ResolvedConfig ` |
251+ | User config loaded | ` ~/.teaagent/config.json ` values in ` ResolvedConfig ` |
252+ | Workspace overrides user | Same key → workspace wins |
253+ | Env overrides workspace | ` TEAAGENT_PERMISSION_MODE ` beats file |
254+ | ` ChatAgentConfig.from_root() ` applies profile | ` permission_mode ` , ` max_iterations ` set from config |
255+ | Explicit kwargs override profile | Caller wins over workspace config |
256+
257+ ---
258+
259+ #### Run Undo Journal (new)
260+
261+ ** File:** ` test_run_undo.py ` (integration)
262+
263+ ** Story (P2 — AC-NEW-6):** * As a developer, I want ` UndoJournal ` to track
264+ pre-write file state so that I can revert all workspace writes from a run.*
265+
266+ | Assertion | Details |
267+ | ---| ---|
268+ | Newly created file deleted on ` restore() ` | Path in ` UndoResult.deleted ` |
269+ | Overwritten file restored | Original content re-written; path in ` restored ` |
270+ | Multiple writes all undone | Two files → two deletions |
271+ | Idempotent when no writes | Empty ` restore() ` with no errors |
272+ | ` UndoResult.ok ` false on errors | Error list non-empty → ` ok=False ` |
273+ | Read-only tools not captured | No journal entry for ` workspace_read_file ` |
274+ | Path traversal attempts ignored | ` ../ ` outside root silently dropped |
275+
276+ ---
277+
278+ #### Run Export / Import (new)
279+
280+ ** File:** ` test_run_export.py ` (integration)
281+
282+ ** Story (P2 — AC-NEW-19):** * As a user, I want to export a run to a portable
283+ archive and import it on another machine so that runs are reproducible and
284+ shareable.*
285+
286+ | Assertion | Details |
287+ | ---| ---|
288+ | ` export_run() ` creates ` .tar.gz ` | Archive exists at ` output_path ` |
289+ | ` import_run() ` restores run | Events appear in destination ` RunStore ` |
290+ | Round-trip preserves all event fields | ` src_events == dst_events ` |
291+ | Import does not clobber other runs | Pre-existing run survives import |
292+ | ` ExportManifest ` fields correct | ` run_id ` , ` event_count ` , ` archive_path ` |
293+ | Import missing archive → ` FileNotFoundError ` | Clear error |
294+ | Export missing run → ` FileNotFoundError ` | Clear error |
295+ | Hash chain valid after import | ` verify_audit_chain() ` passes on imported log |
296+
297+ ---
298+
177299## Integration Tests (tests/integration/)
178300
179301| File | Coverage |
@@ -193,6 +315,12 @@ verifiable so that tampering is detectable.*
193315| ` test_streaming_tool_calls.py ` | ` on_chunk ` callbacks, audit events, token accumulation (IT-13) |
194316| ` test_schema_migration_live.py ` | Migration ordering, idempotency, data survival, version tracking (IT-14) |
195317| ` test_dpop_replay_concurrency.py ` | Concurrent JTI consume: exactly one success; expiry reset (IT-15) |
318+ | ` test_audit_chain.py ` | Hash-chain validity, tampering/insertion/deletion detection (new) |
319+ | ` test_plugins.py ` | Plugin discovery, isolation, custom group (new) |
320+ | ` test_a2a_circuit_breaker.py ` | Circuit open/close, skip, reset, backward compat (new) |
321+ | ` test_config_loader.py ` | Layer precedence, env override, workspace profile applied (new) |
322+ | ` test_run_undo.py ` | Pre-write capture, file deletion/restore, path traversal guard (new) |
323+ | ` test_run_export.py ` | Export/import round-trip, hash-chain survival, error cases (new) |
196324
197325---
198326
@@ -204,16 +332,13 @@ acceptance tests. Each maps to a Review gap analysis recommendation.
204332| ID | Story | Priority |
205333| ---| ---| ---|
206334| AC-NEW-1 | ` teaagent init ` first-time wizard | P2 |
207- | AC-NEW-3 | Workspace profile (` .teaagent/profile.toml ` ) | P2 |
208335| AC-NEW-4 | Interactive diff preview + inline y/n approval | P0 |
209- | AC-NEW-6 | ` teaagent run undo <run_id> ` workspace rollback | P2 |
210336| AC-NEW-8 | Desktop/webhook notification on ultrawork completion | P2 |
211337| AC-NEW-11 | A2A delegation carries ` traceparent ` header | P1 |
212338| AC-NEW-15 | Configurable PII redaction categories | P2 |
213339| AC-NEW-16 | HTTPS_PROXY / CA bundle / mTLS "just works" | P2 |
214340| AC-NEW-17 | ` teaagent upgrade ` schema migration dry-run preview | P2 |
215341| AC-NEW-18 | Graceful degradation when disk fills mid-run | P2 |
216- | AC-NEW-19 | ` teaagent run export / import ` | P2 |
217342| AC-NEW-20 | Local model (Ollama/vLLM) with full governance | P2 |
218343| AC-NEW-21 | ` teaagent eval run <suite> ` with HTML report | P2 |
219344| AC-NEW-22 | ` teaagent benchmark ` latency/cost regression tracking | P2 |
0 commit comments