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.
- agentbundle/__init__.py +14 -0
- agentbundle/__main__.py +5 -0
- agentbundle/_data/adapter.schema.json +270 -0
- agentbundle/_data/adapter.toml +584 -0
- agentbundle/_data/install-marker.py +1099 -0
- agentbundle/_data/pack.schema.json +152 -0
- agentbundle/_data/plugin-manifest.derived.schema.json +33 -0
- agentbundle/_data/plugin-manifest.schema.json +18 -0
- agentbundle/build/__init__.py +206 -0
- agentbundle/build/__main__.py +8 -0
- agentbundle/build/adapter_root_bins.py +336 -0
- agentbundle/build/adapters/__init__.py +46 -0
- agentbundle/build/adapters/claude_code.py +142 -0
- agentbundle/build/adapters/codex.py +227 -0
- agentbundle/build/adapters/copilot.py +149 -0
- agentbundle/build/adapters/kiro.py +608 -0
- agentbundle/build/adapters/kiro_cli.py +53 -0
- agentbundle/build/adapters/kiro_ide.py +275 -0
- agentbundle/build/contract.py +20 -0
- agentbundle/build/lint_packs.py +555 -0
- agentbundle/build/main.py +596 -0
- agentbundle/build/phase_order.py +40 -0
- agentbundle/build/projections/__init__.py +13 -0
- agentbundle/build/projections/codex_agent_toml.py +232 -0
- agentbundle/build/projections/copilot_agent_md.py +206 -0
- agentbundle/build/projections/copilot_hooks_json.py +142 -0
- agentbundle/build/projections/direct_directory.py +41 -0
- agentbundle/build/projections/hook_id.py +27 -0
- agentbundle/build/projections/kiro_ide_hook.py +256 -0
- agentbundle/build/projections/merge_into_agent_json.py +264 -0
- agentbundle/build/projections/merge_json.py +58 -0
- agentbundle/build/projections/user_merge_json.py +324 -0
- agentbundle/build/scope_rails.py +728 -0
- agentbundle/build/self_host.py +1486 -0
- agentbundle/build/shared_libs.py +309 -0
- agentbundle/build/target_resolver.py +85 -0
- agentbundle/build/tests/__init__.py +0 -0
- agentbundle/build/tests/test_adapter_claude_code.py +275 -0
- agentbundle/build/tests/test_adapter_codex.py +699 -0
- agentbundle/build/tests/test_adapter_copilot.py +91 -0
- agentbundle/build/tests/test_adapter_kiro.py +449 -0
- agentbundle/build/tests/test_adapter_kiro_alias.py +105 -0
- agentbundle/build/tests/test_adapter_kiro_cli.py +102 -0
- agentbundle/build/tests/test_adapter_kiro_ide.py +173 -0
- agentbundle/build/tests/test_adapter_root_bins_projection.py +429 -0
- agentbundle/build/tests/test_build_ships_seeds.py +78 -0
- agentbundle/build/tests/test_contract.py +582 -0
- agentbundle/build/tests/test_contract_scope.py +224 -0
- agentbundle/build/tests/test_contract_v07.py +191 -0
- agentbundle/build/tests/test_contract_v08.py +230 -0
- agentbundle/build/tests/test_direct_directory_cleanup.py +65 -0
- agentbundle/build/tests/test_end_to_end_build.py +227 -0
- agentbundle/build/tests/test_lint_agents_md_legacy_block.py +135 -0
- agentbundle/build/tests/test_lint_agents_md_risk_block.py +116 -0
- agentbundle/build/tests/test_lint_packs.py +703 -0
- agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +176 -0
- agentbundle/build/tests/test_pack_schema.py +265 -0
- agentbundle/build/tests/test_pack_schema_allowed_adapters.py +258 -0
- agentbundle/build/tests/test_pack_schema_install.py +305 -0
- agentbundle/build/tests/test_pipeline.py +272 -0
- agentbundle/build/tests/test_plugin_manifest_schema.py +327 -0
- agentbundle/build/tests/test_projections_merge_json.py +148 -0
- agentbundle/build/tests/test_scope_rails.py +398 -0
- agentbundle/build/tests/test_security.py +97 -0
- agentbundle/build/tests/test_self_host_check.py +2100 -0
- agentbundle/build/tests/test_shared_libs_projection.py +415 -0
- agentbundle/build/tests/test_shipped_packs_v07_declarations.py +100 -0
- agentbundle/build/tests/test_shipped_packs_v08_declarations.py +80 -0
- agentbundle/build/tests/test_validate.py +250 -0
- agentbundle/build/validate.py +141 -0
- agentbundle/catalogue.py +164 -0
- agentbundle/cli.py +486 -0
- agentbundle/commands/__init__.py +5 -0
- agentbundle/commands/_common.py +174 -0
- agentbundle/commands/_drop_warning.py +329 -0
- agentbundle/commands/adapt.py +343 -0
- agentbundle/commands/config.py +125 -0
- agentbundle/commands/diff.py +211 -0
- agentbundle/commands/init_state.py +279 -0
- agentbundle/commands/install.py +3026 -0
- agentbundle/commands/list_packs.py +170 -0
- agentbundle/commands/list_targets.py +23 -0
- agentbundle/commands/reconcile.py +161 -0
- agentbundle/commands/render.py +165 -0
- agentbundle/commands/scaffold.py +69 -0
- agentbundle/commands/uninstall.py +294 -0
- agentbundle/commands/upgrade.py +699 -0
- agentbundle/commands/validate.py +688 -0
- agentbundle/config.py +747 -0
- agentbundle/render.py +123 -0
- agentbundle/safety.py +633 -0
- agentbundle/scope.py +319 -0
- agentbundle/user_config.py +284 -0
- agentbundle/version.py +49 -0
- agentbundle-0.2.0.dist-info/METADATA +37 -0
- agentbundle-0.2.0.dist-info/RECORD +99 -0
- agentbundle-0.2.0.dist-info/WHEEL +5 -0
- agentbundle-0.2.0.dist-info/entry_points.txt +2 -0
- 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()
|