agentbundle 0.2.0__py3-none-any.whl

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.
Files changed (99) hide show
  1. agentbundle/__init__.py +14 -0
  2. agentbundle/__main__.py +5 -0
  3. agentbundle/_data/adapter.schema.json +270 -0
  4. agentbundle/_data/adapter.toml +584 -0
  5. agentbundle/_data/install-marker.py +1099 -0
  6. agentbundle/_data/pack.schema.json +152 -0
  7. agentbundle/_data/plugin-manifest.derived.schema.json +33 -0
  8. agentbundle/_data/plugin-manifest.schema.json +18 -0
  9. agentbundle/build/__init__.py +206 -0
  10. agentbundle/build/__main__.py +8 -0
  11. agentbundle/build/adapter_root_bins.py +336 -0
  12. agentbundle/build/adapters/__init__.py +46 -0
  13. agentbundle/build/adapters/claude_code.py +142 -0
  14. agentbundle/build/adapters/codex.py +227 -0
  15. agentbundle/build/adapters/copilot.py +149 -0
  16. agentbundle/build/adapters/kiro.py +608 -0
  17. agentbundle/build/adapters/kiro_cli.py +53 -0
  18. agentbundle/build/adapters/kiro_ide.py +275 -0
  19. agentbundle/build/contract.py +20 -0
  20. agentbundle/build/lint_packs.py +555 -0
  21. agentbundle/build/main.py +596 -0
  22. agentbundle/build/phase_order.py +40 -0
  23. agentbundle/build/projections/__init__.py +13 -0
  24. agentbundle/build/projections/codex_agent_toml.py +232 -0
  25. agentbundle/build/projections/copilot_agent_md.py +206 -0
  26. agentbundle/build/projections/copilot_hooks_json.py +142 -0
  27. agentbundle/build/projections/direct_directory.py +41 -0
  28. agentbundle/build/projections/hook_id.py +27 -0
  29. agentbundle/build/projections/kiro_ide_hook.py +256 -0
  30. agentbundle/build/projections/merge_into_agent_json.py +264 -0
  31. agentbundle/build/projections/merge_json.py +58 -0
  32. agentbundle/build/projections/user_merge_json.py +324 -0
  33. agentbundle/build/scope_rails.py +728 -0
  34. agentbundle/build/self_host.py +1486 -0
  35. agentbundle/build/shared_libs.py +309 -0
  36. agentbundle/build/target_resolver.py +85 -0
  37. agentbundle/build/tests/__init__.py +0 -0
  38. agentbundle/build/tests/test_adapter_claude_code.py +275 -0
  39. agentbundle/build/tests/test_adapter_codex.py +699 -0
  40. agentbundle/build/tests/test_adapter_copilot.py +91 -0
  41. agentbundle/build/tests/test_adapter_kiro.py +449 -0
  42. agentbundle/build/tests/test_adapter_kiro_alias.py +105 -0
  43. agentbundle/build/tests/test_adapter_kiro_cli.py +102 -0
  44. agentbundle/build/tests/test_adapter_kiro_ide.py +173 -0
  45. agentbundle/build/tests/test_adapter_root_bins_projection.py +429 -0
  46. agentbundle/build/tests/test_build_ships_seeds.py +78 -0
  47. agentbundle/build/tests/test_contract.py +582 -0
  48. agentbundle/build/tests/test_contract_scope.py +224 -0
  49. agentbundle/build/tests/test_contract_v07.py +191 -0
  50. agentbundle/build/tests/test_contract_v08.py +230 -0
  51. agentbundle/build/tests/test_direct_directory_cleanup.py +65 -0
  52. agentbundle/build/tests/test_end_to_end_build.py +227 -0
  53. agentbundle/build/tests/test_lint_agents_md_legacy_block.py +135 -0
  54. agentbundle/build/tests/test_lint_agents_md_risk_block.py +116 -0
  55. agentbundle/build/tests/test_lint_packs.py +703 -0
  56. agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +176 -0
  57. agentbundle/build/tests/test_pack_schema.py +265 -0
  58. agentbundle/build/tests/test_pack_schema_allowed_adapters.py +258 -0
  59. agentbundle/build/tests/test_pack_schema_install.py +305 -0
  60. agentbundle/build/tests/test_pipeline.py +272 -0
  61. agentbundle/build/tests/test_plugin_manifest_schema.py +327 -0
  62. agentbundle/build/tests/test_projections_merge_json.py +148 -0
  63. agentbundle/build/tests/test_scope_rails.py +398 -0
  64. agentbundle/build/tests/test_security.py +97 -0
  65. agentbundle/build/tests/test_self_host_check.py +2100 -0
  66. agentbundle/build/tests/test_shared_libs_projection.py +415 -0
  67. agentbundle/build/tests/test_shipped_packs_v07_declarations.py +100 -0
  68. agentbundle/build/tests/test_shipped_packs_v08_declarations.py +80 -0
  69. agentbundle/build/tests/test_validate.py +250 -0
  70. agentbundle/build/validate.py +141 -0
  71. agentbundle/catalogue.py +164 -0
  72. agentbundle/cli.py +486 -0
  73. agentbundle/commands/__init__.py +5 -0
  74. agentbundle/commands/_common.py +174 -0
  75. agentbundle/commands/_drop_warning.py +329 -0
  76. agentbundle/commands/adapt.py +343 -0
  77. agentbundle/commands/config.py +125 -0
  78. agentbundle/commands/diff.py +211 -0
  79. agentbundle/commands/init_state.py +279 -0
  80. agentbundle/commands/install.py +3026 -0
  81. agentbundle/commands/list_packs.py +170 -0
  82. agentbundle/commands/list_targets.py +23 -0
  83. agentbundle/commands/reconcile.py +161 -0
  84. agentbundle/commands/render.py +165 -0
  85. agentbundle/commands/scaffold.py +69 -0
  86. agentbundle/commands/uninstall.py +294 -0
  87. agentbundle/commands/upgrade.py +699 -0
  88. agentbundle/commands/validate.py +688 -0
  89. agentbundle/config.py +747 -0
  90. agentbundle/render.py +123 -0
  91. agentbundle/safety.py +633 -0
  92. agentbundle/scope.py +319 -0
  93. agentbundle/user_config.py +284 -0
  94. agentbundle/version.py +49 -0
  95. agentbundle-0.2.0.dist-info/METADATA +37 -0
  96. agentbundle-0.2.0.dist-info/RECORD +99 -0
  97. agentbundle-0.2.0.dist-info/WHEEL +5 -0
  98. agentbundle-0.2.0.dist-info/entry_points.txt +2 -0
  99. agentbundle-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,415 @@
1
+ """T4: shared-libs/ build-pipeline primitive projection tests.
2
+
3
+ Covers AC20 (project per skill declaring metadata.auth: creds),
4
+ AC20 trailing clause (scripts/ created if absent), AC21 (inter-pack
5
+ collision), AC23 (drift gate: modified/missing/orphaned), AC25
6
+ (no projection into skills NOT declaring auth: creds).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import shutil
12
+ import tempfile
13
+ import unittest
14
+ from pathlib import Path
15
+
16
+ from agentbundle.build import shared_libs
17
+
18
+
19
+ def _write_pack(
20
+ packs_dir: Path,
21
+ name: str,
22
+ *,
23
+ shared_libs_files: dict[str, str] | None = None,
24
+ skills: dict[str, dict] | None = None,
25
+ ) -> Path:
26
+ """Build a fixture pack tree.
27
+
28
+ skills entries: {skill_name: {"auth": "creds"|"env", "scripts": {basename: text}}}
29
+ """
30
+ pack = packs_dir / name
31
+ pack.mkdir()
32
+ (pack / "pack.toml").write_text(
33
+ f'[pack]\nname = "{name}"\nversion = "0.1.0"\n',
34
+ encoding="utf-8",
35
+ )
36
+ if shared_libs_files:
37
+ sl = pack / ".apm" / "shared-libs"
38
+ sl.mkdir(parents=True)
39
+ for fname, text in shared_libs_files.items():
40
+ (sl / fname).write_text(text, encoding="utf-8")
41
+ if skills:
42
+ skills_dir = pack / ".apm" / "skills"
43
+ skills_dir.mkdir(parents=True)
44
+ for skill_name, opts in skills.items():
45
+ sd = skills_dir / skill_name
46
+ sd.mkdir()
47
+ auth = opts.get("auth")
48
+ if auth is None:
49
+ fm = f"---\nname: {skill_name}\ndescription: x\n---\nBody.\n"
50
+ else:
51
+ fm = (
52
+ f"---\nname: {skill_name}\n"
53
+ f"description: x\n"
54
+ f"metadata:\n"
55
+ f" credentialed: true\n"
56
+ f" auth: {auth}\n"
57
+ f"---\nBody.\n"
58
+ )
59
+ (sd / "SKILL.md").write_text(fm, encoding="utf-8")
60
+ if "scripts" in opts:
61
+ scripts = sd / "scripts"
62
+ scripts.mkdir()
63
+ for basename, text in opts["scripts"].items():
64
+ (scripts / basename).write_text(text, encoding="utf-8")
65
+ return pack
66
+
67
+
68
+ class _FixtureBase(unittest.TestCase):
69
+ def setUp(self) -> None:
70
+ self.tmp = Path(tempfile.mkdtemp())
71
+ self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
72
+ self.packs_dir = self.tmp / "packs"
73
+ self.packs_dir.mkdir()
74
+
75
+
76
+ class ProjectionMechanicsTests(_FixtureBase):
77
+ """AC20 / AC25: project into auth: creds skills; skip auth: env skills."""
78
+
79
+ def test_projects_into_creds_skills_only(self) -> None:
80
+ _write_pack(
81
+ self.packs_dir, "broker",
82
+ shared_libs_files={"credentials_shim.py": "shim-body\n"},
83
+ )
84
+ _write_pack(
85
+ self.packs_dir, "consumer",
86
+ skills={
87
+ "secret-skill": {"auth": "creds"},
88
+ "env-skill": {"auth": "env"},
89
+ },
90
+ )
91
+ shared_libs.apply_projection(self.packs_dir)
92
+ creds_scripts = (
93
+ self.packs_dir / "consumer" / ".apm" / "skills"
94
+ / "secret-skill" / "scripts" / "credentials_shim.py"
95
+ )
96
+ env_scripts = (
97
+ self.packs_dir / "consumer" / ".apm" / "skills"
98
+ / "env-skill" / "scripts" / "credentials_shim.py"
99
+ )
100
+ self.assertTrue(creds_scripts.is_file())
101
+ self.assertEqual(creds_scripts.read_text(encoding="utf-8"), "shim-body\n")
102
+ self.assertFalse(env_scripts.exists())
103
+
104
+ def test_creates_scripts_dir_if_absent(self) -> None:
105
+ """AC20 trailing: receiving skill without scripts/ — the
106
+ projection creates the directory."""
107
+ _write_pack(
108
+ self.packs_dir, "broker",
109
+ shared_libs_files={"credentials_shim.py": "x"},
110
+ )
111
+ _write_pack(
112
+ self.packs_dir, "consumer",
113
+ skills={"s": {"auth": "creds"}},
114
+ )
115
+ consumer_scripts = (
116
+ self.packs_dir / "consumer" / ".apm" / "skills" / "s" / "scripts"
117
+ )
118
+ self.assertFalse(consumer_scripts.exists())
119
+ shared_libs.apply_projection(self.packs_dir)
120
+ self.assertTrue(consumer_scripts.is_dir())
121
+ self.assertTrue((consumer_scripts / "credentials_shim.py").is_file())
122
+
123
+ def test_projects_multiple_files(self) -> None:
124
+ _write_pack(
125
+ self.packs_dir, "broker",
126
+ shared_libs_files={
127
+ "credentials_shim.py": "shim",
128
+ "_keychain_macos.py": "kc",
129
+ "_credman_windows.py": "cw",
130
+ },
131
+ )
132
+ _write_pack(
133
+ self.packs_dir, "consumer",
134
+ skills={"s": {"auth": "creds"}},
135
+ )
136
+ shared_libs.apply_projection(self.packs_dir)
137
+ scripts = (
138
+ self.packs_dir / "consumer" / ".apm" / "skills" / "s" / "scripts"
139
+ )
140
+ for name in ("credentials_shim.py", "_keychain_macos.py", "_credman_windows.py"):
141
+ self.assertTrue((scripts / name).is_file(), name)
142
+
143
+
144
+ class InterPackCollisionTests(_FixtureBase):
145
+ """AC21: two packs shipping the same shared-libs basename is a
146
+ hard error at projection time."""
147
+
148
+ def test_collision_raises_with_both_paths(self) -> None:
149
+ _write_pack(
150
+ self.packs_dir, "broker-a",
151
+ shared_libs_files={"credentials_shim.py": "a"},
152
+ )
153
+ _write_pack(
154
+ self.packs_dir, "broker-b",
155
+ shared_libs_files={"credentials_shim.py": "b"},
156
+ )
157
+ with self.assertRaises(ValueError) as ctx:
158
+ shared_libs.collect_sources(self.packs_dir)
159
+ msg = str(ctx.exception)
160
+ self.assertIn("credentials_shim.py", msg)
161
+ self.assertIn("broker-a", msg)
162
+ self.assertIn("broker-b", msg)
163
+
164
+ def test_check_drift_surfaces_collision(self) -> None:
165
+ _write_pack(
166
+ self.packs_dir, "broker-a",
167
+ shared_libs_files={"credentials_shim.py": "a"},
168
+ )
169
+ _write_pack(
170
+ self.packs_dir, "broker-b",
171
+ shared_libs_files={"credentials_shim.py": "b"},
172
+ )
173
+ drifts = shared_libs.check_drift(self.packs_dir)
174
+ self.assertEqual(len(drifts), 1)
175
+ self.assertIn("collision", drifts[0])
176
+ self.assertIn("credentials_shim.py", drifts[0])
177
+
178
+
179
+ class DriftGateTests(_FixtureBase):
180
+ """AC23: build-check detects three drift outcomes; build-self resolves."""
181
+
182
+ def _setup_baseline(self) -> None:
183
+ _write_pack(
184
+ self.packs_dir, "broker",
185
+ shared_libs_files={"credentials_shim.py": "source-body\n"},
186
+ )
187
+ _write_pack(
188
+ self.packs_dir, "consumer",
189
+ skills={"s": {"auth": "creds"}},
190
+ )
191
+
192
+ def test_clean_tree_no_drift(self) -> None:
193
+ self._setup_baseline()
194
+ shared_libs.apply_projection(self.packs_dir)
195
+ self.assertEqual(shared_libs.check_drift(self.packs_dir), [])
196
+
197
+ def test_modified_drift_detected(self) -> None:
198
+ self._setup_baseline()
199
+ shared_libs.apply_projection(self.packs_dir)
200
+ # Tamper with the projected copy.
201
+ target = (
202
+ self.packs_dir / "consumer" / ".apm" / "skills" / "s"
203
+ / "scripts" / "credentials_shim.py"
204
+ )
205
+ target.write_text("tampered-body\n", encoding="utf-8")
206
+ drifts = shared_libs.check_drift(self.packs_dir)
207
+ self.assertEqual(len(drifts), 1)
208
+ self.assertIn("modified", drifts[0])
209
+ self.assertIn("credentials_shim.py", drifts[0])
210
+ self.assertIn("make build-self", drifts[0])
211
+
212
+ def test_missing_drift_detected(self) -> None:
213
+ self._setup_baseline()
214
+ # No projection applied yet.
215
+ drifts = shared_libs.check_drift(self.packs_dir)
216
+ self.assertEqual(len(drifts), 1)
217
+ self.assertIn("missing", drifts[0])
218
+ self.assertIn("credentials_shim.py", drifts[0])
219
+
220
+ def test_orphan_drift_detected(self) -> None:
221
+ self._setup_baseline()
222
+ shared_libs.apply_projection(self.packs_dir)
223
+ # Strip the consumer's auth: creds declaration so the projected
224
+ # file is now orphaned.
225
+ skill_md = (
226
+ self.packs_dir / "consumer" / ".apm" / "skills" / "s" / "SKILL.md"
227
+ )
228
+ skill_md.write_text(
229
+ "---\nname: s\ndescription: x\n---\nBody.\n",
230
+ encoding="utf-8",
231
+ )
232
+ drifts = shared_libs.check_drift(self.packs_dir)
233
+ self.assertEqual(len(drifts), 1)
234
+ self.assertIn("orphan", drifts[0])
235
+ self.assertIn("credentials_shim.py", drifts[0])
236
+
237
+ def test_build_self_resolves_drift(self) -> None:
238
+ """After any drift outcome, apply_projection produces a clean tree."""
239
+ self._setup_baseline()
240
+ # Mix of outcomes — modified-and-missing across two files.
241
+ _write_pack(
242
+ self.packs_dir, "broker-2",
243
+ shared_libs_files={}, # extend broker tree below
244
+ )
245
+ # broker already has credentials_shim.py; add a second helper.
246
+ (self.packs_dir / "broker" / ".apm" / "shared-libs" / "_helper.py").write_text(
247
+ "helper-source", encoding="utf-8",
248
+ )
249
+ shutil.rmtree(self.packs_dir / "broker-2") # cleanup the unused pack
250
+ # Pre-state: modified + missing across the two basenames.
251
+ scripts = (
252
+ self.packs_dir / "consumer" / ".apm" / "skills" / "s" / "scripts"
253
+ )
254
+ scripts.mkdir()
255
+ (scripts / "credentials_shim.py").write_text("stale", encoding="utf-8")
256
+ # _helper.py is missing entirely.
257
+ pre_drift = shared_libs.check_drift(self.packs_dir)
258
+ self.assertGreaterEqual(len(pre_drift), 2)
259
+ shared_libs.apply_projection(self.packs_dir)
260
+ self.assertEqual(shared_libs.check_drift(self.packs_dir), [])
261
+
262
+
263
+ class OrphanRemovalTests(_FixtureBase):
264
+ """AC23 build-self resolves orphan drift by removing the file."""
265
+
266
+ def test_apply_projection_removes_orphan(self) -> None:
267
+ _write_pack(
268
+ self.packs_dir, "broker",
269
+ shared_libs_files={"credentials_shim.py": "src\n"},
270
+ )
271
+ _write_pack(
272
+ self.packs_dir, "consumer",
273
+ skills={"s": {"auth": "creds"}},
274
+ )
275
+ shared_libs.apply_projection(self.packs_dir)
276
+ target = (
277
+ self.packs_dir / "consumer" / ".apm" / "skills" / "s"
278
+ / "scripts" / "credentials_shim.py"
279
+ )
280
+ self.assertTrue(target.is_file())
281
+ # Strip the consumer's auth: creds declaration.
282
+ skill_md = target.parent.parent / "SKILL.md"
283
+ skill_md.write_text(
284
+ "---\nname: s\ndescription: x\n---\nBody.\n",
285
+ encoding="utf-8",
286
+ )
287
+ shared_libs.apply_projection(self.packs_dir)
288
+ self.assertFalse(
289
+ target.exists(),
290
+ "apply_projection should remove orphans after auth: creds is stripped",
291
+ )
292
+ self.assertEqual(shared_libs.check_drift(self.packs_dir), [])
293
+
294
+ def test_orphan_surfaces_when_sources_dropped(self) -> None:
295
+ """If a future PR drops the shared-libs source pack entirely,
296
+ stale projected copies under consumer skills must still surface
297
+ as drift (not silent)."""
298
+ _write_pack(
299
+ self.packs_dir, "broker",
300
+ shared_libs_files={"credentials_shim.py": "src\n"},
301
+ )
302
+ _write_pack(
303
+ self.packs_dir, "consumer",
304
+ skills={"s": {"auth": "creds"}},
305
+ )
306
+ shared_libs.apply_projection(self.packs_dir)
307
+ # Drop the broker's shared-libs source entirely.
308
+ import shutil as _sh
309
+ _sh.rmtree(self.packs_dir / "broker" / ".apm" / "shared-libs")
310
+ drifts = shared_libs.check_drift(self.packs_dir)
311
+ self.assertTrue(
312
+ any("orphan" in d for d in drifts),
313
+ f"orphan rail silent when sources dropped; got {drifts!r}",
314
+ )
315
+
316
+
317
+ class IdempotenceTests(_FixtureBase):
318
+ """apply_projection is idempotent: running twice produces identical
319
+ filesystem state."""
320
+
321
+ def test_apply_projection_byte_identical_on_second_run(self) -> None:
322
+ _write_pack(
323
+ self.packs_dir, "broker",
324
+ shared_libs_files={
325
+ "credentials_shim.py": "shim-source\n",
326
+ "_keychain_macos.py": "kc-source\n",
327
+ "_credman_windows.py": "cw-source\n",
328
+ },
329
+ )
330
+ _write_pack(
331
+ self.packs_dir, "consumer",
332
+ skills={
333
+ "s1": {"auth": "creds"},
334
+ "s2": {"auth": "creds"},
335
+ },
336
+ )
337
+ shared_libs.apply_projection(self.packs_dir)
338
+ # Capture every projected file's bytes after the first run.
339
+ first_pass: dict[Path, bytes] = {}
340
+ for proj in shared_libs.compute_projections(self.packs_dir):
341
+ first_pass[proj.target] = proj.target.read_bytes()
342
+ # Re-run; assert byte-identical output.
343
+ shared_libs.apply_projection(self.packs_dir)
344
+ for target, expected_bytes in first_pass.items():
345
+ self.assertTrue(target.is_file(), f"target lost on second run: {target}")
346
+ self.assertEqual(
347
+ target.read_bytes(), expected_bytes,
348
+ f"apply_projection produced different bytes on second run for {target}",
349
+ )
350
+ # Drift gate stays clean.
351
+ self.assertEqual(shared_libs.check_drift(self.packs_dir), [])
352
+
353
+
354
+ class EmptyTreeTests(_FixtureBase):
355
+ """No source pack carrying shared-libs → no projection, no drift."""
356
+
357
+ def test_no_sources_no_drift(self) -> None:
358
+ _write_pack(
359
+ self.packs_dir, "consumer",
360
+ skills={"s": {"auth": "creds"}},
361
+ )
362
+ self.assertEqual(shared_libs.check_drift(self.packs_dir), [])
363
+ shared_libs.apply_projection(self.packs_dir) # no-op
364
+ scripts = (
365
+ self.packs_dir / "consumer" / ".apm" / "skills" / "s" / "scripts"
366
+ )
367
+ # No scripts dir created, no files written.
368
+ self.assertFalse(scripts.exists())
369
+
370
+
371
+ class AuthDetectionTests(_FixtureBase):
372
+ """Frontmatter regex correctly admits / refuses the auth value."""
373
+
374
+ def test_auth_creds_with_comment(self) -> None:
375
+ _write_pack(
376
+ self.packs_dir, "broker",
377
+ shared_libs_files={"credentials_shim.py": "x"},
378
+ )
379
+ pack = _write_pack(
380
+ self.packs_dir, "consumer",
381
+ skills={"s": {"auth": "creds"}},
382
+ )
383
+ # Inject a trailing comment after auth: creds.
384
+ skill_md = pack / ".apm" / "skills" / "s" / "SKILL.md"
385
+ text = skill_md.read_text(encoding="utf-8")
386
+ skill_md.write_text(
387
+ text.replace("auth: creds", "auth: creds # broker shape"),
388
+ encoding="utf-8",
389
+ )
390
+ consumers = shared_libs.find_creds_consumers(self.packs_dir)
391
+ self.assertEqual(len(consumers), 1)
392
+
393
+ def test_body_only_match_does_not_count(self) -> None:
394
+ """A body mention (not frontmatter) does not declare auth."""
395
+ _write_pack(
396
+ self.packs_dir, "broker",
397
+ shared_libs_files={"credentials_shim.py": "x"},
398
+ )
399
+ skill_dir = self.packs_dir / "p" / ".apm" / "skills" / "s"
400
+ skill_dir.mkdir(parents=True)
401
+ (self.packs_dir / "p" / "pack.toml").write_text(
402
+ '[pack]\nname = "p"\nversion = "0.1.0"\n',
403
+ encoding="utf-8",
404
+ )
405
+ (skill_dir / "SKILL.md").write_text(
406
+ "---\nname: s\ndescription: x\n---\n"
407
+ "The skill body mentions ` auth: creds` in prose.\n",
408
+ encoding="utf-8",
409
+ )
410
+ consumers = shared_libs.find_creds_consumers(self.packs_dir)
411
+ self.assertEqual(consumers, [])
412
+
413
+
414
+ if __name__ == "__main__":
415
+ unittest.main()
@@ -0,0 +1,100 @@
1
+ """Tests for the eight shipped packs' v0.7 declarations (RFC-0012 /
2
+ repo-scope-per-adapter-projection AC5-AC6).
3
+
4
+ Two cohorts:
5
+
6
+ - Four user-scope-capable packs (`atlassian`, `figma`, `converters`,
7
+ `contracts`) bump `[pack.adapter-contract] version` from 0.6 to
8
+ 0.7. `allowed-adapters` is unchanged from RFC-0011 (`["claude-code",
9
+ "kiro", "codex"]`).
10
+ - Four repo-only packs (`core`, `governance-extras`,
11
+ `user-guide-diataxis`, `monorepo-extras`) bump from 0.2 to 0.7 —
12
+ load-bearing per Drawback #7: without this bump the legacy
13
+ heuristic at step 5 still fires at repo scope for these packs and
14
+ cannot return codex / copilot via the no-flag default. They remain
15
+ implicit-default (no `allowed-adapters` declared).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import tomllib
21
+ import unittest
22
+ from pathlib import Path
23
+
24
+ REPO_ROOT = Path(__file__).resolve().parents[5]
25
+ PACKS_DIR = REPO_ROOT / "packs"
26
+
27
+ USER_SCOPE_PACKS = ("atlassian", "figma", "converters", "contracts")
28
+ REPO_ONLY_PACKS = ("core", "governance-extras", "user-guide-diataxis", "monorepo-extras")
29
+
30
+
31
+ def _load_pack_toml(name: str) -> dict:
32
+ return tomllib.loads((PACKS_DIR / name / "pack.toml").read_text(encoding="utf-8"))
33
+
34
+
35
+ class TestUserScopePacksV07(unittest.TestCase):
36
+ def test_user_scope_packs_bump_to_v07(self) -> None:
37
+ """Test name preserved across bumps; the version assertion now
38
+ pins v0.8 (post docs/specs/dropped-primitives-coverage T7).
39
+ See test_shipped_packs_v08_declarations.py for the load-bearing
40
+ v0.8 pin; this preserves the structural invariant that the
41
+ four user-scope packs all share the same contract version."""
42
+ for name in USER_SCOPE_PACKS:
43
+ with self.subTest(pack=name):
44
+ pack = _load_pack_toml(name)
45
+ self.assertEqual(
46
+ pack["pack"]["adapter-contract"]["version"],
47
+ "0.8",
48
+ f"{name} must bump to v0.8",
49
+ )
50
+
51
+ def test_user_scope_packs_allowed_adapters_unchanged(self) -> None:
52
+ """allowed-adapters is unchanged from RFC-0011."""
53
+ for name in USER_SCOPE_PACKS:
54
+ with self.subTest(pack=name):
55
+ pack = _load_pack_toml(name)
56
+ self.assertEqual(
57
+ pack["pack"]["install"]["allowed-adapters"],
58
+ ["claude-code", "kiro", "codex"],
59
+ f"{name} declared adapter set wrong",
60
+ )
61
+
62
+
63
+ class TestRepoOnlyPacksV07(unittest.TestCase):
64
+ def test_repo_only_packs_bump_to_v07(self) -> None:
65
+ """Drawback #7 mitigation — without the bump the legacy
66
+ heuristic at step 5 fires at repo scope for these packs. Test
67
+ name preserved; version assertion pins v0.8, except `core` which
68
+ docs/specs/copilot-full-parity bumps to v0.10 (its 4 subagents +
69
+ hook-wiring now project to copilot)."""
70
+ expected = {
71
+ "core": "0.10",
72
+ "governance-extras": "0.8",
73
+ "user-guide-diataxis": "0.8",
74
+ "monorepo-extras": "0.8",
75
+ }
76
+ for name in REPO_ONLY_PACKS:
77
+ with self.subTest(pack=name):
78
+ pack = _load_pack_toml(name)
79
+ self.assertEqual(
80
+ pack["pack"]["adapter-contract"]["version"],
81
+ expected[name],
82
+ f"{name} must declare contract v{expected[name]}",
83
+ )
84
+
85
+ def test_repo_only_packs_remain_implicit_default(self) -> None:
86
+ """Repo-only packs declare no allowed-adapters — they project
87
+ to every adapter when targeted via --adapter at repo scope."""
88
+ for name in REPO_ONLY_PACKS:
89
+ with self.subTest(pack=name):
90
+ pack = _load_pack_toml(name)
91
+ install = pack.get("pack", {}).get("install", {})
92
+ self.assertNotIn(
93
+ "allowed-adapters",
94
+ install,
95
+ f"{name} unexpectedly declares allowed-adapters",
96
+ )
97
+
98
+
99
+ if __name__ == "__main__":
100
+ unittest.main()
@@ -0,0 +1,80 @@
1
+ """Tests for T7 of docs/specs/dropped-primitives-coverage — the shipped
2
+ packs declaring ``[pack.adapter-contract] version = "0.8"``.
3
+
4
+ Per `dropped-primitives-coverage` AC12, eight packs bumped to v0.8:
5
+
6
+ - atlassian, figma, converters, contracts (the four credentialed /
7
+ consumer packs).
8
+ - core, governance-extras, user-guide-diataxis, monorepo-extras (the
9
+ four scaffold packs).
10
+
11
+ The `research` pack (shipped later by the `research-pack` spec) also
12
+ declared v0.8 at birth. A future pack landing at v0.8 should add itself to
13
+ ``V08_PACKS`` so this test surfaces the new declaration.
14
+
15
+ Packs in-tree NOT at v0.8:
16
+
17
+ - ``architect``: still at v0.6 (older, pre-RFC-0013).
18
+ - ``credential-brokers``: still at v0.7 (RFC-0013 shipped on v0.7 and
19
+ a v0.7 pack continues to work under v0.8 — the legacy resolver path
20
+ for codex drops agents/hooks per the v0.7 contract, fine for
21
+ backward compat).
22
+ - ``core`` and ``research``: bumped to v0.10 by RFC-0024 /
23
+ docs/specs/copilot-full-parity (copilot now projects their agents +
24
+ hook-wiring), so they leave ``V08_PACKS``.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import tomllib
30
+ import unittest
31
+ from pathlib import Path
32
+
33
+ REPO_ROOT = Path(__file__).resolve().parents[5]
34
+ PACKS_DIR = REPO_ROOT / "packs"
35
+
36
+ V08_PACKS = (
37
+ "atlassian",
38
+ "contracts",
39
+ "converters",
40
+ "figma",
41
+ "governance-extras",
42
+ "monorepo-extras",
43
+ "user-guide-diataxis",
44
+ )
45
+
46
+
47
+ class TestShippedPacksDeclareV08(unittest.TestCase):
48
+ def test_each_named_pack_declares_v08(self) -> None:
49
+ for name in V08_PACKS:
50
+ with self.subTest(pack=name):
51
+ pack_toml = PACKS_DIR / name / "pack.toml"
52
+ self.assertTrue(pack_toml.exists(), f"missing {pack_toml}")
53
+ data = tomllib.loads(pack_toml.read_text(encoding="utf-8"))
54
+ version = data.get("pack", {}).get("adapter-contract", {}).get("version")
55
+ self.assertEqual(
56
+ version,
57
+ "0.8",
58
+ f"pack {name!r} expected adapter-contract.version='0.8', got {version!r}",
59
+ )
60
+
61
+ def test_shipped_v08_packs_match_named_set(self) -> None:
62
+ """The pack.tomls in-tree declaring v0.8 are exactly ``V08_PACKS``.
63
+ A future pack landing at v0.8 should add itself to that tuple
64
+ explicitly so this test surfaces the new declaration."""
65
+ v08_seen: list[str] = []
66
+ for pack_dir in sorted(PACKS_DIR.iterdir()):
67
+ pack_toml = pack_dir / "pack.toml"
68
+ if not pack_toml.exists():
69
+ continue
70
+ data = tomllib.loads(pack_toml.read_text(encoding="utf-8"))
71
+ version = (
72
+ data.get("pack", {}).get("adapter-contract", {}).get("version")
73
+ )
74
+ if version == "0.8":
75
+ v08_seen.append(pack_dir.name)
76
+ self.assertEqual(sorted(v08_seen), sorted(V08_PACKS))
77
+
78
+
79
+ if __name__ == "__main__":
80
+ unittest.main()