diff --git a/.github/workflows/composed.yml b/.github/workflows/composed.yml new file mode 100644 index 0000000..ac49d82 --- /dev/null +++ b/.github/workflows/composed.yml @@ -0,0 +1,442 @@ +name: composed + +# THE COMPOSED RUN, PERMANENT (plan 038 task T029, U-9, in openxFactory +# `specs/038-opendox-document-tool-self-maintenance/`). It runs F12.1, the +# falsifier of #1144's 12.5 ("the governed flow is unchanged"), where it can +# run: composed with openxFactory, as F5.2 runs under R1Q23 (a). In a lone +# checkout 15 of 12.5's 16 governed suites cannot even be collected, because +# they reach openxFactory's `doc_health`. ARC-Q2 (a) (openxFactory#656 comment +# 6003918488) makes this composition their permanent home (CF-5). Decision +# N-7a gives the run its own file and its own pin, apart from `validate.yml`. +# Both were ruled with plan 038 (comment 6013547504). +# +# WHAT IT RUNS, IN ORDER. +# 1. This checkout, with its whole history, because the oracle reads the +# arc's landings since ARC_BASE. +# 2. The package and its test extra, in a fresh venv, with `validate.yml`'s +# install command. So openDox arrives at the pin `pyproject.toml` names, +# and F9.2's pin assertion holds it there. +# 3. openxFactory, at the ONE commit `tests/composed_host_pin.yaml` names, +# with `openDox`, `openXdox` and `openXwallet` initialized recursively. +# Without them, `corpus_adapter_openxfactory` refuses at import. The run +# initializes ONLY those three. openxFactory's fourth submodule, +# `installs/omnigent-install`, is a private repository that this run never +# imports and that this repository's token cannot read, so a blanket +# recursive init would fail before any test ran. +# 4. T022's `scripts/composed_placements.py`, which links the composed tree's +# session runbook into `docs/` for the run. +# 5. 12.5's governed suites, computed the way its falsifier computes them +# (16 is the floor). Each suite runs alone, with +# `PYTHONPATH="$OPENXFACTORY/scripts"` (R1Q23 (a)'s form). +# 5a. The declaration's other entries (plan 038 T073; ARC-Q2 (a), +# openxFactory#656 comment 6003918488). Every file of +# `tests/declared_exclusion.yaml` is a declared integration test that +# this workflow runs. The governed suites among them ran in step 5, and +# this step runs every other entry in the same form, each alone. It +# refuses an entry with no report from either step, so none is dropped +# from both suites. The rail the R1Q24 rail files need is registered by +# `tests/conftest.py` where the run is composed. +# 6. The oracle, `scripts/protected_suites.py`, in 12.5's call as batch Q +# amends it (`--chains`): no landing of the arc touched a governed suite +# outside allow-list entries that hold. +# 7. The assembled `--help` tree, against the golden it records (R2Q3 (a): +# the hosts' help goldens are unchanged). `validate.yml` leaves that test +# out (`LEFT_OUT`), because building the tree needs `doc_health`. Composed, +# it can run. +# Steps 5a, 6 and 7 run even when step 5 is red, so one run reports every red. +# +# WHAT IS NOT HERE. Making `composed` a REQUIRED check is the holder's act, +# after this workflow's first green run on `main` (N-7b (a), ruled +# 6013547504), and it is recorded in T029's evidence. This file changes no +# repository setting. + +on: + pull_request: + # `edited` beside the default three: the oracle reads the pull request's + # body for the arc's trailer line, so an edited body is judged again. + types: [opened, synchronize, reopened, edited] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + composed: + runs-on: ubuntu-latest + timeout-minutes: 60 + defaults: + run: + shell: bash + working-directory: openXdox-code + env: + # The openxFactory checkout of the composed run, beside this one. + OPENXFACTORY: ${{ github.workspace }}/openxFactory + # openXdox-code's ARC_BASE, as plan 034 T003 recorded it (openxFactory + # `specs/034-opendox-standalone-operation/evidence/arc-base.md`). The + # oracle's landings are the commits from it to HEAD that carry the + # arc's trailer. + ARC_BASE: e28930bf052febff0c7bdd0463a72ce3c4f5e8fe + steps: + - uses: actions/checkout@v4 + with: + path: openXdox-code + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + # A fresh venv, as 12.5's falsifier makes one. Its `bin/` goes on PATH, + # so every later step's `python` is this one. + - name: install the package and its test extra, openDox at the pin + id: install + run: | + echo "COMPOSED=$RUNNER_TEMP/composed" >> "$GITHUB_ENV" + mkdir -p "$RUNNER_TEMP/composed" + python -m venv --clear "$RUNNER_TEMP/venv" + echo "$RUNNER_TEMP/venv/bin" >> "$GITHUB_PATH" + "$RUNNER_TEMP/venv/bin/python" -m pip install -e ".[test]" + # F9.2's pin assertion: the composition names a full-commit pin, and + # the openDox installed is that one. + "$RUNNER_TEMP/venv/bin/python" - <<'PY' + import importlib.metadata as md, json, pathlib, re, tomllib + deps = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["dependencies"] + pins = [d for d in deps if re.match(r"opendox\s*@", d)] + assert len(pins) == 1 and re.search(r"@[0-9a-f]{40}$", pins[0]), f"the composition names no full-commit pin: {pins}" + got = json.loads(md.distribution("opendox").read_text("direct_url.json") or "{}").get("vcs_info", {}).get("commit_id") + assert got == pins[0].rsplit("@", 1)[1], f"the installed openDox is {got!r}, not the pinned commit" + print(f"openDox installed at the pin: openDox-code@{got}") + PY + + # The pin file's rules are in its own header. A file that breaks one is + # refused, naming the rule, and nothing is checked out. + - name: read the host pin + id: pin + run: | + python - <<'PY' + import os, re, sys + import yaml + + PIN = "tests/composed_host_pin.yaml" + + + def refuse(why): + sys.exit(f"::error file={PIN}::{PIN} is refused: {why}") + + + class Strict(yaml.SafeLoader): + """Refuses a key given twice, which YAML would quietly collapse.""" + + + def mapping(loader, node, deep=False): + keys = [loader.construct_object(key, deep=deep) for key, _ in node.value] + twice = sorted({str(key) for key in keys if keys.count(key) > 1}) + if twice: + raise yaml.constructor.ConstructorError( + None, None, f"a key is given twice: {twice}", node.start_mark) + return loader.construct_mapping(node, deep=deep) + + + Strict.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, mapping) + try: + with open(PIN, encoding="utf-8") as handle: + pin = yaml.load(handle, Loader=Strict) + except (OSError, yaml.YAMLError) as exc: + refuse(f"it cannot be read as YAML ({exc})") + if not isinstance(pin, dict): + refuse("it is not a mapping") + keys = {"schema_version", "kind", "commit"} + if set(pin) != keys: + refuse(f"its keys are {sorted(map(str, pin))}, and they must be exactly {sorted(keys)}") + if type(pin["schema_version"]) is not int or pin["schema_version"] != 1: + refuse(f"schema_version is {pin['schema_version']!r}, not the integer 1") + if pin["kind"] != "composed-host-pin": + refuse(f"kind is {pin['kind']!r}, not 'composed-host-pin'") + commit = pin["commit"] + if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-f]{40}", commit): + refuse(f"commit is {commit!r}, not a full 40-character lowercase commit id") + for name, value in (("GITHUB_OUTPUT", f"commit={commit}"), + ("GITHUB_ENV", f"COMPOSED_HOST_COMMIT={commit}")): + with open(os.environ[name], "a", encoding="utf-8") as handle: + handle.write(value + "\n") + print(f"the composed run's host: opensoft/openxFactory@{commit}") + PY + + - uses: actions/checkout@v4 + with: + repository: opensoft/openxFactory + ref: ${{ steps.pin.outputs.commit }} + path: openxFactory + fetch-depth: 0 + persist-credentials: false + + # openxFactory's `.gitmodules` spells these three as ssh URLs, and this + # run holds no ssh key. All three, and the legs nested in them, are + # public, so the URLs are read over https. They are rewritten in this + # checkout's own `.git/config`, which `git submodule init` writes, and + # `.gitmodules` is not touched. + - name: initialize openDox, openXdox and openXwallet, recursively + id: submodules + working-directory: openxFactory + run: | + paths=(openDox openXdox openXwallet) + git submodule init -- "${paths[@]}" + for path in "${paths[@]}"; do + url=$(git config --get "submodule.$path.url") + case "$url" in + git@github.com:*) git config "submodule.$path.url" "https://github.com/${url#git@github.com:}" ;; + esac + done + git submodule update --init --recursive -- "${paths[@]}" + head=$(git rev-parse HEAD) + if [ "$head" != "$COMPOSED_HOST_COMMIT" ]; then + echo "::error::the openxFactory checkout is at $head, and tests/composed_host_pin.yaml pins $COMPOSED_HOST_COMMIT" + exit 1 + fi + echo "OPENXFACTORY (an openxFactory checkout) at $head" + git submodule status --recursive -- "${paths[@]}" + + - name: place the composed tree's files (T022) + id: placements + run: python scripts/composed_placements.py --openxfactory "$OPENXFACTORY" + + # 12.5's falsifier computes the set and holds it to a floor: every suite + # that drives `open-pr` or injects the fake. Fewer than 16 means a proof + # vanished. + - name: the governed set, computed + id: governed + run: | + status=0 + git grep -l -e 'open-pr' -e 'open_pr' -e 'FakePullRequests' -- 'tests/test_*.py' > "$COMPOSED/governed.txt" || status=$? + # git grep exits 1 when nothing matches, which the floor below names. + if [ "$status" -gt 1 ]; then + echo "::error::git grep could not read this checkout (exit $status)" + exit "$status" + fi + count=$(wc -l < "$COMPOSED/governed.txt") + echo "governed suites: $count (the floor is 16)" + cat "$COMPOSED/governed.txt" + if [ "$count" -lt 16 ]; then + echo "::error::12.5's governed set has $count suites, below its floor of 16: a proof vanished" + exit 1 + fi + + # Each suite runs alone, as 12.5's falsifier runs it. pytest's stdin is + # /dev/null, so no test can read the list this loop reads. Every suite + # runs whatever the one before it did, and the step fails at the end if + # any of them did. + - name: the governed suites, composed, each alone + id: suites + run: | + export PYTHONPATH="$OPENXFACTORY/scripts" + echo "OPENXFACTORY (an openxFactory checkout) at $(git -C "$OPENXFACTORY" rev-parse HEAD)" + echo "python $(python -c 'import platform; print(platform.python_version())'), $(python -m pytest --version 2>&1), node $(node --version 2>&1)" + mkdir -p "$COMPOSED/reports" + red=0 + while read -r suite <&3; do + echo "::group::$suite" + rc=0 + python -m pytest -q -p no:cacheprovider -rfE \ + --junitxml="$COMPOSED/reports/$(basename "$suite" .py).xml" "$suite" < /dev/null || rc=$? + echo "::endgroup::" + echo "$suite: pytest exit $rc" + if [ "$rc" -ne 0 ]; then red=1; fi + done 3< "$COMPOSED/governed.txt" + python - "$COMPOSED" <<'PY' + import os, pathlib, sys + import xml.etree.ElementTree as ET + + out = pathlib.Path(sys.argv[1]) + suites = [l.strip() for l in (out / "governed.txt").read_text().splitlines() if l.strip()] + rows, red, total = [], set(), dict.fromkeys(("passed", "failed", "errors", "skipped"), 0) + for suite in suites: + report = out / "reports" / (pathlib.PurePosixPath(suite).stem + ".xml") + if not report.exists(): + rows.append(f"{suite}: NO REPORT (pytest did not finish)") + red.add(suite) + continue + root = ET.parse(report).getroot() + n = dict.fromkeys(("tests", "failures", "errors", "skipped"), 0) + module = suite[:-3].replace("/", ".") + for ts in (root.iter("testsuite") if root.tag == "testsuites" else [root]): + for key in n: + n[key] += int(ts.get(key, 0)) + for case in ts.iter("testcase"): + if case.find("failure") is None and case.find("error") is None: + continue + cls, name = case.get("classname", ""), case.get("name", "") + if not cls: # a collection error names the file, not a test + red.add(suite) + continue + rest = cls[len(module):] if cls == module or cls.startswith(module + ".") else cls + red.add("::".join([suite, *[p for p in rest.split(".") if p], name])) + passed = n["tests"] - n["failures"] - n["errors"] - n["skipped"] + for key, value in (("passed", passed), ("failed", n["failures"]), + ("errors", n["errors"]), ("skipped", n["skipped"])): + total[key] += value + rows.append(f"{suite}: {passed} passed, {n['failures']} failed, " + f"{n['errors']} errors, {n['skipped']} skipped") + lines = [*rows, + f"TOTAL over {len(suites)} suites: {total['passed']} passed, " + f"{total['failed']} failed, {total['errors']} errors, {total['skipped']} skipped; " + f"{total['failed'] + total['errors']} red, in {len(red)} red nodes", + *(f"RED {node}" for node in sorted(red))] + print("\n".join(lines)) + (out / "red-nodes.txt").write_text("".join(f"{node}\n" for node in sorted(red))) + if os.environ.get("GITHUB_STEP_SUMMARY"): + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle: + handle.write("## 12.5's governed suites, composed\n\n```\n" + "\n".join(lines) + "\n```\n") + PY + exit "$red" + + # Step 5a (T073): the declaration's other entries, each alone, in step + # 5's form. The list is the declaration's own `entries`, read here, so an + # entry runs from the landing that lists it; the governed suites among + # them ran in step 5. Every entry must then have a report from one of the + # two steps, or it is refused as dropped from both suites. + - name: the declared integration tests, composed, each alone + id: declared + if: ${{ !cancelled() && steps.governed.outcome == 'success' }} + run: | + export PYTHONPATH="$OPENXFACTORY/scripts" + echo "the declaration's composed home: opensoft/openxFactory@$COMPOSED_HOST_COMMIT, the commit tests/composed_host_pin.yaml names" + python - "$COMPOSED" <<'PY' + import pathlib, sys + import yaml + + out = pathlib.Path(sys.argv[1]) + declaration = yaml.safe_load(pathlib.Path("tests/declared_exclusion.yaml").read_text(encoding="utf-8")) + entries = [entry["path"] for entry in declaration["entries"]] + governed = {line.strip() for line in (out / "governed.txt").read_text().splitlines() if line.strip()} + here = [path for path in entries if path not in governed] + (out / "declared.txt").write_text("".join(f"{path}\n" for path in here)) + print(f"declared integration tests: {len(entries)} (count {declaration['count']}); " + f"{len(entries) - len(here)} ran in step 5, {len(here)} run here") + PY + mkdir -p "$COMPOSED/declared-reports" + red=0 + while read -r test_file <&3; do + echo "::group::$test_file" + rc=0 + python -m pytest -q -p no:cacheprovider -rfE \ + --junitxml="$COMPOSED/declared-reports/$(basename "$test_file" .py).xml" "$test_file" < /dev/null || rc=$? + echo "::endgroup::" + echo "$test_file: pytest exit $rc" + if [ "$rc" -ne 0 ]; then red=1; fi + done 3< "$COMPOSED/declared.txt" + python - "$COMPOSED" <<'PY' || red=1 + import os, pathlib, sys + import xml.etree.ElementTree as ET + import yaml + + out = pathlib.Path(sys.argv[1]) + entries = [entry["path"] for entry in yaml.safe_load( + pathlib.Path("tests/declared_exclusion.yaml").read_text(encoding="utf-8"))["entries"]] + here = [line.strip() for line in (out / "declared.txt").read_text().splitlines() if line.strip()] + rows, red, total = [], set(), dict.fromkeys(("passed", "failed", "errors", "skipped"), 0) + for path in entries: + stem = pathlib.PurePosixPath(path).stem + if not any((out / where / f"{stem}.xml").exists() for where in ("reports", "declared-reports")): + rows.append(f"{path}: NOT RUN by step 5 or step 5a, so dropped from both suites") + red.add(path) + for path in here: + report = out / "declared-reports" / f"{pathlib.PurePosixPath(path).stem}.xml" + if not report.exists(): + continue + root = ET.parse(report).getroot() + n = dict.fromkeys(("tests", "failures", "errors", "skipped"), 0) + module = path[:-3].replace("/", ".") + for ts in (root.iter("testsuite") if root.tag == "testsuites" else [root]): + for key in n: + n[key] += int(ts.get(key, 0)) + for case in ts.iter("testcase"): + if case.find("failure") is None and case.find("error") is None: + continue + cls, name = case.get("classname", ""), case.get("name", "") + if not cls: # a collection error names the file, not a test + red.add(path) + continue + rest = cls[len(module):] if cls == module or cls.startswith(module + ".") else cls + red.add("::".join([path, *[p for p in rest.split(".") if p], name])) + passed = n["tests"] - n["failures"] - n["errors"] - n["skipped"] + for key, value in (("passed", passed), ("failed", n["failures"]), + ("errors", n["errors"]), ("skipped", n["skipped"])): + total[key] += value + rows.append(f"{path}: {passed} passed, {n['failures']} failed, " + f"{n['errors']} errors, {n['skipped']} skipped") + lines = [*rows, + f"TOTAL over {len(here)} declared integration tests: {total['passed']} passed, " + f"{total['failed']} failed, {total['errors']} errors, {total['skipped']} skipped; " + f"{total['failed'] + total['errors']} red, in {len(red)} red nodes", + *(f"RED {node}" for node in sorted(red))] + print("\n".join(lines)) + (out / "declared-red-nodes.txt").write_text("".join(f"{node}\n" for node in sorted(red))) + if os.environ.get("GITHUB_STEP_SUMMARY"): + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle: + handle.write("## The declared integration tests, composed\n\n```\n" + "\n".join(lines) + "\n```\n") + sys.exit(1 if red else 0) + PY + exit "$red" + + # 12.5's call, with `--chains`: Brett Heap ruled "Amend F12.1 with + # --chains (Recommended)" (openxFactory#656 comment 6016648451), which + # batch Q (T005) records at 12.5's falsifier. So several entries, chained + # by blob, may together admit one landing's edits to one governed suite, + # each still one edit inside its own named test or span, under batch K's + # rule unchanged. + # + # ON A PULL REQUEST, THE LANDING IT WOULD BE. GitHub checks out a merge + # commit whose first parent is the base, so the walk below finds only + # what has landed already, and an edit the pull request makes to a + # governed suite would be judged only once it is on `main`. The oracle + # judges a landing by its diff against its first parent and by the + # blobs on either side, so the merge commit IS the landing as it would + # be. It joins the landings where the pull request's body carries the + # arc's trailer line, which the lander copies into the landing's + # message. A body with no such line makes no arc landing, so 12.5 does + # not judge it. + - name: the oracle, protected_suites.py + id: oracle + if: ${{ !cancelled() && steps.governed.outcome == 'success' }} + env: + PR_BODY: ${{ github.event.pull_request.body }} + PR_HEAD: ${{ github.event.pull_request.head.sha }} + run: | + git log --first-parent --format=%H --grep='^Arc: neutral-product-standalone-operability$' "$ARC_BASE..HEAD" > "$COMPOSED/x-arc.txt" + if [ ! -s "$COMPOSED/x-arc.txt" ]; then + echo "::error::no landing since ARC_BASE $ARC_BASE carries the arc's trailer, so the trailer was dropped" + exit 1 + fi + echo "the arc's landings since ARC_BASE: $(wc -l < "$COMPOSED/x-arc.txt")" + if [ "$GITHUB_EVENT_NAME" = pull_request ]; then + if [ "$(git rev-parse --verify --quiet 'HEAD^2')" != "$PR_HEAD" ]; then + echo "::error::HEAD is not this pull request's merge commit (its second parent is not the head $PR_HEAD), so the change cannot be judged as the landing it would be" + exit 1 + fi + if python -c 'import os, sys; sys.exit("Arc: neutral-product-standalone-operability" not in os.environ.get("PR_BODY", "").replace("\r", "").split("\n"))'; then + git rev-parse HEAD >> "$COMPOSED/x-arc.txt" + echo "the pull request's body carries the arc's trailer, so its merge commit $(git rev-parse HEAD) is judged as the landing it would be" + else + echo "the pull request's body carries no arc trailer line, so its change is not an arc landing" + fi + fi + python scripts/protected_suites.py --chains --landings="$(cat "$COMPOSED/x-arc.txt")" --suites="$(cat "$COMPOSED/governed.txt")" + + # The test builds the tree in a child process. It hands the child this + # run's environment LESS `PYTHONPATH` (its `SCRUBBED_ENVIRONMENT`), so + # the composition of step 5 never reaches it, and the child needs + # `doc_health` (`cli_gate` imports `gate_console`, which imports it). So + # the composed tree's `scripts/` reaches the child through a `.pth` file + # in this run's own venv. The file is written here, after the governed + # suites have run in R1Q23 (a)'s form, and nothing runs after it. + - name: the assembled --help tree, against its recorded golden + id: help-tree + if: ${{ !cancelled() && steps.submodules.outcome == 'success' }} + run: | + purelib=$(python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') + printf '%s\n' "$OPENXFACTORY/scripts" > "$purelib/composed-openxfactory-scripts.pth" + python -m pytest -q -p no:cacheprovider -rfE \ + "tests/integration/test_assembled_surface.py::test_the_assembled_help_tree_is_the_32_entry_tree_the_manifest_records" < /dev/null diff --git a/tests/composed_host_pin.yaml b/tests/composed_host_pin.yaml new file mode 100644 index 0000000..6c5ba1b --- /dev/null +++ b/tests/composed_host_pin.yaml @@ -0,0 +1,33 @@ +# THE COMPOSED RUN'S HOST: the one openxFactory commit the composed workflow +# checks out (plan 038 task T029, U-9, in openxFactory +# `specs/038-opendox-document-tool-self-maintenance/`; decision N-7a, ruled +# with the plan on openxFactory#656 comment 6013547504). +# +# WHAT READS IT. `.github/workflows/composed.yml`, and nothing else. It checks +# openxFactory out at `commit`, initializes that checkout's `openDox`, +# `openXdox` and `openXwallet` submodules recursively, and runs 12.5's +# governed suites with that checkout's `scripts/` on `PYTHONPATH` (R1Q23 (a)'s +# form; F12.1 runs composed, CF-5). The workflow refuses the file, naming the +# rule, unless all of these hold: +# * `schema_version` is the integer 1, and `kind` is `composed-host-pin`; +# * `commit` is a full 40-character lowercase commit id; +# * no other key is present, and no key is given twice. +# The repository is openxFactory, always: the workflow names it, and this file +# names only the commit. +# +# WHEN IT MOVES. Never on its own: each advance is an openXdox-code pull +# request of its own, so the composed run always tests the host that +# openxFactory's `main` carries (plan 038 plan.md, "The composed workflow's +# openxFactory pin"). It advances after T030, T064 and T075 land in +# openxFactory, and T073 re-runs after each advance. +# +# ITS FIRST VALUE. openxFactory `main`'s head when T029 authored this file, +# read with `git ls-remote` at 2026-10-06T11:56:20Z: `92010d3e`, "Land +# add-factory-mcp-conformance on main as Speckit feature 037, with repairs +# and validator fixes (#1243)". +# +# A CREATED file: no row in openxFactory's `docs/opendox-carve-manifest.yaml` +# (RULED OQ-C). +schema_version: 1 +kind: composed-host-pin +commit: 92010d3e67f2c556687e1c3583ed3ca1867f4b8b diff --git a/tests/conftest.py b/tests/conftest.py index 44397eb..2fca5db 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1027,8 +1027,11 @@ def _the_hosts_seams_for_the_seam_suites(request): # * `/examples/ideation-dashboard/`: a link to each YAML example the # three owners keep under `examples/ideation-dashboard/`, at the same # relative path, less openDox's own `opendox-snapshot-*` examples, whose -# kind the family's validator does not serve. Two owners naming one path -# is refused, never resolved. +# kind the family's validator does not serve. Only openDox-spec's are left +# out: an example of that prefix from openxFactory or openXdox-spec is +# linked like any other (T073; Copilot r4201411760 on openXdox-code#46, +# the holder's r4201421102). Two owners naming one path is refused, never +# resolved. # It is OUTSIDE the checkout, in a temporary directory removed at exit: the # validator refuses a `contracts/` that links out of its tree (`:157-163`). A # temporary root inside the checkout (a TMPDIR, or `under`, beneath it) is @@ -1036,6 +1039,9 @@ def _the_hosts_seams_for_the_seam_suites(request): # The reach is read with `sys.path` saved and put back, and it must be the # composed tree's own. CONTRACT_EXAMPLE_LEFT_OUT = "opendox-snapshot-" +#: The one owner whose examples of that prefix are left out: openDox-spec, +#: as `carved_reach.MOUNTS` names it. +CONTRACT_EXAMPLE_LEFT_OUT_OWNER = "opendox_spec" def build_contracts_farm(reach, farm: Path) -> Path: @@ -1054,11 +1060,13 @@ def build_contracts_farm(reach, farm: Path) -> Path: examples = farm / "examples" / "ideation-dashboard" examples.mkdir(parents=True) linked: dict[Path, Path] = {} + left_out_from = reach.MOUNTS[CONTRACT_EXAMPLE_LEFT_OUT_OWNER] for owner in (reach.REPO_ROOT, reach.MOUNTS["opendox_spec"], reach.MOUNTS["openxdox_spec"]): base = owner / "examples" / "ideation-dashboard" for path in sorted(base.rglob("*.yaml")): - if path.name.startswith(CONTRACT_EXAMPLE_LEFT_OUT): + if (owner == left_out_from + and path.name.startswith(CONTRACT_EXAMPLE_LEFT_OUT)): continue relative = path.relative_to(base) if relative in linked: @@ -1119,3 +1127,102 @@ def runs_a_local_install(suite: str) -> bool: fixture stands in.""" return suite == _LAUNCH_SUITE or ( suite in LOCAL_INSTALL_SUITES and openxfactory_host() is not None) + + +# --------------------------------------------------------------------------- +# THE HOST'S STATUS-EXEMPTION RAIL FOR THE DECLARED RAIL SUITES (plan 038 +# T073; ARC-Q2 (a), openxFactory#656 comment 6003918488; R1Q24 (a), comment +# 5850003126). +# +# R1Q24 (a) declared the files that need openxFactory's status-exemption rail, +# which only openxFactory's host registers at openDox's status-exemption seam +# (`opendox.doxbench_packet.register_status_exemption`). In a lone checkout +# nothing registers it, so their cases refuse with +# `StatusExemptionNotRegistered`, which is the declaration's evidence for their +# reason. ARC-Q2 (a) makes them declared integration tests, which the composed +# workflow runs. So, as U-1 registers the governed host above, for the length +# of each test of a file `tests/declared_exclusion.yaml` names under the reason +# `status-exemption-rail`, this registers THE HOST'S RAIL where the run is +# composed, and puts the seam back afterwards. Where the run is not composed it +# registers nothing, and the lone checkout's evidence is unchanged. +# +# THE RAIL IS THE ONE openxFactory'S HOST REGISTERS: the +# `register_status_exemption` row of `opendox_host.seams()`, the table the +# host's process start registers from. It is read off that table rather than +# named here, so a later host pin is followed. +# +# THE LIST IS THE DECLARATION'S, as the root `conftest.py` has already held it +# to its rules: a file gets the rail, or stops getting it, only with its entry. +# +# THE SEAM IS PUT BACK. A rail held before the test is dropped for it. After +# it, a host's rail is registered again, and openDox's own default, an entry +# point's, is not, since the next entry point registers it again, as +# `a_hosts_plane` treats the default profile. +# --------------------------------------------------------------------------- + +STATUS_EXEMPTION_RAIL_REASON = "status-exemption-rail" + + +@functools.cache +def status_exemption_rail_suites() -> frozenset[str]: + """The test modules the declaration names under the rail's reason.""" + import yaml + + declaration = yaml.safe_load( + (HERE / "declared_exclusion.yaml").read_text(encoding="utf-8")) + return frozenset(Path(entry["path"]).name + for entry in declaration["entries"] + if STATUS_EXEMPTION_RAIL_REASON in entry["reasons"]) + + +@functools.cache +def openxfactory_status_exemption_rail(): + """openxFactory's status-exemption rail where the run is composed, and + None where it is not: what its host registers at openDox's seam, read off + the host's own seam table.""" + host_module = openxfactory_host() + if host_module is None: + return None + saved = list(sys.path) + try: + rails = [what for _, call, what in host_module.seams() + if call == "register_status_exemption"] + finally: + sys.path[:] = saved + _refuse_an_openxdox_from_elsewhere() + if len(rails) != 1: + raise RuntimeError( + f"openxFactory's host seam table names {len(rails)} " + "status-exemption rails, so the rail it registers is not one " + "this harness can register for it") + return rails[0] + + +@contextlib.contextmanager +def a_hosts_rail(rail): + """Register `rail` at openDox's status-exemption seam for the length of + the block, then put the seam back as it was, less openDox's default.""" + from opendox import doxbench_packet as _packet + + previous = getattr(_packet, "_status_exemption_rail", None) + a_hosts = previous is not None and not getattr( + _packet, "_status_exemption_is_default", False) + _packet.unregister_status_exemption() + try: + yield _packet.register_status_exemption(rail) + finally: + _packet.unregister_status_exemption() + if a_hosts: + _packet.register_status_exemption(previous) + + +@_pytest.fixture(autouse=True) +def _the_hosts_rail_for_the_declared_rail_suites(request): + rail = (openxfactory_status_exemption_rail() + if request.node.path.name in status_exemption_rail_suites() + else None) + if rail is None: + yield + return + with a_hosts_rail(rail): + yield diff --git a/tests/declared_exclusion.yaml b/tests/declared_exclusion.yaml index f658b7b..05da0ce 100644 --- a/tests/declared_exclusion.yaml +++ b/tests/declared_exclusion.yaml @@ -12,6 +12,23 @@ # emptied list is `count: 0` with no reasons and no entries. The root # conftest loads it, excludes nothing, and prints that nothing is left out. # +# EVERY ENTRY IS ALSO A DECLARED INTEGRATION TEST (plan 038 task T073, in +# openxFactory `specs/038-opendox-document-tool-self-maintenance/`; ARC-Q2 +# (a), openxFactory#656 comment 6003918488; #1144's F9.1 as batch Q amends +# it). What a lone checkout cannot run runs in this repository's own CI, +# composed with openxFactory: `.github/workflows/composed.yml` runs every +# file below at the ONE openxFactory commit `tests/composed_host_pin.yaml` +# names, in R1Q23 (a)'s form. 12.5's governed suites among them run in its +# governed step, and every other entry runs in its declared step, each file +# alone. So no file here is dropped from both suites. The count is `count`, +# and each entry keeps its reason. This file names that commit by naming the +# pin file, and writes no commit id of its own, so the pin advances (after +# T030, T064 and T075) with no edit here. Read with the paragraph above: in a +# lone checkout each entry stays an open extraction until its reason is +# cleared, and composed it is a declared integration test, by which +# declaration requirement 9 closes for openXdox-code (batch Q). Each reason's +# `open_until` names that composed home, so every lone run prints it. +# # WHO READS IT. # * The repository-root `conftest.py`. It derives `collect_ignore` from # `entries`, so a run that loads it collects the whole suite less these @@ -27,6 +44,12 @@ # list cannot hide a failure of any other kind. A file that stops failing # for its reason turns that test red, and it leaves the list in the pull # request that clears the reason. +# * `.github/workflows/composed.yml`. Its declared step runs every entry +# that is not one of 12.5's governed suites, composed and each alone, and +# refuses an entry that neither of its two steps runs. +# * `tests/conftest.py`. Where the run is composed, it registers the rail +# openxFactory's host registers for each entry naming +# `status-exemption-rail`, and only for those. # # THE RULES. # * `schema_version` is the integer 1, and `count` is an integer equal to the @@ -65,15 +88,15 @@ reasons: - id: doc_health reason: "reaches openxFactory's doc_health, which openxFactory packages nowhere and no lone checkout supplies" ruled: "R1Q6 (d), openxFactory#656 comment 5817152735" - open_until: "the doc_health direction arc (plan 034 T008)" + open_until: "the doc_health direction arc (plan 034 T008); run composed, as a declared integration test, by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" - id: status-exemption-rail reason: "needs openxFactory's status-exemption rail, which only openxFactory registers at openDox's status-exemption seam" ruled: "R1Q24 (a), openxFactory#656 comment 5850003126" - open_until: "the doc_health direction arc (plan 034 T008)" + open_until: "never in a lone checkout: a declared integration test, run composed by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" - id: openxfactory-contracts reason: "reads openxFactory's contracts (the contract family's validator, schemas and examples, or its contracts/manifest.yaml) where openxFactory's tree keeps them, which is outside this checkout" ruled: "R1Q24 (a), openxFactory#656 comment 5850003126" - open_until: "the doc_health direction arc (plan 034 T008)" + open_until: "never in a lone checkout: a declared integration test, run composed by .github/workflows/composed.yml at the openxFactory commit tests/composed_host_pin.yaml names (ARC-Q2 (a), openxFactory#656 comment 6003918488)" entries: - {path: tests/test_authoring_agent.py, reasons: [doc_health]} diff --git a/tests/test_declared_rail_registration.py b/tests/test_declared_rail_registration.py new file mode 100644 index 0000000..b6f7907 --- /dev/null +++ b/tests/test_declared_rail_registration.py @@ -0,0 +1,100 @@ +"""The rail registration `tests/conftest.py` makes for the declared rail +suites, held to its word (plan 038 task T073; ARC-Q2 (a), openxFactory#656 +comment 6003918488; Copilot r4197793290 on openXdox-code#45). + +Where the run is composed, each test of a file `tests/declared_exclusion.yaml` +names under `status-exemption-rail` runs with the rail openxFactory's host +registers, through `conftest.a_hosts_rail`. Those runs prove the rail is +registered. These cases prove what a composed run cannot see: that the seam is +put back afterwards, so no test after a rail suite meets a rail it did not +register, and no host's rail is lost to one. They use stand-in rails, so they +run in a lone checkout too, inside the required check. + +Each case leaves openDox's seam exactly as it found it: `_the_seam_as_found` +records the seam's three globals through `monkeypatch`, which writes them back +at teardown, whatever the case did. +""" + +from __future__ import annotations + +import pytest +import yaml + +from conftest import HERE, a_hosts_rail, status_exemption_rail_suites +from opendox import doxbench_packet + + +class _Rail: + """A stand-in rail: the two names a rail must carry, each its own.""" + + def __init__(self, name: str): + self.name = name + + def lifecycle_status(text): + return None + + def is_compression_exempt(text): + return False + + self.lifecycle_status = lifecycle_status + self.is_compression_exempt = is_compression_exempt + + +def _answering() -> object: + """The rail openDox's seam answers with now, read through the name it + hands over (`doxbench_packet.is_compression_exempt`), or None when no + rail is registered.""" + if not doxbench_packet.status_exemption_registered(): + return None + return doxbench_packet.is_compression_exempt + + +@pytest.fixture(autouse=True) +def _the_seam_as_found(monkeypatch): + for name in ("_status_exemption_rail", "_status_exemption_is_default", + "_status_exemption_default_read"): + monkeypatch.setattr(doxbench_packet, name, getattr(doxbench_packet, name)) + doxbench_packet.unregister_status_exemption() + + +def test_a_hosts_earlier_rail_is_dropped_for_the_block_and_put_back_after(): + earlier, rail = _Rail("earlier"), _Rail("the host's") + doxbench_packet.register_status_exemption(earlier) + with a_hosts_rail(rail): + assert _answering() is rail.is_compression_exempt + assert _answering() is earlier.is_compression_exempt + + +def test_no_rail_is_left_behind_where_none_was_registered(): + rail = _Rail("the host's") + with a_hosts_rail(rail): + assert _answering() is rail.is_compression_exempt + assert _answering() is None + + +def test_opendoxs_own_default_is_not_put_back(): + """As `a_hosts_plane` treats the default profile: the next entry point + registers the default again, as it does in any process.""" + default, rail = _Rail("openDox's default"), _Rail("the host's") + doxbench_packet.register_default_status_exemption(default) + with a_hosts_rail(rail): + assert _answering() is rail.is_compression_exempt + assert _answering() is None + + +def test_the_seam_is_put_back_when_the_block_fails(): + earlier, rail = _Rail("earlier"), _Rail("the host's") + doxbench_packet.register_status_exemption(earlier) + with pytest.raises(RuntimeError, match="the case failed"): + with a_hosts_rail(rail): + raise RuntimeError("the case failed") + assert _answering() is earlier.is_compression_exempt + + +def test_the_rail_suites_are_the_declarations_rail_entries(): + declaration = yaml.safe_load( + (HERE / "declared_exclusion.yaml").read_text(encoding="utf-8")) + named = {entry["path"].rsplit("/", 1)[-1] for entry in declaration["entries"] + if "status-exemption-rail" in entry["reasons"]} + assert named, "the declaration names no rail file" + assert status_exemption_rail_suites() == frozenset(named) diff --git a/tests/test_host_plane.py b/tests/test_host_plane.py index 19f4537..cefdf26 100644 --- a/tests/test_host_plane.py +++ b/tests/test_host_plane.py @@ -663,6 +663,28 @@ def test_the_farm_links_the_familys_schemas_and_examples(tmp_path) -> None: } +def test_the_farm_leaves_out_only_opendox_specs_snapshot_examples(tmp_path) -> None: + """The `opendox-snapshot-` prefix is left out for openDox-spec's own + examples only (T073; Copilot r4201411760 on #46, the holder's + r4201421102). An example of the same prefix from openxFactory or from + openXdox-spec is linked like any other.""" + root = tmp_path / "openxFactory" + _a_tree(root, { + "examples/ideation-dashboard/opendox-snapshot-a.example.yaml": "", + "openXdox/spec/examples/ideation-dashboard/negative/opendox-snapshot-b.negative.yaml": "", + "openDox/spec/examples/ideation-dashboard/opendox-snapshot-c.example.yaml": "", + "openDox/spec/examples/ideation-dashboard/negative/opendox-snapshot-d.negative.yaml": "", + }) + farm = tmp_path / "farm" + tests_conftest.build_contracts_farm(_a_reach(root, {}), farm) + assert _links(farm / "examples" / "ideation-dashboard") == { + "opendox-snapshot-a.example.yaml": + root / "examples/ideation-dashboard/opendox-snapshot-a.example.yaml", + "negative/opendox-snapshot-b.negative.yaml": + root / "openXdox/spec/examples/ideation-dashboard/negative/opendox-snapshot-b.negative.yaml", + } + + def test_the_farm_refuses_an_example_two_owners_keep(tmp_path) -> None: root = tmp_path / "openxFactory" _a_tree(root, {