@ikon85/agent-workflow-kit 0.42.1 → 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.
Files changed (133) hide show
  1. package/.agents/skills/grill-me/SKILL.md +3 -1
  2. package/.agents/skills/grill-with-docs/SKILL.md +3 -1
  3. package/.agents/skills/kit-update/SKILL.md +9 -11
  4. package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
  5. package/.agents/skills/setup-workflow/SKILL.md +20 -18
  6. package/.agents/skills/setup-workflow/board-sync.md +4 -1
  7. package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
  8. package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
  9. package/.agents/skills/to-issues/SKILL.md +43 -0
  10. package/.agents/skills/wrapup/SKILL.md +98 -54
  11. package/.claude/skills/grill-me/SKILL.md +3 -1
  12. package/.claude/skills/grill-me-codex/SKILL.md +3 -1
  13. package/.claude/skills/grill-with-docs/SKILL.md +3 -1
  14. package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
  15. package/.claude/skills/kit-update/SKILL.md +9 -11
  16. package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
  17. package/.claude/skills/setup-workflow/SKILL.md +20 -18
  18. package/.claude/skills/setup-workflow/board-sync.md +4 -1
  19. package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
  20. package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
  21. package/.claude/skills/to-issues/SKILL.md +43 -0
  22. package/.claude/skills/wrapup/SKILL.md +98 -54
  23. package/README.md +268 -7
  24. package/agent-workflow-kit.package.json +42 -42
  25. package/package.json +16 -2
  26. package/scripts/marker_lib.py +3 -0
  27. package/scripts/profile_globs.py +26 -45
  28. package/scripts/release-delta-guard.mjs +29 -4
  29. package/scripts/worktree-lifecycle/README.md +57 -154
  30. package/scripts/worktree-lifecycle/capabilities.json +2 -3
  31. package/scripts/worktree-lifecycle/classify.py +480 -0
  32. package/scripts/worktree-lifecycle/cleanup.py +9 -29
  33. package/scripts/worktree-lifecycle/core.py +68 -1289
  34. package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
  35. package/scripts/worktree-lifecycle/profile.py +46 -29
  36. package/scripts/worktree-lifecycle/setup.py +7 -19
  37. package/scripts/wrapup-land.py +1292 -545
  38. package/src/consumer-migrations.json +1 -16
  39. package/src/lib/bundle.mjs +31 -1
  40. package/docs/adr/0001-consumer-divergence-policy.md +0 -53
  41. package/docs/adr/0002-capability-gated-orchestration.md +0 -70
  42. package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
  43. package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
  44. package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
  45. package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
  46. package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
  47. package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
  48. package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
  49. package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
  50. package/docs/agents/board-sync.md +0 -74
  51. package/docs/agents/code-review.md +0 -14
  52. package/docs/agents/domain.md +0 -7
  53. package/docs/agents/issue-tracker.md +0 -23
  54. package/docs/agents/skills/local-ci.md +0 -90
  55. package/docs/agents/skills/orchestrate-wave.md +0 -166
  56. package/docs/agents/skills/spec-self-critique.md +0 -18
  57. package/docs/agents/triage-labels.md +0 -10
  58. package/docs/agents/workflow-capabilities.json +0 -28
  59. package/docs/conventions/spec-completeness.md +0 -23
  60. package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
  61. package/docs/research/benchlm-routing-source.md +0 -198
  62. package/docs/research/consumer-owned-protocol-files.md +0 -238
  63. package/docs/research/frontend-agent-benchmarks.md +0 -282
  64. package/docs/research/model-effort-routing-benchmarks.md +0 -261
  65. package/docs/research/provider-neutral-agent-routing.md +0 -207
  66. package/docs/research/wave-152-consumer-acceptance.md +0 -98
  67. package/docs/research/wave-43-script-hook-census.md +0 -128
  68. package/scripts/build-kit.mjs +0 -117
  69. package/scripts/build-kit.test.mjs +0 -204
  70. package/scripts/census/census.test.mjs +0 -252
  71. package/scripts/census/state.test.mjs +0 -15
  72. package/scripts/census/transaction.test.mjs +0 -103
  73. package/scripts/census-contract.test.mjs +0 -227
  74. package/scripts/check-kit-staleness.mjs +0 -45
  75. package/scripts/check-kit-staleness.test.mjs +0 -19
  76. package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -152
  77. package/scripts/codex-exec.test.mjs +0 -871
  78. package/scripts/grill-census-wiring-guard.mjs +0 -87
  79. package/scripts/grill-census-wiring-guard.test.mjs +0 -107
  80. package/scripts/kit-release.test.mjs +0 -273
  81. package/scripts/kit-update-pr.test.mjs +0 -151
  82. package/scripts/lib/audit-refs.mjs +0 -146
  83. package/scripts/lib/scrub.mjs +0 -113
  84. package/scripts/lib/scrub.test.mjs +0 -187
  85. package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
  86. package/scripts/portability_profile_scan.py +0 -53
  87. package/scripts/release-delta-guard.test.mjs +0 -151
  88. package/scripts/release-parity.test.mjs +0 -90
  89. package/scripts/release-state.test.mjs +0 -360
  90. package/scripts/test_anchor_table.py +0 -69
  91. package/scripts/test_board_bootstrap.py +0 -348
  92. package/scripts/test_board_sync.py +0 -208
  93. package/scripts/test_board_sync_create_idempotency.py +0 -117
  94. package/scripts/test_board_sync_wave_title.py +0 -120
  95. package/scripts/test_census_backstop.py +0 -712
  96. package/scripts/test_census_forward_contract.py +0 -91
  97. package/scripts/test_census_update_contract.test.mjs +0 -454
  98. package/scripts/test_codex_adapter_sync_contract.py +0 -169
  99. package/scripts/test_dist_kit_smoke.py +0 -220
  100. package/scripts/test_drift_guard_diagnostics.py +0 -295
  101. package/scripts/test_issue_claim_contract.py +0 -169
  102. package/scripts/test_kit_docs_language_census.py +0 -178
  103. package/scripts/test_marker_lib.py +0 -162
  104. package/scripts/test_orchestrate_wave_contract.py +0 -381
  105. package/scripts/test_pr_body_check.py +0 -264
  106. package/scripts/test_profile_globs.py +0 -280
  107. package/scripts/test_program_planning_contract.py +0 -133
  108. package/scripts/test_release_authorization_contract.py +0 -101
  109. package/scripts/test_render_anchor.py +0 -267
  110. package/scripts/test_retro_wrapup_contract.py +0 -153
  111. package/scripts/test_skill_code_review_seed.py +0 -89
  112. package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
  113. package/scripts/test_skill_frontmatter_lint.py +0 -234
  114. package/scripts/test_skill_gh_lint.py +0 -124
  115. package/scripts/test_skill_language_census.py +0 -254
  116. package/scripts/test_skill_optional_readiness.py +0 -171
  117. package/scripts/test_skill_portability_lint.py +0 -866
  118. package/scripts/test_skill_precommit_template.py +0 -30
  119. package/scripts/test_skill_publish_audit.py +0 -358
  120. package/scripts/test_skill_readiness_contract.py +0 -264
  121. package/scripts/test_skill_readiness_preflight.py +0 -180
  122. package/scripts/test_skill_required_readiness.py +0 -233
  123. package/scripts/test_skill_selfcontainment_lint.py +0 -255
  124. package/scripts/test_skill_setup_workflow_seeds.py +0 -691
  125. package/scripts/test_skill_stale_name_lint.py +0 -126
  126. package/scripts/test_skill_surface_refs.py +0 -300
  127. package/scripts/test_skill_trailing_artifact_lint.py +0 -71
  128. package/scripts/test_tdd_contract.py +0 -78
  129. package/scripts/test_worktree_ignore_seed.py +0 -320
  130. package/scripts/test_worktree_setup_base_guard.py +0 -140
  131. package/scripts/test_worktree_wrapup_contract.py +0 -1648
  132. package/scripts/test_wrapup_land.py +0 -428
  133. package/scripts/worktree-lifecycle/session.py +0 -1857
@@ -1,428 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Focused contracts for wrapup's bounded PR-check merge gate."""
3
-
4
- import importlib.util
5
- import io
6
- import json
7
- import subprocess
8
- import unittest
9
- from pathlib import Path
10
-
11
- REPO = Path(__file__).resolve().parent.parent
12
- WRAPUP = REPO / "scripts/wrapup-land.py"
13
-
14
-
15
- def load_wrapup():
16
- spec = importlib.util.spec_from_file_location("wrapup_land_check_gate", WRAPUP)
17
- module = importlib.util.module_from_spec(spec)
18
- spec.loader.exec_module(module)
19
- return module
20
-
21
-
22
- def completed(payload, returncode=0, stderr=""):
23
- return subprocess.CompletedProcess([], returncode, json.dumps(payload), stderr)
24
-
25
-
26
- def pr_snapshot(**overrides):
27
- snapshot = {
28
- "state": "OPEN",
29
- "mergeable": "MERGEABLE",
30
- "mergeStateStatus": "CLEAN",
31
- "statusCheckRollup": [],
32
- "baseRefName": "main",
33
- }
34
- snapshot.update(overrides)
35
- return snapshot
36
-
37
-
38
- def gate_runner(snapshots, required_checks):
39
- snapshots = iter(snapshots)
40
- required_checks = iter(required_checks)
41
-
42
- def runner(cmd, **_kwargs):
43
- if cmd[:3] == ["gh", "pr", "view"]:
44
- return completed(next(snapshots))
45
- if cmd[:3] == ["gh", "pr", "checks"]:
46
- checks = next(required_checks)
47
- return completed(checks, returncode=1 if any(
48
- check.get("state") == "FAILURE" for check in checks
49
- ) else 0)
50
- raise AssertionError(f"unexpected command: {cmd}")
51
-
52
- return runner
53
-
54
-
55
- class FakeClock:
56
- def __init__(self):
57
- self.now = 0.0
58
-
59
- def monotonic(self):
60
- return self.now
61
-
62
- def sleep(self, seconds):
63
- self.now += seconds
64
-
65
-
66
- class WrapupCheckGateContract(unittest.TestCase):
67
- def setUp(self):
68
- self.wrapup = load_wrapup()
69
-
70
- def test_required_check_names_come_from_active_base_branch_rules(self):
71
- commands = []
72
-
73
- def runner(cmd, **_kwargs):
74
- commands.append(cmd)
75
- if cmd[:3] == ["gh", "repo", "view"]:
76
- return completed({"nameWithOwner": "acme/repo"})
77
- if cmd[:2] == ["gh", "api"]:
78
- return completed([
79
- {"type": "pull_request"},
80
- {
81
- "type": "required_status_checks",
82
- "parameters": {
83
- "required_status_checks": [
84
- {"context": "test", "integration_id": 1},
85
- {"context": "lint", "integration_id": 1},
86
- ],
87
- },
88
- },
89
- ])
90
- raise AssertionError(f"unexpected command: {cmd}")
91
-
92
- self.assertEqual(
93
- self.wrapup._configured_required_check_names(
94
- pr_snapshot(baseRefName="release/next"), runner
95
- ),
96
- {"test", "lint"},
97
- )
98
- self.assertIn(
99
- ["gh", "api", "repos/acme/repo/rules/branches/release%2Fnext"],
100
- commands,
101
- )
102
-
103
- def test_pending_checks_are_visible_then_green_checks_proceed(self):
104
- snapshots = iter([
105
- pr_snapshot(mergeable="UNKNOWN", mergeStateStatus="BLOCKED"),
106
- pr_snapshot(),
107
- ])
108
- checks = iter([
109
- [{"name": "test", "state": "PENDING", "link": "https://example.test"}],
110
- [{"name": "test", "state": "SUCCESS", "link": "https://example.test"}],
111
- ])
112
- calls = []
113
-
114
- def runner(cmd, **_kwargs):
115
- calls.append(cmd)
116
- if cmd[:3] == ["gh", "pr", "view"]:
117
- return completed(next(snapshots))
118
- if cmd[:3] == ["gh", "pr", "checks"]:
119
- return completed(next(checks))
120
- raise AssertionError(f"unexpected command: {cmd}")
121
-
122
- clock = FakeClock()
123
- progress = io.StringIO()
124
- already_merged = self.wrapup.wait_for_merge_gate(
125
- "42",
126
- timeout_seconds=30,
127
- poll_interval=5,
128
- command_runner=runner,
129
- clock=clock.monotonic,
130
- sleeper=clock.sleep,
131
- progress_stream=progress,
132
- configured_required_names={"test"},
133
- )
134
-
135
- self.assertFalse(already_merged)
136
- self.assertEqual(len(calls), 4)
137
- self.assertIn("waiting for PR #42 checks", progress.getvalue())
138
- self.assertIn("test", progress.getvalue())
139
-
140
- def test_explicit_null_conclusion_waits_even_with_green_legacy_state(self):
141
- checks = [{
142
- "name": "test",
143
- "status": "COMPLETED",
144
- "conclusion": None,
145
- "state": "SUCCESS",
146
- }]
147
-
148
- self.assertEqual(self.wrapup.pending_checks(checks), checks)
149
-
150
- def test_terminal_red_check_stops_before_merge_and_names_check(self):
151
- snapshot = pr_snapshot(mergeStateStatus="BLOCKED")
152
- checks = [[{
153
- "name": "test",
154
- "state": "FAILURE",
155
- "link": "https://example.test",
156
- }]]
157
-
158
- with self.assertRaises(self.wrapup.Stop) as stopped:
159
- self.wrapup.wait_for_merge_gate(
160
- "42",
161
- command_runner=gate_runner([snapshot], checks),
162
- configured_required_names={"test"},
163
- )
164
-
165
- self.assertEqual(stopped.exception.step, "0c merge-gate")
166
- self.assertIn("test", stopped.exception.detail)
167
- self.assertNotIn("infrastructure failure", stopped.exception.detail)
168
-
169
- def test_timeout_names_pending_checks_and_elapsed_time(self):
170
- snapshot = pr_snapshot(mergeable="UNKNOWN", mergeStateStatus="BLOCKED")
171
- checks = [
172
- {"name": "test", "state": "QUEUED"},
173
- {"name": "lint", "state": "PENDING"},
174
- ]
175
- clock = FakeClock()
176
-
177
- with self.assertRaises(self.wrapup.Stop) as stopped:
178
- self.wrapup.wait_for_merge_gate(
179
- "42",
180
- timeout_seconds=10,
181
- poll_interval=5,
182
- command_runner=gate_runner(
183
- [snapshot, snapshot, snapshot],
184
- [checks, checks, checks],
185
- ),
186
- clock=clock.monotonic,
187
- sleeper=clock.sleep,
188
- progress_stream=io.StringIO(),
189
- configured_required_names={"test", "lint"},
190
- )
191
-
192
- self.assertIn("wait budget exceeded", stopped.exception.reason)
193
- self.assertIn("elapsed=10.0s", stopped.exception.detail)
194
- self.assertIn("test", stopped.exception.detail)
195
- self.assertIn("lint", stopped.exception.detail)
196
-
197
- def test_already_merged_pr_bypasses_check_wait(self):
198
- snapshot = pr_snapshot(
199
- state="MERGED", mergeable="UNKNOWN", mergeStateStatus="UNKNOWN"
200
- )
201
- clock = FakeClock()
202
- commands = []
203
-
204
- def runner(cmd, **_kwargs):
205
- commands.append(cmd)
206
- return completed(snapshot)
207
-
208
- already_merged = self.wrapup.wait_for_merge_gate(
209
- "42",
210
- command_runner=runner,
211
- clock=clock.monotonic,
212
- sleeper=clock.sleep,
213
- progress_stream=io.StringIO(),
214
- configured_required_names={"test"},
215
- )
216
-
217
- self.assertTrue(already_merged)
218
- self.assertEqual(clock.now, 0)
219
- self.assertEqual(len(commands), 1)
220
-
221
- def test_optional_red_check_does_not_block_when_required_check_is_green(self):
222
- snapshot = pr_snapshot(
223
- mergeStateStatus="BLOCKED",
224
- statusCheckRollup=[{
225
- "name": "advisory-browser",
226
- "status": "COMPLETED",
227
- "conclusion": "FAILURE",
228
- }],
229
- )
230
- runner = gate_runner([snapshot], [[{
231
- "name": "test",
232
- "state": "SUCCESS",
233
- "link": "https://example.test",
234
- }]])
235
-
236
- self.assertFalse(self.wrapup.wait_for_merge_gate(
237
- "42", command_runner=runner, configured_required_names={"test"}
238
- ))
239
-
240
- def test_fresh_pr_waits_until_required_checks_become_visible(self):
241
- snapshots = [
242
- pr_snapshot(mergeable="UNKNOWN", mergeStateStatus="BLOCKED"),
243
- pr_snapshot(mergeable="UNKNOWN", mergeStateStatus="BLOCKED"),
244
- pr_snapshot(),
245
- ]
246
- required = [
247
- [],
248
- [{"name": "test", "state": "PENDING"}],
249
- [{"name": "test", "state": "SUCCESS"}],
250
- ]
251
- clock = FakeClock()
252
- progress = io.StringIO()
253
-
254
- self.assertFalse(self.wrapup.wait_for_merge_gate(
255
- "42",
256
- timeout_seconds=30,
257
- poll_interval=5,
258
- command_runner=gate_runner(snapshots, required),
259
- clock=clock.monotonic,
260
- sleeper=clock.sleep,
261
- progress_stream=progress,
262
- configured_required_names={"test"},
263
- ))
264
- self.assertIn("test (awaiting discovery)", progress.getvalue())
265
- self.assertIn("test", progress.getvalue())
266
-
267
- def test_visible_optional_check_cannot_mask_required_discovery(self):
268
- snapshot = pr_snapshot(
269
- mergeStateStatus="BLOCKED",
270
- statusCheckRollup=[{
271
- "name": "advisory-browser",
272
- "status": "COMPLETED",
273
- "conclusion": "FAILURE",
274
- }],
275
- )
276
-
277
- clock = FakeClock()
278
- with self.assertRaises(self.wrapup.Stop) as stopped:
279
- self.wrapup.wait_for_merge_gate(
280
- "42",
281
- timeout_seconds=0,
282
- command_runner=gate_runner([snapshot], [[]]),
283
- clock=clock.monotonic,
284
- sleeper=clock.sleep,
285
- progress_stream=io.StringIO(),
286
- configured_required_names={"test"},
287
- )
288
- self.assertIn("test (awaiting discovery)", stopped.exception.detail)
289
-
290
- def test_zero_step_failed_job_is_named_as_infrastructure_failure(self):
291
- snapshot = pr_snapshot(mergeStateStatus="BLOCKED")
292
- check = {
293
- "name": "test",
294
- "state": "FAILURE",
295
- "link": "https://github.com/acme/repo/actions/runs/123/job/456",
296
- }
297
- commands = []
298
-
299
- def runner(cmd, **_kwargs):
300
- commands.append(cmd)
301
- if cmd[:3] == ["gh", "pr", "view"]:
302
- return completed(snapshot)
303
- if cmd[:3] == ["gh", "pr", "checks"]:
304
- return completed([check], returncode=1)
305
- if "--json" in cmd:
306
- return completed({
307
- "jobs": [{
308
- "databaseId": 456,
309
- "name": "test",
310
- "conclusion": "failure",
311
- "steps": [],
312
- }],
313
- })
314
- return subprocess.CompletedProcess(
315
- cmd, 1, "", "log unavailable"
316
- )
317
-
318
- with self.assertRaises(self.wrapup.Stop) as stopped:
319
- self.wrapup.wait_for_merge_gate(
320
- "42", command_runner=runner, configured_required_names={"test"}
321
- )
322
-
323
- self.assertIn("test", stopped.exception.detail)
324
- self.assertIn("infrastructure failure", stopped.exception.detail)
325
- self.assertIn(["gh", "run", "view", "123", "--json", "jobs"], commands)
326
- self.assertEqual(
327
- [cmd[:3] for cmd in commands].count(["gh", "run", "view"]), 1
328
- )
329
-
330
- def test_billing_annotation_is_sanitized_and_bounded(self):
331
- noisy = (
332
- "The job was not started because recent account payments have failed. "
333
- + "TOKEN=secret "
334
- + "x" * 1000
335
- )
336
- check = {
337
- "name": "test",
338
- "state": "FAILURE",
339
- "link": "https://github.com/acme/repo/actions/runs/123/job/456",
340
- }
341
-
342
- def runner(cmd, **_kwargs):
343
- if "--json" in cmd:
344
- return completed({"jobs": [{
345
- "databaseId": 456,
346
- "name": "test",
347
- "conclusion": "failure",
348
- "steps": [{"name": "x"}],
349
- }]})
350
- self.assertIn("--job", cmd)
351
- self.assertIn("456", cmd)
352
- return subprocess.CompletedProcess(cmd, 0, noisy, "")
353
-
354
- diagnosis = self.wrapup.infrastructure_failure_diagnosis(
355
- check, command_runner=runner
356
- )
357
-
358
- self.assertIn("infrastructure failure", diagnosis)
359
- self.assertLessEqual(len(diagnosis), self.wrapup.MAX_EXTERNAL_DETAIL)
360
- self.assertNotIn("\n", diagnosis)
361
-
362
- def test_mixed_run_does_not_misclassify_real_test_failure_as_infra(self):
363
- check = {
364
- "name": "test",
365
- "state": "FAILURE",
366
- "link": "https://github.com/acme/repo/actions/runs/123/job/456",
367
- }
368
- commands = []
369
-
370
- def runner(cmd, **_kwargs):
371
- commands.append(cmd)
372
- if "--json" in cmd:
373
- return completed({"jobs": [
374
- {
375
- "databaseId": 111,
376
- "name": "setup",
377
- "conclusion": "startup_failure",
378
- "steps": [],
379
- },
380
- {
381
- "databaseId": 456,
382
- "name": "test",
383
- "conclusion": "failure",
384
- "steps": [{"name": "Run tests", "conclusion": "failure"}],
385
- },
386
- ]})
387
- return subprocess.CompletedProcess(
388
- cmd, 0, "AssertionError: expected green", ""
389
- )
390
-
391
- diagnosis = self.wrapup.infrastructure_failure_diagnosis(
392
- check, command_runner=runner
393
- )
394
-
395
- self.assertEqual(diagnosis, "")
396
- self.assertIn(
397
- ["gh", "run", "view", "123", "--job", "456", "--log-failed"],
398
- commands,
399
- )
400
-
401
- def test_check_without_unique_job_match_is_not_classified_from_run_logs(self):
402
- check = {
403
- "name": "test",
404
- "state": "FAILURE",
405
- "link": "https://github.com/acme/repo/actions/runs/123",
406
- }
407
- commands = []
408
-
409
- def runner(cmd, **_kwargs):
410
- commands.append(cmd)
411
- if "--json" in cmd:
412
- return completed({"jobs": [
413
- {"name": "test", "conclusion": "failure", "steps": []},
414
- {"name": "test", "conclusion": "failure", "steps": []},
415
- ]})
416
- raise AssertionError("aggregate run logs must not be inspected")
417
-
418
- self.assertEqual(
419
- self.wrapup.infrastructure_failure_diagnosis(
420
- check, command_runner=runner
421
- ),
422
- "",
423
- )
424
- self.assertEqual(len(commands), 1)
425
-
426
-
427
- if __name__ == "__main__":
428
- unittest.main()