test_wo31_register.py

"""CX-1 acceptance for the one canonical audit remediation register."""
import json
from pathlib import Path
import re
import subprocess
import unittest

ROOT = Path(__file__).resolve().parents[3]

DATED = re.compile(r'20\d\d-\d\d-\d\d')


def _cells(line):
    return [c.strip() for c in line.strip().strip('|').split('|')]


def _row_id(line):
    cells = _cells(line)
    return cells[0].replace('~~', '').strip() if cells else ''


def register_row_moved_with_evidence(old_line, current_lines):
    """The register rule (docs/OPEN_ITEMS.md header): never delete a row, strike
    it; close a row with the commit, run, or ruling that closed it.

    So a baseline row may differ from the current file in its LAST cell
    (State), which must either keep the old state struck through
    (``~~OPEN~~ CLOSED ...``) or carry a dated ruling / closure
    (``DONE 2026-09-15 — ...``); and any other cell may change only by keeping
    its original text struck through in front of the new text
    (``~~build (blocked: S4-1)~~ build (protected path) ...``). A cell that is
    reworded, appended to, or whose struck original is missing or misspelled is
    refused. Returns ``(ok, reason)``.
    """
    rid = _row_id(old_line)
    matches = [line for line in current_lines
               if line.startswith('|') and _row_id(line) == rid]
    if not matches:
        return False, f'{rid}: row deleted from the register'
    old_cells, new_cells = _cells(old_line), _cells(matches[0])
    if len(old_cells) != len(new_cells):
        return False, f'{rid}: cell count {len(old_cells)} -> {len(new_cells)}'
    for i, (old, new) in enumerate(zip(old_cells[:-1], new_cells[:-1])):
        # Equality, not substring (Copilot on #1064): an appended clause
        # ("do the thing" -> "do the thing now") is a rewording too.
        plain_old = old.replace('~~', '')
        if plain_old == new.replace('~~', ''):
            continue
        # 2026-09-17: three lanes moved Owner / Evidence / Item cells in place
        # on seven rows in one day (S2-D2, S2-D4, EV-PROV-1, S6-3, S4-5, L6-5,
        # L6-8) and this CI-unwired suite was the only thing that noticed. The
        # register rule is "never delete, strike": a cell other than State may
        # change only by keeping its original text struck through IN FRONT of
        # the new text (`~~old~~ new`). Order is enforced so the history reads
        # first; a struck fragment buried elsewhere is still a rewording.
        if new.startswith(f'~~{plain_old}~~'):
            continue
        return False, f'{rid}: cell {i} reworded: {old[:60]!r} -> {new[:60]!r}'
    old_state, new_state = old_cells[-1], new_cells[-1]
    if old_state == new_state or f'~~{old_state}~~' in new_state:
        return True, ''
    if DATED.search(new_state):
        return True, ''
    return False, f'{rid}: state moved without a dated ruling or closure: {new_state[:80]!r}'


class RegisterAcceptance(unittest.TestCase):
    def test_wo31_preserves_open_items_and_strikes_superseded_wave1_item(self):
        register_path = 'docs/OPEN_ITEMS.md'
        current = (ROOT / register_path).read_text()
        self.assertTrue('| ~~V4-1~~ |' in current, 'landed WS-1b is still an open register item')
        row = next(line for line in current.splitlines() if '| ~~V4-1~~ |' in line)
        self.assertIn('~~OPEN — WS-1b only~~', row)
        self.assertIn('CLOSED', row)
        self.assertIn('0568720', row)
        baseline = subprocess.check_output(
            ['git', 'show', '26bec9c6b424ca4b86b35621602f7a06764e5afe:' + register_path],
            cwd=ROOT, text=True,
        )
        # Non-row baseline lines that legitimately moved. The per-row opt-outs
        # that used to sit here (S2-D5, S7-3, W3-S8-7, and V4-4 for the
        # owner-merged WO-49 Item-cell rewording in PR #1056) were retired on
        # 2026-09-17: every one of those rows now satisfies the general rule
        # below — a moved cell keeps its original text struck in front of the
        # new text — so the rule has no per-row exceptions to grow.
        allowed_updates = (
            'The homepage deploy lane is **WO-44**',
            '- [WO-44]',
            # 2026-09-17: the header Rule line gained the strike-in-front
            # sentence for non-State cells in the same commit that taught
            # register_row_moved_with_evidence the shape (rule 01: the claim
            # and the row that authorizes it move together).
            '**Rule:** this file is the single forward list for MIZ OKI.',
        )
        current_lines = current.splitlines()
        for old_line in baseline.splitlines():
            if old_line.startswith('| V4-1 |'):
                # Every original cell survives, including its historical wording.
                # The changed item may only gain strike-throughs and closure evidence.
                old_cells = old_line.strip('|').split('|')
                new_cells = row.strip('|').split('|')
                self.assertEqual(len(old_cells), len(new_cells))
                for old, new in zip(old_cells[:-1], new_cells[:-1]):
                    # every cell but State survives verbatim, strike-throughs removed
                    self.assertEqual(old.strip(), new.replace('~~', '').strip())
                self.assertIn(old_cells[-1].strip(), new_cells[-1].replace('~~', ''))
                continue
            if old_line.startswith(allowed_updates):
                continue
            if old_line in current_lines:
                continue
            # 2026-09-15: S3-2, S3-4, S3-5, S7-1 and S7-4 moved their State
            # cell to a dated ruling or closure without striking the old
            # state. The register rule permits closing a row with the ruling
            # that closed it; what it forbids is deleting or rewording one.
            # Enforce exactly that, instead of growing the allow-list per row.
            self.assertTrue(old_line.startswith('|'),
                            f'baseline line lost: {old_line[:80]!r}')
            ok, reason = register_row_moved_with_evidence(old_line, current_lines)
            self.assertTrue(ok, reason)

    def test_register_row_rule_accepts_dated_state_moves_only(self):
        # Seeded both directions (rule 01): the legal moves it must accept and
        # the illegal ones it must refuse.
        old = '| X-1 | do the thing | build | `docs/x.md` | OPEN |'
        for good in (
            ['| X-1 | do the thing | build | `docs/x.md` | DONE 2026-09-15 — landed `abc1234` |'],
            ['| ~~X-1~~ | do the thing | build | `docs/x.md` | ~~OPEN~~ CLOSED `abc1234` |'],
            ['| X-1 | do the thing | build | `docs/x.md` | OPEN |'],
            # 2026-09-17: a non-State cell may move when its original survives
            # struck in front of the new text (Owner, Evidence, Item alike).
            ['| X-1 | do the thing | ~~build~~ operator (dispatch) | `docs/x.md` | OPEN |'],
            ['| X-1 | do the thing | build | ~~`docs/x.md`~~ `docs/y.md` (design of record); `docs/x.md` | OPEN |'],
            ['| X-1 | ~~do the thing~~ do the thing, then verify it | build | `docs/x.md` | OPEN — RULED 2026-09-17 |'],
        ):
            ok, reason = register_row_moved_with_evidence(old, good)
            self.assertTrue(ok, reason)
        for bad in (
            [],                                                              # deleted
            ['| X-1 | do the OTHER thing | build | `docs/x.md` | OPEN |'],   # reworded
            ['| X-1 | do the thing now | build | `docs/x.md` | OPEN |'],     # appended (still a rewording)
            ['| X-1 | do the thing | build | `docs/x.md` | DONE |'],         # undated move
            ['| X-1 | do the thing | build | OPEN |'],                       # cell dropped
            ['| X-1 | do the thing | operator (dispatch) | `docs/x.md` | OPEN |'],   # owner reworded, nothing struck
            ['| X-1 | do the thing | ~~builder~~ operator | `docs/x.md` | OPEN |'],  # struck text is not the original
            ['| X-1 | do the thing | operator ~~build~~ | `docs/x.md` | OPEN |'],    # original struck, but not in front
        ):
            ok, reason = register_row_moved_with_evidence(old, bad)
            self.assertFalse(ok, f'accepted an illegal row change: {bad!r}')

    def test_wo31_every_manifest_work_order_links_once_to_its_issue(self):
        # The requested root spelling is absent on the audit and current main;
        # docs/OPEN_ITEMS.md is the existing register. Do not fork it.
        self.assertFalse((ROOT / 'OPEN_ITEMS.md').exists())
        register = (ROOT / 'docs/OPEN_ITEMS.md').read_text()
        heading = '## Audit 2026-09-06 remediation'
        self.assertEqual(register.count(heading), 1)
        section = register.split(heading, 1)[1].split('\n## ', 1)[0]
        manifest = json.loads((ROOT / 'docs/audits/wo/manifest.json').read_text())
        issues = json.loads((ROOT / 'docs/audits/wo/issue_map.json').read_text())
        for entry in manifest:
            wo = entry['id']
            lines = [line for line in section.splitlines() if f'[{wo}]' in line]
            self.assertEqual(len(lines), 1, f'{wo} must have exactly one register entry')
            self.assertIn(f'audits/wo/{wo}.md', lines[0])
            self.assertIn(f'#{issues[wo]}', lines[0])
        debt = (ROOT / 'docs/BUILD_DEBT.md').read_text()
        self.assertIn('OPEN_ITEMS.md#audit-2026-09-06-remediation', debt)
        self.assertNotRegex(debt, r'(?m)^[-|].*\[WO-\d{2}\]')

    def test_wo44_register_and_work_order_record_the_2026_09_13_runtime_failure(self):
        register = (ROOT / 'docs/OPEN_ITEMS.md').read_text()
        github_section = register.split(
            '## F. GitHub org migration / github-virtuoso close-out v2 (2026-09-12)', 1
        )[1].split('\n## ', 1)[0]
        self.assertIn('The homepage deploy lane remains **WO-44**', github_section)
        self.assertIn('ruling **A1** is implemented in-repo', github_section)
        self.assertIn('ruling **B1**', github_section)
        self.assertIn('34774896326', github_section)
        self.assertIn('guards 1/2/5', github_section)
        self.assertIn('attributeCondition', github_section)
        self.assertNotIn('not re-listed here', github_section)

        audit_section = register.split('## Audit 2026-09-06 remediation', 1)[1]
        # 2026-09-15: the row is struck through (`- ~~[WO-44]`) once STEP 4 was
        # measured done by the owner lane; the closure keeps the history inline.
        wo44_row = next(
            line for line in audit_section.splitlines()
            if line.startswith('- [WO-44]') or line.startswith('- ~~[WO-44]')
        )
        self.assertIn('A1 landed', wo44_row)
        self.assertIn('run #120 proved guards 1/2/5', wo44_row)
        # moved 2026-09-14: the row records STEP 1–2 as applied (run #126) and
        # names STEP 4 — first as the owner-held remainder, since 2026-09-15 as
        # DONE with its measurement; the intent — the row names the runbook step
        # — is unchanged.
        self.assertIn('runbook §5 STEP 4', wo44_row)

        work_order = (ROOT / 'docs/audits/wo/WO-44.md').read_text()
        self.assertIn('run #120 already proved guards 1/2/5', work_order)
        self.assertIn('`github.ref_protected=true`', work_order)
        self.assertIn('do not treat the REST branch `.protected` field as the measurement of record', work_order)
        self.assertIn('runbook §5 STEP 1–2 are applied', work_order)
        self.assertIn('breakages (3)/(4)', work_order)
        self.assertNotIn(
            '`gh api repos/MIZOKI-3-5/MIZOKICloudRun/branches/main -q .protected` returns `true`',
            work_order,
        )


if __name__ == '__main__':
    unittest.main()
← All docsView source on GitHub →