@ikon85/agent-workflow-kit 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/grill-me/SKILL.md +3 -1
- package/.agents/skills/grill-with-docs/SKILL.md +3 -1
- package/.agents/skills/kit-update/SKILL.md +9 -11
- package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
- package/.agents/skills/setup-workflow/SKILL.md +20 -18
- package/.agents/skills/setup-workflow/board-sync.md +4 -1
- package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.agents/skills/to-issues/SKILL.md +43 -0
- package/.agents/skills/wrapup/SKILL.md +98 -54
- package/.claude/skills/grill-me/SKILL.md +3 -1
- package/.claude/skills/grill-me-codex/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
- package/.claude/skills/kit-update/SKILL.md +9 -11
- package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
- package/.claude/skills/setup-workflow/SKILL.md +20 -18
- package/.claude/skills/setup-workflow/board-sync.md +4 -1
- package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.claude/skills/to-issues/SKILL.md +43 -0
- package/.claude/skills/wrapup/SKILL.md +98 -54
- package/README.md +249 -7
- package/agent-workflow-kit.package.json +42 -42
- package/package.json +16 -12
- package/scripts/marker_lib.py +3 -0
- package/scripts/profile_globs.py +26 -45
- package/scripts/release-delta-guard.mjs +26 -9
- package/scripts/worktree-lifecycle/README.md +57 -160
- package/scripts/worktree-lifecycle/capabilities.json +2 -3
- package/scripts/worktree-lifecycle/classify.py +480 -0
- package/scripts/worktree-lifecycle/cleanup.py +9 -29
- package/scripts/worktree-lifecycle/core.py +68 -1429
- package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
- package/scripts/worktree-lifecycle/profile.py +46 -29
- package/scripts/worktree-lifecycle/setup.py +7 -19
- package/scripts/wrapup-land.py +1292 -545
- package/src/consumer-migrations.json +1 -16
- package/src/lib/bundle.mjs +31 -1
- package/docs/adr/0001-consumer-divergence-policy.md +0 -53
- package/docs/adr/0002-capability-gated-orchestration.md +0 -70
- package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
- package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
- package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
- package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
- package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
- package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
- package/docs/agents/board-sync.md +0 -74
- package/docs/agents/code-review.md +0 -14
- package/docs/agents/workflow-capabilities.json +0 -28
- package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
- package/docs/research/benchlm-routing-source.md +0 -198
- package/docs/research/consumer-owned-protocol-files.md +0 -238
- package/docs/research/frontend-agent-benchmarks.md +0 -282
- package/docs/research/model-effort-routing-benchmarks.md +0 -261
- package/docs/research/provider-neutral-agent-routing.md +0 -207
- package/docs/research/wave-152-consumer-acceptance.md +0 -98
- package/docs/research/wave-43-script-hook-census.md +0 -128
- package/scripts/build-kit.mjs +0 -117
- package/scripts/build-kit.test.mjs +0 -207
- package/scripts/census/census.test.mjs +0 -252
- package/scripts/census/state.test.mjs +0 -15
- package/scripts/census/transaction.test.mjs +0 -103
- package/scripts/census-contract.test.mjs +0 -227
- package/scripts/check-kit-staleness.mjs +0 -45
- package/scripts/check-kit-staleness.test.mjs +0 -19
- package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
- package/scripts/codex-exec.test.mjs +0 -871
- package/scripts/grill-census-wiring-guard.mjs +0 -87
- package/scripts/grill-census-wiring-guard.test.mjs +0 -107
- package/scripts/kit-release.test.mjs +0 -273
- package/scripts/kit-update-pr.test.mjs +0 -151
- package/scripts/lib/audit-refs.mjs +0 -146
- package/scripts/lib/scrub.mjs +0 -113
- package/scripts/lib/scrub.test.mjs +0 -187
- package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
- package/scripts/portability_profile_scan.py +0 -53
- package/scripts/release-delta-guard.test.mjs +0 -174
- package/scripts/release-parity.test.mjs +0 -90
- package/scripts/release-state.test.mjs +0 -360
- package/scripts/test_anchor_table.py +0 -69
- package/scripts/test_board_bootstrap.py +0 -348
- package/scripts/test_board_sync.py +0 -208
- package/scripts/test_board_sync_create_idempotency.py +0 -117
- package/scripts/test_board_sync_wave_title.py +0 -120
- package/scripts/test_census_backstop.py +0 -712
- package/scripts/test_census_forward_contract.py +0 -91
- package/scripts/test_census_update_contract.test.mjs +0 -454
- package/scripts/test_codex_adapter_sync_contract.py +0 -169
- package/scripts/test_dist_kit_smoke.py +0 -220
- package/scripts/test_drift_guard_diagnostics.py +0 -295
- package/scripts/test_issue_claim_contract.py +0 -169
- package/scripts/test_kit_docs_language_census.py +0 -178
- package/scripts/test_marker_lib.py +0 -162
- package/scripts/test_orchestrate_wave_contract.py +0 -381
- package/scripts/test_pr_body_check.py +0 -264
- package/scripts/test_profile_globs.py +0 -280
- package/scripts/test_program_planning_contract.py +0 -133
- package/scripts/test_release_authorization_contract.py +0 -101
- package/scripts/test_render_anchor.py +0 -267
- package/scripts/test_retro_wrapup_contract.py +0 -153
- package/scripts/test_skill_code_review_seed.py +0 -89
- package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
- package/scripts/test_skill_frontmatter_lint.py +0 -234
- package/scripts/test_skill_gh_lint.py +0 -124
- package/scripts/test_skill_language_census.py +0 -254
- package/scripts/test_skill_optional_readiness.py +0 -171
- package/scripts/test_skill_portability_lint.py +0 -866
- package/scripts/test_skill_precommit_template.py +0 -30
- package/scripts/test_skill_publish_audit.py +0 -358
- package/scripts/test_skill_readiness_contract.py +0 -264
- package/scripts/test_skill_readiness_preflight.py +0 -180
- package/scripts/test_skill_required_readiness.py +0 -233
- package/scripts/test_skill_selfcontainment_lint.py +0 -255
- package/scripts/test_skill_setup_workflow_seeds.py +0 -691
- package/scripts/test_skill_stale_name_lint.py +0 -126
- package/scripts/test_skill_surface_refs.py +0 -300
- package/scripts/test_skill_trailing_artifact_lint.py +0 -71
- package/scripts/test_tdd_contract.py +0 -78
- package/scripts/test_worktree_ignore_seed.py +0 -320
- package/scripts/test_worktree_setup_base_guard.py +0 -140
- package/scripts/test_worktree_wrapup_contract.py +0 -1737
- package/scripts/test_wrapup_land.py +0 -428
- package/scripts/worktree-lifecycle/session.py +0 -1857
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Regression contract for the Codex-only adapter audit skill."""
|
|
3
|
-
|
|
4
|
-
import importlib.util
|
|
5
|
-
import unittest
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
10
|
-
ADAPTER = REPO_ROOT / ".agents/skills/codex-adapter-sync/SKILL.md"
|
|
11
|
-
AGENTS_README = REPO_ROOT / ".codex/agents/README.md"
|
|
12
|
-
_FRONTMATTER_SPEC = importlib.util.spec_from_file_location(
|
|
13
|
-
"skill_frontmatter_lint", REPO_ROOT / "scripts/test_skill_frontmatter_lint.py")
|
|
14
|
-
frontmatter = importlib.util.module_from_spec(_FRONTMATTER_SPEC)
|
|
15
|
-
_FRONTMATTER_SPEC.loader.exec_module(frontmatter)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def section(body: str, heading: str) -> str:
|
|
19
|
-
"""Return one second-level Markdown section, excluding the next one."""
|
|
20
|
-
marker = f"## {heading}\n"
|
|
21
|
-
start = body.index(marker) + len(marker)
|
|
22
|
-
end = body.find("\n## ", start)
|
|
23
|
-
return body[start:] if end == -1 else body[start:end]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class AdapterModesContract(unittest.TestCase):
|
|
27
|
-
@classmethod
|
|
28
|
-
def setUpClass(cls):
|
|
29
|
-
cls.body = ADAPTER.read_text(encoding="utf-8")
|
|
30
|
-
|
|
31
|
-
def test_audit_is_read_only_and_apply_is_worktree_gated(self):
|
|
32
|
-
audit = " ".join(section(self.body, "Audit mode (default)").split())
|
|
33
|
-
apply = " ".join(section(self.body, "Apply mode").split())
|
|
34
|
-
|
|
35
|
-
self.assertIn("current checkout", audit)
|
|
36
|
-
self.assertIn("read-only", audit)
|
|
37
|
-
self.assertIn("Do not create or switch branches or worktrees", audit)
|
|
38
|
-
self.assertIn("before the first edit", apply)
|
|
39
|
-
self.assertIn("Never apply adapter changes on `main`", apply)
|
|
40
|
-
self.assertNotIn("before inventory or edits", self.body)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class AdapterRoutingContract(unittest.TestCase):
|
|
44
|
-
@classmethod
|
|
45
|
-
def setUpClass(cls):
|
|
46
|
-
cls.body = ADAPTER.read_text(encoding="utf-8")
|
|
47
|
-
|
|
48
|
-
def test_durable_routing_is_provider_neutral_and_resolved_at_dispatch(self):
|
|
49
|
-
routing = " ".join(section(self.body, "Routing intent").split())
|
|
50
|
-
|
|
51
|
-
self.assertIn("`routing-intent`", routing)
|
|
52
|
-
self.assertIn("`reasoning-intent`", routing)
|
|
53
|
-
self.assertIn("Evidence catalog", routing)
|
|
54
|
-
self.assertIn("Access graph", routing)
|
|
55
|
-
self.assertIn("Routing policy", routing)
|
|
56
|
-
self.assertIn("dispatch time", routing)
|
|
57
|
-
self.assertIn("explicit `inherit`", routing)
|
|
58
|
-
self.assertIn("Dispatch receipt", routing)
|
|
59
|
-
self.assertIn("`default`, `worker`, and `explorer`", routing)
|
|
60
|
-
|
|
61
|
-
self.assertNotRegex(self.body, r"(?<!model_)\breasoning_effort\b")
|
|
62
|
-
self.assertNotRegex(self.body, r"\bagent_type\b")
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class AdapterInventoryContract(unittest.TestCase):
|
|
66
|
-
@classmethod
|
|
67
|
-
def setUpClass(cls):
|
|
68
|
-
cls.body = ADAPTER.read_text(encoding="utf-8")
|
|
69
|
-
|
|
70
|
-
def test_nested_instructions_configs_and_hooks_are_inventoried(self):
|
|
71
|
-
inventory = section(self.body, "Inventory")
|
|
72
|
-
|
|
73
|
-
for surface in (
|
|
74
|
-
"`**/CLAUDE.md`",
|
|
75
|
-
"`**/AGENTS.md`",
|
|
76
|
-
"`**/AGENTS.override.md`",
|
|
77
|
-
"`**/.codex/config.toml`",
|
|
78
|
-
"`.claude/settings*.json`",
|
|
79
|
-
"`.claude/hooks/**`",
|
|
80
|
-
"`.codex/hooks.json`",
|
|
81
|
-
"inline `[hooks]`",
|
|
82
|
-
):
|
|
83
|
-
self.assertIn(surface, inventory)
|
|
84
|
-
self.assertIn("trusted project layers", inventory)
|
|
85
|
-
self.assertIn("Codex-adapted", inventory)
|
|
86
|
-
self.assertIn("intentionally Claude-only", inventory)
|
|
87
|
-
self.assertNotIn("`frontend/CLAUDE.md`", self.body)
|
|
88
|
-
self.assertNotIn("`backend/CLAUDE.md`", self.body)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class AdapterAgentContract(unittest.TestCase):
|
|
92
|
-
@classmethod
|
|
93
|
-
def setUpClass(cls):
|
|
94
|
-
cls.body = ADAPTER.read_text(encoding="utf-8")
|
|
95
|
-
|
|
96
|
-
def test_custom_agent_toml_uses_the_current_schema(self):
|
|
97
|
-
agents = " ".join(section(self.body, "Custom-agent validation").split())
|
|
98
|
-
|
|
99
|
-
self.assertIn(
|
|
100
|
-
"required `name`, `description`, and `developer_instructions`", agents)
|
|
101
|
-
self.assertIn("optional `model` and `model_reasoning_effort`", agents)
|
|
102
|
-
self.assertIn("Parse every `.codex/agents/*.toml`", agents)
|
|
103
|
-
self.assertIn("Reject a file", agents)
|
|
104
|
-
|
|
105
|
-
def test_codex_host_inventory_is_dated_and_does_not_invent_spawn_selectors(self):
|
|
106
|
-
inventory = " ".join(
|
|
107
|
-
AGENTS_README.read_text(encoding="utf-8").split()
|
|
108
|
-
)
|
|
109
|
-
for fragment in (
|
|
110
|
-
"2026-07-23",
|
|
111
|
-
"`task_name`",
|
|
112
|
-
"`message`",
|
|
113
|
-
"`fork_turns`",
|
|
114
|
-
"no per-spawn `model`",
|
|
115
|
-
"no per-spawn effort selector",
|
|
116
|
-
"`src/lib/routingAdapters/codex.mjs`",
|
|
117
|
-
"blocks differentiated AFK",
|
|
118
|
-
"Dispatch receipt v2",
|
|
119
|
-
):
|
|
120
|
-
self.assertIn(fragment, inventory)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
class AdapterValidationContract(unittest.TestCase):
|
|
124
|
-
@classmethod
|
|
125
|
-
def setUpClass(cls):
|
|
126
|
-
cls.body = ADAPTER.read_text(encoding="utf-8")
|
|
127
|
-
|
|
128
|
-
def test_validation_covers_every_current_adapter_surface(self):
|
|
129
|
-
validation = " ".join(section(self.body, "Validation").split())
|
|
130
|
-
|
|
131
|
-
for proof in (
|
|
132
|
-
"`codex --strict-config --version`",
|
|
133
|
-
"skill-frontmatter guard",
|
|
134
|
-
"skill metadata and loading",
|
|
135
|
-
"custom-agent TOML",
|
|
136
|
-
"`git check-ignore",
|
|
137
|
-
"references, assets, scripts",
|
|
138
|
-
"mirror-parity guard",
|
|
139
|
-
"X of Y",
|
|
140
|
-
):
|
|
141
|
-
self.assertIn(proof, validation)
|
|
142
|
-
for maintainer_only_path in (
|
|
143
|
-
"scripts/test_skill_frontmatter_lint.py",
|
|
144
|
-
"scripts/test_skill_portability_lint.py",
|
|
145
|
-
):
|
|
146
|
-
self.assertNotIn(maintainer_only_path, self.body)
|
|
147
|
-
|
|
148
|
-
def test_description_cap_is_an_enforced_repository_safeguard(self):
|
|
149
|
-
validation = " ".join(section(self.body, "Validation").split())
|
|
150
|
-
self.assertIn("1024-character description cap", validation)
|
|
151
|
-
self.assertIn("repository safeguard", validation)
|
|
152
|
-
self.assertIn("not a Codex product limit", validation)
|
|
153
|
-
|
|
154
|
-
problems = []
|
|
155
|
-
for skill in (REPO_ROOT / ".agents/skills").glob("*/SKILL.md"):
|
|
156
|
-
data = frontmatter.parse_frontmatter(
|
|
157
|
-
frontmatter.extract_frontmatter(skill.read_text(encoding="utf-8")))
|
|
158
|
-
description = data.get("description")
|
|
159
|
-
if isinstance(description, str) and len(description) > 1024:
|
|
160
|
-
problems.append(f"{skill.relative_to(REPO_ROOT)}: {len(description)}")
|
|
161
|
-
self.assertEqual(
|
|
162
|
-
problems,
|
|
163
|
-
[],
|
|
164
|
-
"skill descriptions over 1024 characters:\n" + "\n".join(problems),
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if __name__ == "__main__":
|
|
169
|
-
unittest.main(verbosity=2)
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""dist-kit smoke — proves the BUILT kit's `validate-graph` imports + runs
|
|
3
|
-
(Welle 52 / Slice 7 / #1936, PLAN Step 15).
|
|
4
|
-
|
|
5
|
-
Why: `bundle.mjs`'s HELPER_FILES allowlist is the only thing that ships
|
|
6
|
-
program_graph.py / program_graph_parse.py / program_graph_validate.py /
|
|
7
|
-
node_kind.py / board_fields.py / program_sync.py into dist-kit/scripts/ — all
|
|
8
|
-
six are imported UNCONDITIONALLY at board-sync.py's module top (not lazily
|
|
9
|
-
inside a subcommand handler), so a forgotten entry does not fail at build time,
|
|
10
|
-
it fails the FIRST time a consumer runs the shipped CLI (ImportError). This
|
|
11
|
-
test catches that class of regression by actually invoking `validate-graph`
|
|
12
|
-
against the built dist-kit/, not the scripts/ SSOT copy the rest of the suite
|
|
13
|
-
imports.
|
|
14
|
-
|
|
15
|
-
Fully offline: a throwaway consumer board profile (BOARD_SYNC_PROFILE env var)
|
|
16
|
-
stands in for a project layer, and a fake `gh` shim on PATH answers the two
|
|
17
|
-
read-only `gh` calls `validate-graph` makes (the PRD body + the empty
|
|
18
|
-
sub-issues/children query) — no network, no real gh auth, no live board.
|
|
19
|
-
|
|
20
|
-
Run: python3 scripts/test_dist_kit_smoke.py
|
|
21
|
-
"""
|
|
22
|
-
import json
|
|
23
|
-
import os
|
|
24
|
-
import shutil
|
|
25
|
-
import stat
|
|
26
|
-
import subprocess
|
|
27
|
-
import sys
|
|
28
|
-
import tempfile
|
|
29
|
-
import unittest
|
|
30
|
-
from pathlib import Path
|
|
31
|
-
|
|
32
|
-
REPO = Path(__file__).resolve().parent.parent
|
|
33
|
-
DIST = REPO / "dist-kit"
|
|
34
|
-
|
|
35
|
-
# A minimal but structurally complete consumer profile (see board_config.py's
|
|
36
|
-
# _REQUIRED_PATHS) — distinct ids from Testreporter's own, so a leak would be
|
|
37
|
-
# obvious. `fields.phase` is included (with the same shape #1936 gives the
|
|
38
|
-
# real Testreporter profile) so the fixture also exercises the Phase-aware
|
|
39
|
-
# path in validate-graph, not just its defensive None-fallback.
|
|
40
|
-
FIXTURE_PROFILE = {
|
|
41
|
-
"repo": "acme/widgets",
|
|
42
|
-
"project": {"number": 7, "owner": "acme", "nodeId": "PVT_FIXTURE"},
|
|
43
|
-
"fields": {
|
|
44
|
-
"status": {"id": "PVTSSF_FIXTURE", "options": {"Spec": "opt-spec"}},
|
|
45
|
-
"wave": "PVTF_FIXTURE_WAVE",
|
|
46
|
-
"cluster": "PVTF_FIXTURE_CLUSTER",
|
|
47
|
-
"specPath": "PVTF_FIXTURE_SPEC",
|
|
48
|
-
"planPath": "PVTF_FIXTURE_PLAN",
|
|
49
|
-
"phase": {"id": "PVTSSF_FIXTURE_PHASE",
|
|
50
|
-
"options": {"P1": "opt-p1", "P2": "opt-p2", "P3": "opt-p3"}},
|
|
51
|
-
},
|
|
52
|
-
"labels": {"readyForAgent": "ready-for-agent", "typePrefix": "type:",
|
|
53
|
-
"clusterType": "type:cluster", "waveStub": "wave-stub"},
|
|
54
|
-
"branchPrefixes": ["feat", "fix"],
|
|
55
|
-
"prMarkers": {"partOf": "Part of", "retroMarker": "**Retro:**",
|
|
56
|
-
"retroValues": ["ran", "skipped"]},
|
|
57
|
-
"headings": {"vorBau": "Clarify Before Build"},
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
# A minimal green Program-PRD body — one wave, two slices, full scope coverage,
|
|
61
|
-
# no cycles/backward-refs/gate warnings — the same shape as test_program_graph.py's
|
|
62
|
-
# GREEN_PRD fixture, duplicated here (not imported) so this smoke stays
|
|
63
|
-
# self-contained and independent of the SSOT test module's import side effects.
|
|
64
|
-
GREEN_PRD = (
|
|
65
|
-
"<!-- prd: program -->\n"
|
|
66
|
-
"**plan_revision:** r1\n\n"
|
|
67
|
-
"## Scope\n"
|
|
68
|
-
"- **S1:** Programm-Graph-Fundament\n"
|
|
69
|
-
"- **S2:** Execute-Ready-Erweiterung\n\n"
|
|
70
|
-
"## Wellenplan\n"
|
|
71
|
-
"<!-- wellenplan:start -->\n"
|
|
72
|
-
"| Welle | Name | Phase | Slices | Gate | covers |\n"
|
|
73
|
-
"|---|---|---|---|---|---|\n"
|
|
74
|
-
"| 1 | Fundament | P1 | 1a, 1b | — | S1,S2 |\n"
|
|
75
|
-
"<!-- wellenplan:end -->\n\n"
|
|
76
|
-
"## Phasen-Gates\n"
|
|
77
|
-
"- [ ] P1: Fundament steht\n\n"
|
|
78
|
-
"## Slices\n"
|
|
79
|
-
"#### 1a — Graph-Modul\n"
|
|
80
|
-
"<!-- wave: 1 -->\n"
|
|
81
|
-
"<!-- phase: P1 -->\n"
|
|
82
|
-
"<!-- area: scripts -->\n"
|
|
83
|
-
"<!-- gate: — -->\n"
|
|
84
|
-
"<!-- blocked_by: none -->\n\n"
|
|
85
|
-
"#### 1b — Execute-Ready\n"
|
|
86
|
-
"<!-- wave: 1 -->\n"
|
|
87
|
-
"<!-- phase: P1 -->\n"
|
|
88
|
-
"<!-- area: scripts -->\n"
|
|
89
|
-
"<!-- gate: — -->\n"
|
|
90
|
-
"<!-- blocked_by: 1a -->\n"
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
# A fake `gh` binary: answers the sub-issues/children GraphQL query (empty —
|
|
94
|
-
# no published stubs yet) and the PRD-body `issue view` read from a file, so
|
|
95
|
-
# `validate-graph` runs end to end without any real `gh` call.
|
|
96
|
-
FAKE_GH = """#!/usr/bin/env python3
|
|
97
|
-
import os, sys
|
|
98
|
-
args = sys.argv[1:]
|
|
99
|
-
if "subIssues" in " ".join(args):
|
|
100
|
-
sys.stdout.write('{"data":{"repository":{"issue":{"subIssues":{"nodes":[]}}}}}')
|
|
101
|
-
else:
|
|
102
|
-
with open(os.environ["DIST_KIT_SMOKE_BODY"], encoding="utf-8") as fh:
|
|
103
|
-
sys.stdout.write(fh.read())
|
|
104
|
-
"""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _build_dist_kit():
|
|
108
|
-
"""Run the real build (subprocess — the maintainer's actual publish step).
|
|
109
|
-
Returns (CompletedProcess, skip_reason); skip_reason is set only when node
|
|
110
|
-
itself is unavailable in this environment (mirrors test_skill_publish_audit.py)."""
|
|
111
|
-
build = REPO / "scripts/build-kit.mjs"
|
|
112
|
-
try:
|
|
113
|
-
r = subprocess.run(["node", str(build)], cwd=REPO,
|
|
114
|
-
capture_output=True, text=True, timeout=120)
|
|
115
|
-
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
|
116
|
-
return None, str(e)
|
|
117
|
-
return r, None
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class DistKitValidateGraphSmoke(unittest.TestCase):
|
|
121
|
-
"""Builds dist-kit/ then runs `board-sync.py validate-graph` from the
|
|
122
|
-
SHIPPED location — proves the whole program_graph* / node_kind / board_fields
|
|
123
|
-
/ program_sync import chain resolves in the actual publish artefact, and
|
|
124
|
-
that the command produces the correct counted report on a known fixture."""
|
|
125
|
-
|
|
126
|
-
def setUp(self):
|
|
127
|
-
r, skip_reason = _build_dist_kit()
|
|
128
|
-
if skip_reason:
|
|
129
|
-
self.skipTest(f"node/build unavailable: {skip_reason}")
|
|
130
|
-
self.assertEqual(r.returncode, 0, r.stderr)
|
|
131
|
-
self.board_sync_py = DIST / "scripts/board-sync.py"
|
|
132
|
-
self.assertTrue(self.board_sync_py.exists(), "dist-kit/scripts/board-sync.py missing")
|
|
133
|
-
|
|
134
|
-
self.tmp = Path(tempfile.mkdtemp())
|
|
135
|
-
self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
|
|
136
|
-
|
|
137
|
-
profile_md = ("<!-- board-sync:profile -->\n```json\n"
|
|
138
|
-
+ json.dumps(FIXTURE_PROFILE) + "\n```\n")
|
|
139
|
-
profile_path = self.tmp / "board-sync.md"
|
|
140
|
-
profile_path.write_text(profile_md, encoding="utf-8")
|
|
141
|
-
|
|
142
|
-
body_path = self.tmp / "prd-body.md"
|
|
143
|
-
body_path.write_text(GREEN_PRD, encoding="utf-8")
|
|
144
|
-
|
|
145
|
-
fake_gh_dir = self.tmp / "bin"
|
|
146
|
-
fake_gh_dir.mkdir()
|
|
147
|
-
gh_path = fake_gh_dir / "gh"
|
|
148
|
-
gh_path.write_text(FAKE_GH, encoding="utf-8")
|
|
149
|
-
gh_path.chmod(gh_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
150
|
-
|
|
151
|
-
self.env = {
|
|
152
|
-
**os.environ,
|
|
153
|
-
"PATH": f"{fake_gh_dir}{os.pathsep}{os.environ.get('PATH', '')}",
|
|
154
|
-
"BOARD_SYNC_PROFILE": str(profile_path),
|
|
155
|
-
"DIST_KIT_SMOKE_BODY": str(body_path),
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
def test_validate_graph_runs_against_the_built_kit(self):
|
|
159
|
-
result = subprocess.run(
|
|
160
|
-
[sys.executable, str(self.board_sync_py), "validate-graph", "--issue", "42"],
|
|
161
|
-
capture_output=True, text=True, timeout=30, env=self.env,
|
|
162
|
-
)
|
|
163
|
-
self.assertEqual(result.returncode, 0,
|
|
164
|
-
f"stdout={result.stdout!r} stderr={result.stderr!r}")
|
|
165
|
-
self.assertIn("Scope-Abdeckung 2 von 2", result.stdout)
|
|
166
|
-
self.assertIn("Rollup-Kette ✓", result.stdout)
|
|
167
|
-
self.assertIn("zyklenfrei ✓", result.stdout)
|
|
168
|
-
self.assertIn("Kapazität ✓", result.stdout)
|
|
169
|
-
self.assertIn("Phasen-Optionen ✓", result.stdout)
|
|
170
|
-
|
|
171
|
-
def test_no_german_status_vocabulary_ships(self):
|
|
172
|
-
"""kit#26 / #1947 acceptance, verbatim: a fresh consumer install must
|
|
173
|
-
grep clean for testreporter's German board-status names. The status
|
|
174
|
-
vocabulary lives ONLY in a board's own profile (fields.status.roles) —
|
|
175
|
-
shipped scripts, skills, and doc templates speak role language. This
|
|
176
|
-
greps the whole BUILT artefact, so any future spot the word-list
|
|
177
|
-
census or the command lint can't see (a bare status name carries no
|
|
178
|
-
German function word and no board-sync.py call) still fails here."""
|
|
179
|
-
import re
|
|
180
|
-
pat = re.compile(r"\bIn Arbeit\b|\bIdee\b")
|
|
181
|
-
hits = []
|
|
182
|
-
for path in sorted(DIST.rglob("*")):
|
|
183
|
-
if not path.is_file():
|
|
184
|
-
continue
|
|
185
|
-
try:
|
|
186
|
-
text = path.read_text(encoding="utf-8")
|
|
187
|
-
except (UnicodeDecodeError, PermissionError):
|
|
188
|
-
continue
|
|
189
|
-
for n, line in enumerate(text.splitlines(), 1):
|
|
190
|
-
if pat.search(line):
|
|
191
|
-
hits.append(f"{path.relative_to(DIST)}:{n}: {line.strip()[:120]}")
|
|
192
|
-
self.assertEqual(hits, [],
|
|
193
|
-
"German board-status vocabulary in the shipped kit — speak role "
|
|
194
|
-
"language (fields.status.roles / --status-role) instead:\n"
|
|
195
|
-
+ "\n".join(hits))
|
|
196
|
-
|
|
197
|
-
def test_shipped_census_runs_for_a_foreign_greenfield_on_both_surfaces(self):
|
|
198
|
-
consumer = self.tmp / "census-consumer"
|
|
199
|
-
shutil.copytree(REPO / "test/fixtures/census-consumers/greenfield", consumer)
|
|
200
|
-
subprocess.run(["git", "init", "--quiet"], cwd=consumer, check=True)
|
|
201
|
-
subprocess.run(["git", "add", "."], cwd=consumer, check=True)
|
|
202
|
-
claude = (DIST / ".claude/skills/census-update/SKILL.md").read_text()
|
|
203
|
-
codex = (DIST / ".agents/skills/census-update/SKILL.md").read_text()
|
|
204
|
-
self.assertEqual(claude, codex)
|
|
205
|
-
module = (DIST / "scripts/census/index.mjs").as_uri()
|
|
206
|
-
runner = (
|
|
207
|
-
f'import {{ scanCensus }} from {json.dumps(module)};'
|
|
208
|
-
"const r=await scanCensus({repoRoot:process.argv[1],enabled:true});"
|
|
209
|
-
"console.log(JSON.stringify({state:r.state,count:r.denominator.length}));"
|
|
210
|
-
)
|
|
211
|
-
result = subprocess.run(
|
|
212
|
-
["node", "--input-type=module", "--eval", runner, str(consumer)],
|
|
213
|
-
cwd=DIST, capture_output=True, text=True, timeout=30,
|
|
214
|
-
)
|
|
215
|
-
self.assertEqual(result.returncode, 0, result.stderr)
|
|
216
|
-
self.assertEqual(json.loads(result.stdout), {"state": "bootstrap", "count": 2})
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if __name__ == "__main__":
|
|
220
|
-
unittest.main(verbosity=2)
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Diagnostics contract for the handoff drift guard.
|
|
3
|
-
|
|
4
|
-
Three independent gaps, one guard:
|
|
5
|
-
|
|
6
|
-
* the issue anchor may only come from the handoff repository's OWN issues —
|
|
7
|
-
a link to a foreign repository's issue must never become the anchor;
|
|
8
|
-
* a census block must name the checkout it evaluated, and say so explicitly
|
|
9
|
-
when the session sits in a different worktree of the same repository;
|
|
10
|
-
* `--census-status` must report WHAT drifted, bounded, not only that the
|
|
11
|
-
topology fingerprint moved.
|
|
12
|
-
"""
|
|
13
|
-
import importlib.util
|
|
14
|
-
import json
|
|
15
|
-
import subprocess
|
|
16
|
-
import sys
|
|
17
|
-
import tempfile
|
|
18
|
-
import unittest
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
from unittest.mock import patch
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
ROOT = Path(__file__).resolve().parents[1]
|
|
24
|
-
HOOKS = ROOT / ".claude" / "hooks"
|
|
25
|
-
sys.path.insert(0, str(HOOKS))
|
|
26
|
-
SPEC = importlib.util.spec_from_file_location("drift_guard", HOOKS / "drift-guard.py")
|
|
27
|
-
DRIFT_GUARD = importlib.util.module_from_spec(SPEC)
|
|
28
|
-
SPEC.loader.exec_module(DRIFT_GUARD)
|
|
29
|
-
|
|
30
|
-
GIT_IDENTITY = [
|
|
31
|
-
"-c", "user.name=drift-guard-test",
|
|
32
|
-
"-c", "user.email=drift-guard@example.invalid",
|
|
33
|
-
"-c", "commit.gpgsign=false",
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class DriftGuardDiagnosticsTest(unittest.TestCase):
|
|
38
|
-
def make_repo(self, remote=None):
|
|
39
|
-
temporary = tempfile.TemporaryDirectory(prefix="awk-drift-guard-")
|
|
40
|
-
root = Path(temporary.name)
|
|
41
|
-
self.addCleanup(temporary.cleanup)
|
|
42
|
-
(root / "src").mkdir()
|
|
43
|
-
(root / "package.json").write_text('{"name":"consumer"}\n', encoding="utf-8")
|
|
44
|
-
(root / "src" / "index.mjs").write_text("export const ready = true;\n", encoding="utf-8")
|
|
45
|
-
subprocess.run(["git", "init", "--quiet"], cwd=root, check=True)
|
|
46
|
-
subprocess.run(["git", "add", "."], cwd=root, check=True)
|
|
47
|
-
if remote:
|
|
48
|
-
subprocess.run(["git", "remote", "add", "origin", remote], cwd=root, check=True)
|
|
49
|
-
return root
|
|
50
|
-
|
|
51
|
-
def enable(self, root):
|
|
52
|
-
census = root / ".census"
|
|
53
|
-
census.mkdir(exist_ok=True)
|
|
54
|
-
(census / "profile.json").write_text(json.dumps({
|
|
55
|
-
"schemaVersion": 1,
|
|
56
|
-
"enabled": True,
|
|
57
|
-
"decisions": [],
|
|
58
|
-
"localScanners": [],
|
|
59
|
-
"overrides": [],
|
|
60
|
-
}) + "\n", encoding="utf-8")
|
|
61
|
-
|
|
62
|
-
def activate_current(self, root):
|
|
63
|
-
fresh = DRIFT_GUARD.scan_census_status(root)["fresh"]
|
|
64
|
-
(root / ".census" / "active.json").write_text(
|
|
65
|
-
json.dumps(fresh) + "\n", encoding="utf-8"
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
def write_payload(self, root, name, content):
|
|
69
|
-
handoff = root / ".handoff"
|
|
70
|
-
handoff.mkdir(exist_ok=True)
|
|
71
|
-
return {
|
|
72
|
-
"tool_name": "Write",
|
|
73
|
-
"tool_input": {"file_path": str(handoff / name), "content": content},
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
def census_status(self, root, *extra):
|
|
77
|
-
completed = subprocess.run(
|
|
78
|
-
[sys.executable, str(HOOKS / "drift-guard.py"), "--census-status", *extra],
|
|
79
|
-
cwd=root, capture_output=True, check=True, text=True,
|
|
80
|
-
)
|
|
81
|
-
return json.loads(completed.stdout)
|
|
82
|
-
|
|
83
|
-
# --- anchor extraction ------------------------------------------------
|
|
84
|
-
|
|
85
|
-
def test_foreign_repository_issue_link_never_becomes_the_anchor(self):
|
|
86
|
-
root = self.make_repo(remote="git@github.com:acme/consumer.git")
|
|
87
|
-
content = (
|
|
88
|
-
"Refresh landed as chore PR "
|
|
89
|
-
"[#2281](https://github.com/acme/consumer/pull/2281).\n"
|
|
90
|
-
"Reported upstream: "
|
|
91
|
-
"[kit#276](https://github.com/iKon85/agent-workflow-kit/issues/276)\n"
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
issue = DRIFT_GUARD.extract_issue(
|
|
95
|
-
self.write_payload(root, "2026-07-26-2279.md", content), content, root
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
self.assertEqual(issue, 2279)
|
|
99
|
-
|
|
100
|
-
def test_own_repository_content_anchor_still_wins_over_the_filename(self):
|
|
101
|
-
for remote in (
|
|
102
|
-
"git@github.com:acme/consumer.git",
|
|
103
|
-
"https://github.com/acme/consumer.git",
|
|
104
|
-
"ssh://git@github.com/acme/consumer",
|
|
105
|
-
):
|
|
106
|
-
with self.subTest(remote=remote):
|
|
107
|
-
root = self.make_repo(remote=remote)
|
|
108
|
-
content = (
|
|
109
|
-
"Anchor [#2280](https://github.com/acme/consumer/issues/2280) — "
|
|
110
|
-
"upstream [#276](https://github.com/iKon85/agent-workflow-kit/issues/276)\n"
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
issue = DRIFT_GUARD.extract_issue(
|
|
114
|
-
self.write_payload(root, "2026-07-26-2279.md", content), content, root
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
self.assertEqual(issue, 2280)
|
|
118
|
-
|
|
119
|
-
def test_without_a_parsable_remote_the_filename_anchor_is_preferred(self):
|
|
120
|
-
root = self.make_repo()
|
|
121
|
-
content = "Upstream [#276](https://github.com/iKon85/agent-workflow-kit/issues/276)\n"
|
|
122
|
-
|
|
123
|
-
issue = DRIFT_GUARD.extract_issue(
|
|
124
|
-
self.write_payload(root, "2026-07-26-2279.md", content), content, root
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
self.assertEqual(issue, 2279)
|
|
128
|
-
|
|
129
|
-
def test_without_remote_and_without_filename_anchor_the_content_anchor_remains(self):
|
|
130
|
-
root = self.make_repo()
|
|
131
|
-
content = "Anchor [#276](https://github.com/iKon85/agent-workflow-kit/issues/276)\n"
|
|
132
|
-
|
|
133
|
-
issue = DRIFT_GUARD.extract_issue(
|
|
134
|
-
self.write_payload(root, "session-notes.md", content), content, root
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
self.assertEqual(issue, 276)
|
|
138
|
-
|
|
139
|
-
def test_a_foreign_only_handoff_without_a_filename_anchor_fails_open(self):
|
|
140
|
-
root = self.make_repo(remote="git@github.com:acme/consumer.git")
|
|
141
|
-
content = "Upstream [#276](https://github.com/iKon85/agent-workflow-kit/issues/276)\n"
|
|
142
|
-
payload = self.write_payload(root, "session-notes.md", content)
|
|
143
|
-
|
|
144
|
-
self.assertIsNone(DRIFT_GUARD.extract_issue(payload, content, root))
|
|
145
|
-
with patch.object(DRIFT_GUARD, "run_check") as check:
|
|
146
|
-
self.assertEqual(DRIFT_GUARD.should_block(payload), (False, ""))
|
|
147
|
-
check.assert_not_called()
|
|
148
|
-
|
|
149
|
-
def test_block_message_names_the_own_anchor_not_the_foreign_issue(self):
|
|
150
|
-
root = self.make_repo(remote="git@github.com:acme/consumer.git")
|
|
151
|
-
content = (
|
|
152
|
-
"Refresh landed as chore PR "
|
|
153
|
-
"[#2281](https://github.com/acme/consumer/pull/2281).\n"
|
|
154
|
-
"Reported upstream: "
|
|
155
|
-
"[kit#276](https://github.com/iKon85/agent-workflow-kit/issues/276)\n"
|
|
156
|
-
)
|
|
157
|
-
payload = self.write_payload(root, "2026-07-26-2279.md", content)
|
|
158
|
-
checked = []
|
|
159
|
-
|
|
160
|
-
def check(issue, intent):
|
|
161
|
-
checked.append(issue)
|
|
162
|
-
return {"deny_recommended": True, "violations": [f"#{issue}: plan_revision missing"]}
|
|
163
|
-
|
|
164
|
-
with patch.object(DRIFT_GUARD, "run_check", side_effect=check):
|
|
165
|
-
blocked, message = DRIFT_GUARD.should_block(payload)
|
|
166
|
-
|
|
167
|
-
self.assertEqual(checked, [2279])
|
|
168
|
-
self.assertTrue(blocked)
|
|
169
|
-
self.assertIn("#2279", message)
|
|
170
|
-
self.assertNotIn("276", message)
|
|
171
|
-
|
|
172
|
-
# --- evaluated checkout -----------------------------------------------
|
|
173
|
-
|
|
174
|
-
def test_census_block_message_names_the_evaluated_checkout(self):
|
|
175
|
-
root = self.make_repo()
|
|
176
|
-
result = {"state": "refresh_required", "reasons": ["topology"], "overrides": []}
|
|
177
|
-
|
|
178
|
-
message = DRIFT_GUARD.build_census_block_message(2279, result, root, root)
|
|
179
|
-
|
|
180
|
-
self.assertIn(str(root), message)
|
|
181
|
-
self.assertIn("evaluated checkout", message)
|
|
182
|
-
self.assertIn("worktree", message)
|
|
183
|
-
self.assertIn("$census-update", message)
|
|
184
|
-
|
|
185
|
-
def test_a_sibling_worktree_working_directory_is_named_explicitly(self):
|
|
186
|
-
root = self.make_repo()
|
|
187
|
-
subprocess.run(["git", *GIT_IDENTITY, "commit", "--quiet", "-m", "init"],
|
|
188
|
-
cwd=root, check=True)
|
|
189
|
-
sibling = root / "sibling-worktree"
|
|
190
|
-
subprocess.run(["git", "worktree", "add", "--quiet", str(sibling), "-b", "slice"],
|
|
191
|
-
cwd=root, check=True)
|
|
192
|
-
result = {"state": "refresh_required", "reasons": ["topology"], "overrides": []}
|
|
193
|
-
|
|
194
|
-
from_sibling = DRIFT_GUARD.build_census_block_message(2279, result, root, sibling)
|
|
195
|
-
from_root = DRIFT_GUARD.build_census_block_message(2279, result, root, root)
|
|
196
|
-
from_elsewhere = DRIFT_GUARD.build_census_block_message(
|
|
197
|
-
2279, result, root, self.make_repo()
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
self.assertIn(str(sibling), from_sibling)
|
|
201
|
-
self.assertIn("different worktree", from_sibling)
|
|
202
|
-
self.assertNotIn("different worktree", from_root)
|
|
203
|
-
self.assertNotIn("different worktree", from_elsewhere)
|
|
204
|
-
|
|
205
|
-
def test_a_blocked_build_handoff_reports_the_checkout_it_evaluated(self):
|
|
206
|
-
root = self.make_repo()
|
|
207
|
-
payload = self.write_payload(
|
|
208
|
-
root, "2279.md", "Build [#2279](https://github.com/acme/consumer/issues/2279)\n"
|
|
209
|
-
)
|
|
210
|
-
refresh = {
|
|
211
|
-
"state": "refresh_required", "block_handoff": True,
|
|
212
|
-
"reasons": ["topology"], "overrides": [],
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
with patch.object(DRIFT_GUARD, "run_check", return_value={"deny_recommended": False}), \
|
|
216
|
-
patch.object(DRIFT_GUARD, "evaluate_census", return_value=refresh):
|
|
217
|
-
blocked, message = DRIFT_GUARD.should_block(payload)
|
|
218
|
-
|
|
219
|
-
self.assertTrue(blocked)
|
|
220
|
-
self.assertIn("evaluated checkout", message)
|
|
221
|
-
self.assertIn(str(root), message)
|
|
222
|
-
|
|
223
|
-
# --- drift delta -------------------------------------------------------
|
|
224
|
-
|
|
225
|
-
def test_census_status_reports_what_drifted(self):
|
|
226
|
-
root = self.make_repo()
|
|
227
|
-
self.enable(root)
|
|
228
|
-
self.activate_current(root)
|
|
229
|
-
(root / "src" / "added.mjs").write_text("export const added = true;\n", encoding="utf-8")
|
|
230
|
-
(root / "test").mkdir()
|
|
231
|
-
(root / "test" / "proof.test.mjs").write_text("// evidence\n", encoding="utf-8")
|
|
232
|
-
(root / "src" / "index.mjs").write_text("export const ready = false;\n", encoding="utf-8")
|
|
233
|
-
subprocess.run(["git", "add", "."], cwd=root, check=True)
|
|
234
|
-
|
|
235
|
-
status = self.census_status(root)
|
|
236
|
-
|
|
237
|
-
self.assertEqual(status["state"], "refresh_required")
|
|
238
|
-
self.assertIn("topology", status["reasons"])
|
|
239
|
-
delta = status["delta"]
|
|
240
|
-
self.assertIn("src/added.mjs", delta["denominator"]["added"])
|
|
241
|
-
self.assertIn("src/index.mjs", delta["denominator"]["changed"])
|
|
242
|
-
self.assertEqual(delta["denominator"]["removed"], [])
|
|
243
|
-
self.assertIn("test/proof.test.mjs", delta["evidence"]["added"])
|
|
244
|
-
self.assertEqual(delta["families"]["added"], [])
|
|
245
|
-
|
|
246
|
-
def test_a_new_surface_and_a_removed_file_are_named_in_the_delta(self):
|
|
247
|
-
root = self.make_repo()
|
|
248
|
-
self.enable(root)
|
|
249
|
-
self.activate_current(root)
|
|
250
|
-
(root / "packages" / "api" / "src").mkdir(parents=True)
|
|
251
|
-
(root / "packages" / "api" / "src" / "index.mjs").write_text(
|
|
252
|
-
"export const api = true;\n", encoding="utf-8"
|
|
253
|
-
)
|
|
254
|
-
(root / "src" / "index.mjs").unlink()
|
|
255
|
-
subprocess.run(["git", "add", "--all"], cwd=root, check=True)
|
|
256
|
-
|
|
257
|
-
status = self.census_status(root)
|
|
258
|
-
delta = status["delta"]
|
|
259
|
-
|
|
260
|
-
self.assertIn("packages/api/src/index.mjs", delta["denominator"]["added"])
|
|
261
|
-
self.assertIn("src/index.mjs", delta["denominator"]["removed"])
|
|
262
|
-
self.assertIn("surface:packages/api", delta["families"]["added"])
|
|
263
|
-
self.assertIn("surface:src", delta["families"]["removed"])
|
|
264
|
-
|
|
265
|
-
def test_a_large_delta_stays_bounded_and_verbose_reports_everything(self):
|
|
266
|
-
root = self.make_repo()
|
|
267
|
-
self.enable(root)
|
|
268
|
-
self.activate_current(root)
|
|
269
|
-
grown = DRIFT_GUARD.CENSUS_DELTA_LIMIT + 7
|
|
270
|
-
for index in range(grown):
|
|
271
|
-
(root / "src" / f"grown-{index:03d}.mjs").write_text(
|
|
272
|
-
f"export const grown{index} = true;\n", encoding="utf-8"
|
|
273
|
-
)
|
|
274
|
-
subprocess.run(["git", "add", "."], cwd=root, check=True)
|
|
275
|
-
|
|
276
|
-
capped = self.census_status(root)["delta"]["denominator"]["added"]
|
|
277
|
-
verbose = self.census_status(root, "--verbose")["delta"]["denominator"]["added"]
|
|
278
|
-
|
|
279
|
-
self.assertEqual(len(capped), DRIFT_GUARD.CENSUS_DELTA_LIMIT + 1)
|
|
280
|
-
self.assertEqual(capped[-1], "…and 7 more")
|
|
281
|
-
self.assertEqual(len(verbose), grown)
|
|
282
|
-
self.assertIn("src/grown-000.mjs", verbose)
|
|
283
|
-
|
|
284
|
-
def test_a_census_without_an_active_snapshot_reports_no_delta(self):
|
|
285
|
-
root = self.make_repo()
|
|
286
|
-
self.enable(root)
|
|
287
|
-
|
|
288
|
-
status = self.census_status(root)
|
|
289
|
-
|
|
290
|
-
self.assertEqual(status["state"], "bootstrap")
|
|
291
|
-
self.assertIsNone(status.get("delta"))
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if __name__ == "__main__":
|
|
295
|
-
unittest.main()
|