@ikon85/agent-workflow-kit 0.16.0 → 0.16.2

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.
@@ -0,0 +1,626 @@
1
+ import importlib.util
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ import unittest
8
+ from pathlib import Path
9
+ from unittest.mock import patch
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+ HOOKS = ROOT / ".claude" / "hooks"
14
+ sys.path.insert(0, str(HOOKS))
15
+ SPEC = importlib.util.spec_from_file_location("drift_guard", HOOKS / "drift-guard.py")
16
+ DRIFT_GUARD = importlib.util.module_from_spec(SPEC)
17
+ SPEC.loader.exec_module(DRIFT_GUARD)
18
+
19
+
20
+ class CensusBackstopTest(unittest.TestCase):
21
+ def make_repo(self):
22
+ temporary = tempfile.TemporaryDirectory(prefix="awk-census-backstop-")
23
+ root = Path(temporary.name)
24
+ (root / "src").mkdir()
25
+ (root / "package.json").write_text('{"name":"consumer"}\n', encoding="utf-8")
26
+ (root / "src" / "index.mjs").write_text("export const ready = true;\n", encoding="utf-8")
27
+ subprocess.run(["git", "init", "--quiet"], cwd=root, check=True)
28
+ subprocess.run(["git", "add", "."], cwd=root, check=True)
29
+ return temporary, root
30
+
31
+ def enable(self, root, overrides=None, local_scanners=None):
32
+ census = root / ".census"
33
+ census.mkdir(exist_ok=True)
34
+ (census / "profile.json").write_text(json.dumps({
35
+ "schemaVersion": 1,
36
+ "enabled": True,
37
+ "decisions": [],
38
+ "localScanners": local_scanners or [],
39
+ "overrides": overrides or [],
40
+ }) + "\n", encoding="utf-8")
41
+
42
+ def activate_current(self, root):
43
+ fresh = DRIFT_GUARD.scan_census_status(root)["fresh"]
44
+ (root / ".census" / "active.json").write_text(
45
+ json.dumps(fresh) + "\n", encoding="utf-8"
46
+ )
47
+
48
+ def test_missing_disabled_bootstrap_and_offline_are_visible_but_fail_open(self):
49
+ temporary, root = self.make_repo()
50
+ self.addCleanup(temporary.cleanup)
51
+
52
+ missing = DRIFT_GUARD.evaluate_census(root)
53
+ self.assertEqual((missing["state"], missing["block_handoff"]), ("no_census", False))
54
+
55
+ self.enable(root)
56
+ profile = root / ".census" / "profile.json"
57
+ body = json.loads(profile.read_text(encoding="utf-8"))
58
+ body["enabled"] = False
59
+ profile.write_text(json.dumps(body) + "\n", encoding="utf-8")
60
+ disabled = DRIFT_GUARD.evaluate_census(root)
61
+ self.assertEqual((disabled["state"], disabled["block_handoff"]), ("disabled", False))
62
+
63
+ body["enabled"] = True
64
+ profile.write_text(json.dumps(body) + "\n", encoding="utf-8")
65
+ bootstrap = DRIFT_GUARD.evaluate_census(root)
66
+ self.assertEqual((bootstrap["state"], bootstrap["block_handoff"]), ("bootstrap", False))
67
+
68
+ with patch.object(DRIFT_GUARD, "scan_census_status", side_effect=OSError("node offline")):
69
+ offline = DRIFT_GUARD.evaluate_census(root)
70
+ self.assertEqual((offline["state"], offline["block_handoff"]), ("offline", False))
71
+ self.assertIn("node offline", offline["detail"])
72
+
73
+ def test_known_growth_unknown_topology_and_newer_builder_require_refresh(self):
74
+ temporary, root = self.make_repo()
75
+ self.addCleanup(temporary.cleanup)
76
+ self.enable(root)
77
+ self.activate_current(root)
78
+ self.assertEqual(DRIFT_GUARD.evaluate_census(root)["state"], "current")
79
+
80
+ (root / "packages" / "api" / "src").mkdir(parents=True)
81
+ (root / "packages" / "api" / "src" / "index.mjs").write_text(
82
+ "export const api = true;\n", encoding="utf-8"
83
+ )
84
+ subprocess.run(["git", "add", "packages"], cwd=root, check=True)
85
+ growth = DRIFT_GUARD.evaluate_census(root)
86
+ self.assertEqual((growth["state"], growth["block_handoff"]), ("refresh_required", True))
87
+ self.assertIn("topology", growth["reasons"])
88
+
89
+ self.activate_current(root)
90
+ (root / "services" / "payments" / "src").mkdir(parents=True)
91
+ (root / "services" / "payments" / "src" / "index.mjs").write_text(
92
+ "export const payments = true;\n", encoding="utf-8"
93
+ )
94
+ unknown = DRIFT_GUARD.evaluate_census(root)
95
+ self.assertEqual((unknown["state"], unknown["block_handoff"]), ("refresh_required", True))
96
+ self.assertIn("open", unknown["reasons"])
97
+
98
+ (root / "services").rename(root / "evidence-only")
99
+ active_path = root / ".census" / "active.json"
100
+ active = json.loads(active_path.read_text(encoding="utf-8"))
101
+ active["fingerprints"]["builder"] = "older-builder"
102
+ active_path.write_text(json.dumps(active) + "\n", encoding="utf-8")
103
+ newer = DRIFT_GUARD.evaluate_census(root)
104
+ self.assertEqual((newer["state"], newer["block_handoff"]), ("refresh_required", True))
105
+ self.assertIn("builder", newer["reasons"])
106
+
107
+ def test_change_local_override_never_greens_real_drift(self):
108
+ temporary, root = self.make_repo()
109
+ self.addCleanup(temporary.cleanup)
110
+ self.enable(root, overrides=[{"scope": "this change", "reason": "generated path alias"}])
111
+ self.activate_current(root)
112
+ (root / "apps" / "web" / "src").mkdir(parents=True)
113
+ (root / "apps" / "web" / "src" / "index.mjs").write_text(
114
+ "export const web = true;\n", encoding="utf-8"
115
+ )
116
+ subprocess.run(["git", "add", "apps"], cwd=root, check=True)
117
+
118
+ result = DRIFT_GUARD.evaluate_census(root)
119
+
120
+ self.assertEqual((result["state"], result["block_handoff"]), ("refresh_required", True))
121
+ self.assertEqual(result["overrides"], [
122
+ {"scope": "this change", "reason": "generated path alias"}
123
+ ])
124
+
125
+ def test_override_requires_a_non_empty_text_reason(self):
126
+ temporary, root = self.make_repo()
127
+ self.addCleanup(temporary.cleanup)
128
+ self.enable(root)
129
+ self.activate_current(root)
130
+ (root / "test").mkdir()
131
+ (root / "test" / "proof.test.mjs").write_text("// evidence\n", encoding="utf-8")
132
+ subprocess.run(["git", "add", "test"], cwd=root, check=True)
133
+ baseline = DRIFT_GUARD.evaluate_census(root)
134
+ self.assertTrue(baseline["mechanical_false_positive"])
135
+
136
+ profile = root / ".census" / "profile.json"
137
+ body = json.loads(profile.read_text(encoding="utf-8"))
138
+ for invalid_reason in (True, "", " "):
139
+ with self.subTest(reason=invalid_reason):
140
+ body["overrides"] = [{
141
+ "scope": "this change",
142
+ "reason": invalid_reason,
143
+ "topologyFingerprint": baseline["change_binding"],
144
+ }]
145
+ profile.write_text(json.dumps(body) + "\n", encoding="utf-8")
146
+ result = DRIFT_GUARD.evaluate_census(root)
147
+ self.assertFalse(result["override_applied"])
148
+ self.assertTrue(result["block_handoff"])
149
+
150
+ def test_profile_and_active_census_files_cannot_follow_foreign_symlinks(self):
151
+ temporary, root = self.make_repo()
152
+ foreign_tmp, foreign = self.make_repo()
153
+ self.addCleanup(temporary.cleanup)
154
+ self.addCleanup(foreign_tmp.cleanup)
155
+ census = root / ".census"
156
+ census.mkdir()
157
+ foreign_marker = "FOREIGN-CENSUS-CONTENT-MUST-NOT-LEAK"
158
+ foreign_profile = foreign / "profile.json"
159
+ foreign_profile.write_text(json.dumps({
160
+ "enabled": True,
161
+ "overrides": [{"reason": foreign_marker, "scope": "this change"}],
162
+ }), encoding="utf-8")
163
+ (census / "profile.json").symlink_to(foreign_profile)
164
+ (census / "active.json").write_text("{}\n", encoding="utf-8")
165
+
166
+ profile_result = DRIFT_GUARD.evaluate_census(root)
167
+
168
+ self.assertEqual((profile_result["state"], profile_result["block_handoff"]),
169
+ ("failed", False))
170
+ self.assertEqual(profile_result["overrides"], [])
171
+ self.assertNotIn(foreign_marker, json.dumps(profile_result))
172
+ completed = subprocess.run(
173
+ [sys.executable, str(HOOKS / "drift-guard.py"), "--census-status"],
174
+ cwd=root,
175
+ capture_output=True,
176
+ check=True,
177
+ text=True,
178
+ )
179
+ self.assertNotIn(foreign_marker, completed.stdout + completed.stderr)
180
+
181
+ (census / "profile.json").unlink()
182
+ self.enable(root)
183
+ (census / "active.json").unlink()
184
+ foreign_active = foreign / "active.json"
185
+ foreign_active.write_text(f'{{"secret":"{foreign_marker}"}}\n', encoding="utf-8")
186
+ (census / "active.json").symlink_to(foreign_active)
187
+
188
+ active_result = DRIFT_GUARD.evaluate_census(root)
189
+
190
+ self.assertEqual((active_result["state"], active_result["block_handoff"]),
191
+ ("failed", True))
192
+ self.assertNotIn(foreign_marker, json.dumps(active_result))
193
+ (census / "profile.json").unlink()
194
+ missing_profile = DRIFT_GUARD.evaluate_census(root)
195
+ self.assertEqual((missing_profile["state"], missing_profile["block_handoff"]),
196
+ ("failed", False))
197
+ self.assertNotIn(foreign_marker, json.dumps(missing_profile))
198
+ self.enable(root)
199
+ (root / ".handoff").mkdir()
200
+ payload = {
201
+ "tool_name": "Write",
202
+ "tool_input": {
203
+ "file_path": str(root / ".handoff" / "52.md"),
204
+ "content": "Build [#52](https://github.com/iKon85/agent-workflow-kit/issues/52)",
205
+ },
206
+ }
207
+ with patch.object(DRIFT_GUARD, "run_check", return_value={"deny_recommended": False}):
208
+ blocked, message = DRIFT_GUARD.should_block(payload)
209
+ self.assertTrue(blocked)
210
+ self.assertIn("failed", message)
211
+ self.assertNotIn(foreign_marker, message)
212
+
213
+ def test_disabled_census_control_failures_stay_visible_but_fail_open(self):
214
+ temporary, root = self.make_repo()
215
+ foreign_tmp, foreign = self.make_repo()
216
+ self.addCleanup(temporary.cleanup)
217
+ self.addCleanup(foreign_tmp.cleanup)
218
+ self.enable(root)
219
+ profile_path = root / ".census" / "profile.json"
220
+ profile = json.loads(profile_path.read_text(encoding="utf-8"))
221
+ profile["enabled"] = False
222
+ profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
223
+ active_path = root / ".census" / "active.json"
224
+
225
+ active_path.write_text("{not-json\n", encoding="utf-8")
226
+ corrupt = DRIFT_GUARD.evaluate_census(root)
227
+ self.assertEqual((corrupt["state"], corrupt["block_handoff"]),
228
+ ("failed", False))
229
+ self.assertEqual(corrupt["detail"], "census scan or active snapshot is invalid")
230
+
231
+ active_path.unlink()
232
+ foreign_active = foreign / "active.json"
233
+ foreign_active.write_text('{"secret":"must-not-leak"}\n', encoding="utf-8")
234
+ active_path.symlink_to(foreign_active)
235
+ unreadable = DRIFT_GUARD.evaluate_census(root)
236
+ self.assertEqual((unreadable["state"], unreadable["block_handoff"]),
237
+ ("failed", False))
238
+ self.assertNotIn("must-not-leak", json.dumps(unreadable))
239
+
240
+ def test_local_scanner_proof_is_required_for_current(self):
241
+ temporary, root = self.make_repo()
242
+ foreign_tmp, foreign = self.make_repo()
243
+ self.addCleanup(temporary.cleanup)
244
+ self.addCleanup(foreign_tmp.cleanup)
245
+ module = root / "scanner.mjs"
246
+ test = root / "scanner.test.mjs"
247
+ module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
248
+ test.write_text("import { test } from 'node:test'; test('proof', () => {});\n",
249
+ encoding="utf-8")
250
+ subprocess.run(["git", "add", "scanner.mjs", "scanner.test.mjs"], cwd=root, check=True)
251
+ scanner = {
252
+ "surface": "src", "module": "scanner.mjs",
253
+ "export": "scanLocal", "test": "scanner.test.mjs",
254
+ }
255
+ self.enable(root, local_scanners=[scanner])
256
+ self.activate_current(root)
257
+ self.assertEqual(DRIFT_GUARD.evaluate_census(root)["state"], "current")
258
+
259
+ profile_path = root / ".census" / "profile.json"
260
+ profile = json.loads(profile_path.read_text(encoding="utf-8"))
261
+ foreign_module = foreign / "foreign.mjs"
262
+ foreign_module.write_text("export function scanLocal() { return ['src']; }\n",
263
+ encoding="utf-8")
264
+ foreign_test = foreign / "foreign.test.mjs"
265
+ foreign_test.write_text(
266
+ "import { test } from 'node:test'; test('foreign', () => {});\n",
267
+ encoding="utf-8",
268
+ )
269
+ invalid_records = [
270
+ {**scanner, "module": "missing.mjs"},
271
+ {**scanner, "module": "../escape.mjs"},
272
+ {**scanner, "export": "missingExport"},
273
+ {**scanner, "test": "missing.test.mjs"},
274
+ {**scanner, "test": "../escape.test.mjs"},
275
+ ]
276
+ (root / "scanner-link.mjs").symlink_to(foreign_module)
277
+ invalid_records.append({**scanner, "module": "scanner-link.mjs"})
278
+ (root / "foreign-dir-link").symlink_to(foreign, target_is_directory=True)
279
+ invalid_records.append({**scanner, "module": "foreign-dir-link/foreign.mjs"})
280
+ (root / "scanner-link.test.mjs").symlink_to(foreign_test)
281
+ invalid_records.append({**scanner, "test": "scanner-link.test.mjs"})
282
+ module.write_text("export function scanLocal() { return ['another-surface']; }\n",
283
+ encoding="utf-8")
284
+ invalid_records.append(scanner)
285
+
286
+ for index, record in enumerate(invalid_records):
287
+ with self.subTest(case=index, record=record):
288
+ if index == len(invalid_records) - 1:
289
+ module.write_text(
290
+ "export function scanLocal() { return ['another-surface']; }\n",
291
+ encoding="utf-8",
292
+ )
293
+ else:
294
+ module.write_text(
295
+ "export function scanLocal() { return ['src']; }\n", encoding="utf-8"
296
+ )
297
+ profile["localScanners"] = [record]
298
+ profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
299
+ result = DRIFT_GUARD.evaluate_census(root)
300
+ self.assertEqual((result["state"], result["block_handoff"]),
301
+ ("refresh_required", True))
302
+ self.assertIn("proof:src", result["reasons"])
303
+
304
+ module.chmod(0)
305
+ try:
306
+ profile["localScanners"] = [scanner]
307
+ profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
308
+ unreadable = DRIFT_GUARD.evaluate_census(root)
309
+ self.assertEqual((unreadable["state"], unreadable["block_handoff"]),
310
+ ("refresh_required", True))
311
+ self.assertIn("proof:src", unreadable["reasons"])
312
+ finally:
313
+ module.chmod(0o644)
314
+
315
+ module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
316
+ test.write_text("import { test } from 'node:test'; test('proof', () => { throw new Error('no'); });\n",
317
+ encoding="utf-8")
318
+ profile["localScanners"] = [scanner]
319
+ profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
320
+ failed_test = DRIFT_GUARD.evaluate_census(root)
321
+ self.assertEqual((failed_test["state"], failed_test["block_handoff"]),
322
+ ("refresh_required", True))
323
+ self.assertIn("proof:src", failed_test["reasons"])
324
+
325
+ def test_activated_census_blocks_when_local_proof_times_out(self):
326
+ temporary, root = self.make_repo()
327
+ self.addCleanup(temporary.cleanup)
328
+ module = root / "scanner.mjs"
329
+ test = root / "scanner.test.mjs"
330
+ module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
331
+ test.write_text(
332
+ "import { test } from 'node:test'; "
333
+ "test('hang', async () => new Promise(resolve => setTimeout(resolve, 250)));\n",
334
+ encoding="utf-8",
335
+ )
336
+ subprocess.run(["git", "add", "scanner.mjs", "scanner.test.mjs"], cwd=root, check=True)
337
+ scanner = {
338
+ "surface": "src", "module": "scanner.mjs",
339
+ "export": "scanLocal", "test": "scanner.test.mjs",
340
+ }
341
+ self.enable(root, local_scanners=[scanner])
342
+ self.activate_current(root)
343
+
344
+ result = DRIFT_GUARD.evaluate_census(root, proof_timeout_ms=25)
345
+
346
+ self.assertEqual((result["state"], result["block_handoff"]),
347
+ ("refresh_required", True))
348
+ self.assertIn("proof:src", result["reasons"])
349
+
350
+ test.write_text("import { test } from 'node:test'; test('proof', () => {});\n",
351
+ encoding="utf-8")
352
+ module.write_text(
353
+ "export async function scanLocal() { return new Promise(() => {}); }\n",
354
+ encoding="utf-8",
355
+ )
356
+ scanner_timeout = DRIFT_GUARD.evaluate_census(root, proof_timeout_ms=25)
357
+ self.assertEqual((scanner_timeout["state"], scanner_timeout["block_handoff"]),
358
+ ("refresh_required", True))
359
+ self.assertIn("proof:src", scanner_timeout["reasons"])
360
+
361
+ def test_activated_census_fails_closed_when_bridge_is_unavailable(self):
362
+ temporary, root = self.make_repo()
363
+ self.addCleanup(temporary.cleanup)
364
+ self.enable(root)
365
+ self.activate_current(root)
366
+
367
+ with patch.object(DRIFT_GUARD, "scan_census_status",
368
+ side_effect=subprocess.TimeoutExpired("node", 1)):
369
+ activated = DRIFT_GUARD.evaluate_census(root)
370
+ self.assertEqual((activated["state"], activated["block_handoff"]),
371
+ ("offline", True))
372
+
373
+ (root / ".census" / "active.json").unlink()
374
+ with patch.object(DRIFT_GUARD, "scan_census_status",
375
+ side_effect=subprocess.TimeoutExpired("node", 1)):
376
+ bootstrap = DRIFT_GUARD.evaluate_census(root)
377
+ self.assertEqual((bootstrap["state"], bootstrap["block_handoff"]),
378
+ ("offline", False))
379
+
380
+ self.activate_current(root)
381
+ profile_path = root / ".census" / "profile.json"
382
+ profile = json.loads(profile_path.read_text(encoding="utf-8"))
383
+ profile["enabled"] = False
384
+ profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
385
+ with patch.object(DRIFT_GUARD, "scan_census_status",
386
+ side_effect=subprocess.TimeoutExpired("node", 1)):
387
+ disabled = DRIFT_GUARD.evaluate_census(root)
388
+ self.assertEqual((disabled["state"], disabled["block_handoff"]),
389
+ ("offline", False))
390
+
391
+ def test_active_local_scanner_history_is_authoritative(self):
392
+ temporary, root = self.make_repo()
393
+ self.addCleanup(temporary.cleanup)
394
+ module = root / "scanner.mjs"
395
+ test = root / "scanner.test.mjs"
396
+ module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
397
+ test.write_text("import { test } from 'node:test'; test('proof', () => {});\n",
398
+ encoding="utf-8")
399
+ subprocess.run(["git", "add", "scanner.mjs", "scanner.test.mjs"], cwd=root, check=True)
400
+ scanner = {
401
+ "surface": "src", "module": "scanner.mjs",
402
+ "export": "scanLocal", "test": "scanner.test.mjs",
403
+ }
404
+ self.enable(root, local_scanners=[scanner])
405
+ self.activate_current(root)
406
+ profile_path = root / ".census" / "profile.json"
407
+ active_path = root / ".census" / "active.json"
408
+
409
+ profile = json.loads(profile_path.read_text(encoding="utf-8"))
410
+ for replacement in (None, "corrupt", [], [{**scanner, "export": "other"}]):
411
+ with self.subTest(current=replacement):
412
+ changed = dict(profile)
413
+ if replacement is None:
414
+ changed.pop("localScanners")
415
+ else:
416
+ changed["localScanners"] = replacement
417
+ profile_path.write_text(json.dumps(changed) + "\n", encoding="utf-8")
418
+ result = DRIFT_GUARD.evaluate_census(root)
419
+ self.assertEqual((result["state"], result["block_handoff"]),
420
+ ("refresh_required", True))
421
+ self.assertTrue(any(reason.startswith("proof:") for reason in result["reasons"]))
422
+
423
+ profile_path.write_text(json.dumps(profile) + "\n", encoding="utf-8")
424
+ for replacement in (None, "corrupt", [], [{**scanner, "test": "other.test.mjs"}]):
425
+ with self.subTest(active=replacement):
426
+ active = json.loads(active_path.read_text(encoding="utf-8"))
427
+ report = active.setdefault("profileReport", {})
428
+ if replacement is None:
429
+ report.pop("localScanners", None)
430
+ else:
431
+ report["localScanners"] = replacement
432
+ active_path.write_text(json.dumps(active) + "\n", encoding="utf-8")
433
+ result = DRIFT_GUARD.evaluate_census(root)
434
+ self.assertEqual((result["state"], result["block_handoff"]),
435
+ ("refresh_required", True))
436
+ self.assertTrue(any(reason.startswith("proof:") for reason in result["reasons"]))
437
+ self.activate_current(root)
438
+
439
+ def test_local_scanner_export_requires_an_array_of_exact_surface_strings(self):
440
+ temporary, root = self.make_repo()
441
+ self.addCleanup(temporary.cleanup)
442
+ module = root / "scanner.mjs"
443
+ test = root / "scanner.test.mjs"
444
+ test.write_text("import { test } from 'node:test'; test('proof', () => {});\n",
445
+ encoding="utf-8")
446
+ scanner = {
447
+ "surface": "src", "module": "scanner.mjs",
448
+ "export": "scanLocal", "test": "scanner.test.mjs",
449
+ }
450
+ self.enable(root, local_scanners=[scanner])
451
+ invalid_results = (
452
+ "'src'",
453
+ "({ includes: () => true })",
454
+ "['src', 42]",
455
+ "['SRC']",
456
+ )
457
+ for expression in invalid_results:
458
+ with self.subTest(expression=expression):
459
+ module.write_text(
460
+ f"export function scanLocal() {{ return {expression}; }}\n", encoding="utf-8"
461
+ )
462
+ subprocess.run(["git", "add", "scanner.mjs", "scanner.test.mjs"],
463
+ cwd=root, check=True)
464
+ self.activate_current(root)
465
+ result = DRIFT_GUARD.evaluate_census(root)
466
+ self.assertEqual((result["state"], result["block_handoff"]),
467
+ ("refresh_required", True))
468
+ self.assertIn("proof:src", result["reasons"])
469
+
470
+ module.write_text("export function scanLocal() { return ['src']; }\n", encoding="utf-8")
471
+ subprocess.run(["git", "add", "scanner.mjs"], cwd=root, check=True)
472
+ self.activate_current(root)
473
+ self.assertEqual(DRIFT_GUARD.evaluate_census(root)["state"], "current")
474
+
475
+ def test_justified_change_local_override_bypasses_only_evidence_topology_noise(self):
476
+ temporary, root = self.make_repo()
477
+ self.addCleanup(temporary.cleanup)
478
+ self.enable(root)
479
+ self.activate_current(root)
480
+ (root / "test").mkdir()
481
+ (root / "test" / "new-proof.test.mjs").write_text(
482
+ "// evidence only\n", encoding="utf-8"
483
+ )
484
+ subprocess.run(["git", "add", "test"], cwd=root, check=True)
485
+
486
+ unbound = DRIFT_GUARD.evaluate_census(root)
487
+ self.assertTrue(unbound["mechanical_false_positive"])
488
+ self.assertFalse(unbound["override_applied"])
489
+ profile = root / ".census" / "profile.json"
490
+ body = json.loads(profile.read_text(encoding="utf-8"))
491
+ body["overrides"] = [{
492
+ "scope": "this change",
493
+ "reason": "test-only evidence file",
494
+ "topologyFingerprint": unbound["change_binding"],
495
+ }]
496
+ profile.write_text(json.dumps(body) + "\n", encoding="utf-8")
497
+
498
+ result = DRIFT_GUARD.evaluate_census(root)
499
+
500
+ self.assertEqual(result["state"], "refresh_required")
501
+ self.assertEqual(result["reasons"], ["topology"])
502
+ self.assertTrue(result["override_applied"])
503
+ self.assertFalse(result["block_handoff"])
504
+
505
+ (root / "test" / "later-proof.test.mjs").write_text(
506
+ "// later evidence\n", encoding="utf-8"
507
+ )
508
+ subprocess.run(["git", "add", "test"], cwd=root, check=True)
509
+ stale = DRIFT_GUARD.evaluate_census(root)
510
+ self.assertNotEqual(stale["change_binding"], result["change_binding"])
511
+ self.assertFalse(stale["override_applied"])
512
+ self.assertTrue(stale["block_handoff"])
513
+
514
+ def test_handoff_payload_uses_target_repo_when_cwd_is_elsewhere(self):
515
+ target_tmp, target = self.make_repo()
516
+ cwd_tmp, cwd_repo = self.make_repo()
517
+ self.addCleanup(target_tmp.cleanup)
518
+ self.addCleanup(cwd_tmp.cleanup)
519
+ self.enable(target)
520
+ self.activate_current(target)
521
+ (target / "test").mkdir()
522
+ (target / "test" / "proof.test.mjs").write_text("// evidence\n", encoding="utf-8")
523
+ subprocess.run(["git", "add", "test"], cwd=target, check=True)
524
+ (target / ".handoff").mkdir()
525
+ payload = {
526
+ "tool_name": "Write",
527
+ "tool_input": {
528
+ "file_path": str(target / ".handoff" / "52.md"),
529
+ "content": "Build [#52](https://github.com/iKon85/agent-workflow-kit/issues/52)",
530
+ },
531
+ }
532
+
533
+ previous = Path.cwd()
534
+ try:
535
+ os.chdir(cwd_repo / "src")
536
+ with patch.object(DRIFT_GUARD, "run_check", return_value={"deny_recommended": False}):
537
+ blocked, message = DRIFT_GUARD.should_block(payload)
538
+ finally:
539
+ os.chdir(previous)
540
+
541
+ self.assertTrue(blocked)
542
+ self.assertIn("CENSUS", message)
543
+
544
+ def test_handoff_payload_rejects_symlink_escape_from_claimed_repo(self):
545
+ target_tmp, target = self.make_repo()
546
+ foreign_tmp, foreign = self.make_repo()
547
+ self.addCleanup(target_tmp.cleanup)
548
+ self.addCleanup(foreign_tmp.cleanup)
549
+ self.enable(foreign)
550
+ (foreign / "handoffs").mkdir()
551
+ (target / ".handoff").symlink_to(foreign / "handoffs", target_is_directory=True)
552
+ payload = {
553
+ "tool_name": "Write",
554
+ "tool_input": {
555
+ "file_path": str(target / ".handoff" / "52.md"),
556
+ "content": "Build [#52](https://github.com/iKon85/agent-workflow-kit/issues/52)",
557
+ },
558
+ }
559
+
560
+ with patch.object(DRIFT_GUARD, "run_check", return_value={"deny_recommended": False}):
561
+ blocked, message = DRIFT_GUARD.should_block(payload)
562
+
563
+ self.assertTrue(blocked)
564
+ self.assertIn("target repository", message)
565
+
566
+ def test_cli_root_resolution_finds_git_root_from_subdirectory(self):
567
+ temporary, root = self.make_repo()
568
+ self.addCleanup(temporary.cleanup)
569
+ self.enable(root)
570
+
571
+ completed = subprocess.run(
572
+ [sys.executable, str(HOOKS / "drift-guard.py"), "--census-status"],
573
+ cwd=root / "src",
574
+ capture_output=True,
575
+ check=True,
576
+ text=True,
577
+ )
578
+ status = json.loads(completed.stdout)
579
+
580
+ self.assertEqual(status["state"], "bootstrap")
581
+
582
+ def test_activated_refresh_blocks_build_handoff_but_not_normal_work(self):
583
+ payload = {
584
+ "tool_name": "Write",
585
+ "tool_input": {
586
+ "file_path": "/tmp/repo/.handoff/52.md",
587
+ "content": "Build [#52](https://github.com/iKon85/agent-workflow-kit/issues/52)",
588
+ },
589
+ }
590
+ refresh = {
591
+ "state": "refresh_required",
592
+ "block_handoff": True,
593
+ "reasons": ["topology"],
594
+ "overrides": [],
595
+ }
596
+ with patch.object(DRIFT_GUARD, "run_check", return_value={"deny_recommended": False}), \
597
+ patch.object(DRIFT_GUARD, "evaluate_census", return_value=refresh):
598
+ blocked, message = DRIFT_GUARD.should_block(payload)
599
+ self.assertTrue(blocked)
600
+ self.assertIn("CENSUS", message)
601
+
602
+ normal_payload = {"tool_name": "Write", "tool_input": {"file_path": "/tmp/repo/notes.md"}}
603
+ with patch.object(DRIFT_GUARD, "evaluate_census") as census:
604
+ self.assertEqual(DRIFT_GUARD.should_block(normal_payload), (False, ""))
605
+ census.assert_not_called()
606
+
607
+ def test_cross_cutting_prd_and_kit_update_prose_use_the_same_backstop_contract(self):
608
+ source_prd = (ROOT / ".claude/skills/to-prd/SKILL.md").read_text(encoding="utf-8")
609
+ mirror_prd = (ROOT / ".agents/skills/to-prd/SKILL.md").read_text(encoding="utf-8")
610
+ source_update = (ROOT / ".claude/skills/kit-update/SKILL.md").read_text(encoding="utf-8")
611
+ mirror_update = (ROOT / ".agents/skills/kit-update/SKILL.md").read_text(encoding="utf-8")
612
+
613
+ for prose in (source_prd, mirror_prd):
614
+ self.assertIn("--census-status", prose)
615
+ self.assertRegex(prose, r"cross-cutting[\s\S]*refresh_required[\s\S]*must not be locked")
616
+ self.assertRegex(prose, r"disabled[\s\S]*no_census[\s\S]*manual walk")
617
+ self.assertRegex(prose, r"change-local\s+override[\s\S]*mechanical\s+false positive")
618
+ self.assertIn("topologyFingerprint", prose)
619
+ for prose in (source_update, mirror_update):
620
+ self.assertIn("--census-status", prose)
621
+ self.assertRegex(prose, r"newer census builder[\s\S]*census-update")
622
+ self.assertRegex(prose, r"never overwrite[\s\S]*consumer-owned census")
623
+
624
+
625
+ if __name__ == "__main__":
626
+ unittest.main()