8OI-502 / FS-502 — MCP contract parity harness. The validator's
9``_TOOL_MODEL_MAP`` and the executor's ``_ARGV_BUILDERS`` must register
10the same set of tool names, and each registered tool must be executable
11against a realistic validated-args dict without tripping the executor's
12``.get()`` defaults into producing a CLI flag that the real ``oct`` CLI
15The test exercises three invariants:
171. **Registry parity** — ``set(_TOOL_MODEL_MAP) == set(_ARGV_BUILDERS)``.
182. **Per-tool argv well-formed** — for every tool, instantiate the model
19 with sensible defaults, call its argv builder, and check the first
20 four elements match ``[sys.executable, "-m", "oct", <subcommand>]``.
213. **No spurious flags** — for a curated list of previously-drifting
22 flags (``--output-dir``, ``--check``, ``--jobs`` on ``oct test``,
23 ``--json`` on ``oct diag``) the argv must *not* contain the flag.
24 Regression coverage for the bugs this harness was introduced to fix.
26The harness is intentionally static — it does not spawn the ``oct`` CLI
27or compare against ``--help`` output, so it runs fast and works even in
28environments where click or pytest are missing.
32- Verify registry parity between ``_TOOL_MODEL_MAP`` and
34- Verify per-tool argv starts with
35 ``[sys.executable, "-m", "oct", <subcommand>]``.
36- Verify previously-drifting flags do not appear in builder output.
43 L3 — assertion details
48- The parity test must fail if a new tool is added to one registry and
50- The spurious-flag checks must fail if a previously-fixed executor
51 regression is re-introduced.
54from __future__
import annotations
57from pathlib
import Path
67_MINIMAL_ARGS: dict[str, dict] = {
77 "oct_format": {
"apply":
False,
"confirm":
False},
79 "oct_export_source": {},
80 "oct_new": {
"path":
"src/foo.py"},
81 "oct_scaffold": {
"name":
"example"},
83 "oct_install_hooks": {},
84 "oct_git_commit": {
"message":
"feat(core): add parity test"},
85 "oct_git_hooks_install": {},
87 "oct_git_changelog": {},
93_EXPECTED_SUBCOMMAND: dict[str, list[str]] = {
95 "oct_health": [
"health"],
96 "oct_skeleton": [
"export-skeleton"],
99 "oct_typecheck": [
"typecheck"],
100 "oct_diag": [
"diag",
"validate-config"],
101 "oct_git_status": [
"git",
"status"],
102 "oct_git_check": [
"git",
"check"],
103 "oct_format": [
"format"],
104 "oct_docs": [
"docs"],
105 "oct_export_source": [
"export-source"],
107 "oct_scaffold": [
"scaffold"],
108 "oct_clean": [
"clean"],
109 "oct_install_hooks": [
"git",
"hooks",
"install"],
110 "oct_git_commit": [
"git",
"commit"],
111 "oct_git_hooks_install": [
"git",
"hooks",
"install"],
112 "oct_git_init": [
"git",
"init"],
113 "oct_git_changelog": [
"git",
"changelog"],
118 """Every validator model must have a matching executor argv builder."""
119 model_names = set(_TOOL_MODEL_MAP)
120 builder_names = set(_ARGV_BUILDERS)
121 assert model_names == builder_names, (
122 f
"Validator/executor drift: "
123 f
"only in validator={model_names - builder_names!r}, "
124 f
"only in executor={builder_names - model_names!r}"
129 """_MINIMAL_ARGS must cover every registered tool."""
130 assert set(_MINIMAL_ARGS) == set(_TOOL_MODEL_MAP), (
131 "Update _MINIMAL_ARGS to mirror validator._TOOL_MODEL_MAP"
135@pytest.mark.parametrize("tool_name", sorted(_TOOL_MODEL_MAP))
137 """Minimal args dict must validate against the registered model."""
138 raw = _MINIMAL_ARGS[tool_name]
139 model = validate_tool_args(tool_name, raw, project_root=tmp_path)
140 assert model
is not None
143@pytest.mark.parametrize("tool_name", sorted(_TOOL_MODEL_MAP))
145 """Builder output must begin with python -m oct <subcommand>."""
146 raw = _MINIMAL_ARGS[tool_name]
147 model = validate_tool_args(tool_name, raw, project_root=tmp_path)
148 cmd = _ARGV_BUILDERS[tool_name](model.model_dump(), tmp_path)
150 assert cmd[0] == sys.executable
151 assert cmd[1] ==
"-m"
152 assert cmd[2] ==
"oct"
154 expected_tail = _EXPECTED_SUBCOMMAND[tool_name]
155 assert cmd[3 : 3 + len(expected_tail)] == expected_tail, (
156 f
"{tool_name}: expected subcommand tail {expected_tail!r}, "
157 f
"got {cmd[3 : 3 + len(expected_tail)]!r} (full cmd={cmd!r})"
167 model = validate_tool_args(
168 tool_name, _MINIMAL_ARGS[tool_name], project_root=tmp_path
170 return _ARGV_BUILDERS[tool_name](model.model_dump(), tmp_path)
174 """OI-502: CLI ``oct export-skeleton`` does not accept ``--output-dir``."""
176 assert "--output-dir" not in cmd
180 """OI-502: CLI ``oct deps`` does not accept ``--check``."""
182 assert "--check" not in cmd
186 """OI-502: CLI ``oct test`` does not accept ``--jobs``."""
188 assert "--jobs" not in cmd
192 """OI-502: no ``oct diag`` subcommand accepts ``--json`` (yet)."""
194 assert "--json" not in cmd
198 """OI-502: ``oct format`` argparser does not register ``--jobs``."""
200 assert "--jobs" not in cmd
208@pytest.mark.parametrize(
211 "feat(core): add parity harness",
212 "fix(mcp): close OI-502",
213 "feat!: drop legacy config",
214 "refactor(git)!: rename internal helper",
218 message: str, tmp_path: Path,
220 """OI-502 / FS-502 — scoped and breaking commits must round-trip."""
221 model = validate_tool_args(
222 "oct_git_commit", {
"message": message}, project_root=tmp_path,
224 assert model.message == message
225 cmd = _ARGV_BUILDERS[
"oct_git_commit"](model.model_dump(), tmp_path)
228 assert message
in cmd
test_diag_does_not_emit_json(Path tmp_path)
test_model_validates_minimal_args(str tool_name, Path tmp_path)
test_argv_starts_with_expected_subcommand(str tool_name, Path tmp_path)
test_minimal_args_covers_all_tools()
test_test_does_not_emit_jobs(Path tmp_path)
list[str] _build_cmd(str tool_name, Path tmp_path)
test_deps_does_not_emit_check(Path tmp_path)
test_git_commit_accepts_valid_conventional_messages(str message, Path tmp_path)
test_format_does_not_emit_jobs(Path tmp_path)
test_skeleton_does_not_emit_output_dir(Path tmp_path)