diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8add45c..10c3426 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] env: PYTHONIOENCODING: utf-8 diff --git a/check_commands.py b/check_commands.py index 45d43ec..a210451 100644 --- a/check_commands.py +++ b/check_commands.py @@ -94,12 +94,22 @@ EXTERNAL_SCRIPTS = { "blutter's own environment script, run from inside the blutter checkout -- the " "record's preceding line is `git clone --depth 1 worawit/blutter` " "(docs/tool-verification/TOOL-VERDICTS.md)", + # Both spellings of the same upstream entry point are declared, because the two records write + # it differently and each spelling has to stand on its own: `./dcc.py` (invoked from inside its + # checkout) and the bare `dcc.py` after a `cd` into it. Both are here for the same reason -- + # the `cd`-then-bare-name form only skips while the gitignored `tools/` tree happens to exist, + # so recognition by filename alone reported drift on a clean checkout. './dcc.py': "the entry point of the dcc checkout, invoked as `./dcc.py` from inside it after " "`cd tools/_work/bench/repos/dcc` -- the `./` is what says so, and this repository " - "ships no dcc.py. Declared rather than inferred: the `cd`-then-bare-name form only " - "skips while the gitignored tools/ tree happens to exist, so on a clean checkout the " - "same line degraded to drift (docs/tool-verification/EXTENSION-reconstruction.md)", + "ships no dcc.py (docs/tool-verification/EXTENSION-reconstruction.md)", + 'dcc.py': + "the dcc tool's own entry point, run from inside its checkout -- the record's " + "preceding line is `cd tools/_work/bench/repos/dcc`, and this repository ships " + "no dcc.py (docs/tool-verification/EXTENSION-java2c.md). Declared rather than " + "inferred because the `cd`-then-bare-name form only skips as workbench-after-cd " + "while the gitignored tools/ tree happens to exist locally; a clean checkout has " + "no such file to recognise, and without this entry the gate reported drift there.", } # Commands an evidence record *quotes* rather than offers. The key is (document, diff --git a/tests/integration/test_check_commands_gate.py b/tests/integration/test_check_commands_gate.py index 355202d..7337b79 100644 --- a/tests/integration/test_check_commands_gate.py +++ b/tests/integration/test_check_commands_gate.py @@ -148,6 +148,39 @@ def test_a_bare_name_after_cd_with_no_resolution_is_skipped_not_drift(gate, fake assert "after-cd-unresolved" in kinds, findings +def test_the_external_dcc_entry_point_is_never_drift(gate): + """`python dcc.py` after `cd tools/_work/bench/repos/dcc` runs inside the external dcc + checkout, so it is a skip on every machine -- including one without the workbench. + + The name is declared in EXTERNAL_SCRIPTS because recognition by filename needs `tools/` to + exist: on a clean checkout `dcc.py` degraded to `missing-script` drift and the gate failed away + from the machine that wrote the record. Asserted as a *property* rather than as one specific + reason, because two mechanisms can legitimately produce it -- the declared entry point and the + workbench-after-`cd` rule -- and pinning either one alone makes the test machine-dependent, + which is the very defect it is guarding against. + """ + doc = str(REPO_ROOT / "docs" / "tool-verification" / "EXTENSION-java2c.md") + _path, why = gate.resolve_script("dcc.py", doc, include_workbench=False, chdir=True) + assert why in ("external", "workbench-after-cd"), why + + text = "```\n$ cd tools/_work/bench/repos/dcc\n$ python dcc.py --no-build\n```\n" + findings = gate.command_findings(doc, text, {}, False) + kinds = [f["kind"] for f in findings] + assert "missing-script" not in kinds, findings + assert "unqualified-workbench" not in kinds, findings + assert any(k in ("workbench-after-cd", "external") for k in kinds), findings + + +def test_a_shipped_script_resolves_from_the_repository_root(gate): + """The order that matters: root, then the skill's scripts, then the document's own directory.""" + doc = str(REPO_ROOT / "docs" / "tool-verification" / "README.md") + path, why = gate.resolve_script("check_repo.py", doc, include_workbench=False) + assert why == "ok" and path and path.endswith("check_repo.py") + path, why = gate.resolve_script("skills/apk-reverse/scripts/scan_leaks.py", doc, + include_workbench=False) + assert why == "ok" and path and path.endswith("scan_leaks.py") + + def test_the_committed_tree_reports_no_drift(gate, tmp_path): """The gate's own verdict on this repository, run in-process so it cannot drift silently.""" docs = gate.collect_docs(None)