| 1 | """Subject-kind plugin layer: schema, capability gaps, and the parity proof.
|
| 2 |
|
| 3 | The load-bearing assertion is parity_report == [] for git and oak: the
|
| 4 | plugin JSONs are a parallel read of config/command_semantics.json, and an
|
| 5 | empty report proves the extraction is measurement-identical, so existing
|
| 6 | lanes' behavior cannot have changed.
|
| 7 | """
|
| 8 |
|
| 9 | from __future__ import annotations
|
| 10 |
|
| 11 | import sys
|
| 12 | import unittest
|
| 13 | from pathlib import Path
|
| 14 |
|
| 15 | ROOT = Path(__file__).resolve().parents[1]
|
| 16 | sys.path.insert(0, str(ROOT / "scripts"))
|
| 17 |
|
| 18 | from oakbench.subject_kinds import (
|
| 19 | SCHEMA_VERSION,
|
| 20 | available_kinds,
|
| 21 | command_for,
|
| 22 | load_subject_kind,
|
| 23 | parity_report,
|
| 24 | )
|
| 25 |
|
| 26 |
|
| 27 | class ParityProofTest(unittest.TestCase):
|
| 28 | """The row_parity proof for the config layer (empty = identical)."""
|
| 29 |
|
| 30 | def test_git_plugin_is_measurement_identical_to_command_semantics(self) -> None:
|
| 31 | self.assertEqual(parity_report("git", ROOT / "config" / "command_semantics.json"), [])
|
| 32 |
|
| 33 | def test_oak_plugin_is_measurement_identical_to_command_semantics(self) -> None:
|
| 34 | self.assertEqual(parity_report("oak", ROOT / "config" / "command_semantics.json"), [])
|
| 35 |
|
| 36 | def test_jj_is_honestly_absent_from_command_semantics(self) -> None:
|
| 37 | report = parity_report("jj", ROOT / "config" / "command_semantics.json")
|
| 38 | self.assertEqual(len(report), 1)
|
| 39 | self.assertIn("kind_not_in_command_semantics:jj", report[0])
|
| 40 |
|
| 41 |
|
| 42 | class SchemaTest(unittest.TestCase):
|
| 43 | def test_all_shipped_plugins_load_and_validate(self) -> None:
|
| 44 | kinds = available_kinds()
|
| 45 | self.assertEqual(kinds, ["git", "jj", "oak"])
|
| 46 | for kind in kinds:
|
| 47 | plugin = load_subject_kind(kind)
|
| 48 | self.assertEqual(plugin.kind, kind)
|
| 49 | self.assertEqual(plugin.schema_version, SCHEMA_VERSION)
|
| 50 | self.assertIsInstance(plugin.workspace_model, str)
|
| 51 | self.assertIn("stage_required", plugin.snapshot_call_semantics)
|
| 52 | self.assertIn("calls_per_snapshot", plugin.snapshot_call_semantics)
|
| 53 |
|
| 54 | def test_workspace_models_match_declarations(self) -> None:
|
| 55 | self.assertEqual(load_subject_kind("git").workspace_model, "working_copy")
|
| 56 | self.assertEqual(load_subject_kind("oak").workspace_model, "virtual_branch")
|
| 57 | self.assertEqual(load_subject_kind("jj").workspace_model, "working_copy_autosnapshot")
|
| 58 |
|
| 59 | def test_remote_env_vars_match_harness(self) -> None:
|
| 60 | self.assertEqual(load_subject_kind("git").remote_env_var, "GIT_BENCH_REMOTE")
|
| 61 | self.assertEqual(load_subject_kind("oak").remote_env_var, "OAK_BENCH_REMOTE")
|
| 62 | self.assertIsNone(load_subject_kind("jj").remote_env_var)
|
| 63 |
|
| 64 |
|
| 65 | class CapabilityGapTest(unittest.TestCase):
|
| 66 | def test_jj_status_has_argv(self) -> None:
|
| 67 | self.assertEqual(command_for("jj", "status"), ["st"])
|
| 68 |
|
| 69 | def test_jj_absent_operation_is_capability_gap(self) -> None:
|
| 70 | self.assertIsNone(command_for("jj", "rebase.interactive"))
|
| 71 |
|
| 72 | def test_jj_explicit_null_is_capability_gap(self) -> None:
|
| 73 | plugin = load_subject_kind("jj")
|
| 74 | self.assertIsNone(plugin.command_for("stage"))
|
| 75 | self.assertIsNone(plugin.command_for("branch_create"))
|
| 76 | self.assertEqual(plugin.skip_reason("stage"), "capability_gap:jj:stage")
|
| 77 |
|
| 78 | def test_track_selection(self) -> None:
|
| 79 | self.assertEqual(command_for("git", "status", track="core-equivalent"), ["status", "--porcelain=v1"])
|
| 80 | self.assertIsNone(command_for("jj", "status", track="core-equivalent"))
|
| 81 |
|
| 82 | def test_oak_stage_gap_matches_command_semantics(self) -> None:
|
| 83 | self.assertIsNone(command_for("oak", "stage"))
|
| 84 |
|
| 85 |
|
| 86 | class UnknownKindTest(unittest.TestCase):
|
| 87 | def test_unknown_kind_raises_clear_error(self) -> None:
|
| 88 | with self.assertRaises(ValueError) as ctx:
|
| 89 | load_subject_kind("sapling")
|
| 90 | message = str(ctx.exception)
|
| 91 | self.assertIn("sapling", message)
|
| 92 | self.assertIn("available", message)
|
| 93 | self.assertIn("git", message)
|
| 94 |
|
| 95 |
|
| 96 | if __name__ == "__main__":
|
| 97 | unittest.main()
|