diff --git a/.claude/skills/report/SKILL.md b/.claude/skills/report/SKILL.md index 03a2e98..a75e5a0 100644 --- a/.claude/skills/report/SKILL.md +++ b/.claude/skills/report/SKILL.md @@ -39,6 +39,13 @@ Structure, in order: 6. **Open items** — split by who acts: owner (dashboard/env/merge), orchestrator (cross-repo), this repo's next pass. +Where the pass consumed a sync spec, per-item dispositions use +exactly these five words: `applied` / `ported-as-contract` / +`already-present` / `not-applicable-because` / `open`. `open` means +the detect fires but the item is deliberately out of this session's +scope — name it under Open items with who acts. Do not invent a +sixth word; the orchestrator's tooling reads these five. + Anti-patterns, all observed in the fleet and all rejected on receipt: "should work" (test it or mark it unverified); summary claims without artifacts; green CI presented as deploy proof when diff --git a/tests/test_auth_demos.py b/tests/test_auth_demos.py new file mode 100644 index 0000000..6cf4d50 --- /dev/null +++ b/tests/test_auth_demos.py @@ -0,0 +1,51 @@ +"""Every auth-gate demo entry resolves — on THIS site, loudly. + +`build_demo` swallows import failures BY DESIGN (a broken example must never +take down the sign-in funnel), and its warning only fires when that +endpoint's card actually renders — which never happens when the endpoint is +not a page here at all. That combination made a dead entry perfectly silent: +every fork inherited the template's entry, it resolved on none of them, and +their gate cards rendered demo-less from fork time without a line of log +(batch-1 finding, excalidraw, 2026-08-25). This file is the one surface +where a dead entry is loud. + +Byte-verbatim across the fleet: it sweeps THIS repo's DEMOS table against +THIS repo's page registry, so the same bytes hold everywhere. The entries +themselves are site judgment (swap in your own hero example) — an EMPTY +table passes; a dead entry never does. +""" + +from __future__ import annotations + +import importlib + +from lib.auth_demos import DEMOS + + +def test_every_demo_endpoint_is_a_registered_page(app_module): + import dash + + registered = {entry["path"] for entry in dash.page_registry.values()} + dead = sorted(set(DEMOS) - registered) + assert dead == [], ( + f"DEMOS endpoints that are not pages on this site: {dead} — " + "a card that never renders can never surface its own broken demo" + ) + + +def test_every_demo_module_imports_and_exposes_component(app_module): + # import_module is exactly what build_demo does in production; app_module + # first, because the example modules assume the app (and the docs pass + # that imports them at startup) already exists. + problems = [] + for path, spec in sorted(DEMOS.items()): + try: + module = importlib.import_module(spec["module"]) + except Exception as e: + problems.append(f"{path}: {spec['module']} failed to import ({e})") + continue + if not hasattr(module, "component"): + problems.append( + f"{path}: {spec['module']} has no module-level `component`" + ) + assert problems == [], "; ".join(problems) diff --git a/tests/test_claude_kit.py b/tests/test_claude_kit.py index 9dfe7b3..b98da15 100644 --- a/tests/test_claude_kit.py +++ b/tests/test_claude_kit.py @@ -37,13 +37,30 @@ def _ignored(path: str) -> bool: ) +def _in_repo(rel: str) -> bool: + return ".." not in rel and not rel.startswith("/") + + def _machine_fence(kind: str, text: str, where: str) -> None: """The shared pin for machine fences (```yaml sync-verbatim in specs, ```yaml byte-owned in DIVERGENCES.md): exactly one block, `- path` lines with `#` comments, every path repo-relative and real at HEAD. Empty is valid — an empty block is a statement, a missing one is an - omission. `# requires: ` lines (the fan-out's adoption gate, - 1.6.23) are validated like paths — a typo'd gate gates nothing.""" + omission. Gate lines (the fan-out's adoption gates) are validated + like paths — a typo'd gate gates nothing: + + `# requires: ` (1.6.23) — the block applies only where + exists. For paths no pre-existing file can occupy; + where one can, the gate must name a contract instead + (sync/README.md — flows' pre-existing CLAUDE.md, 1.6.28). + `# requires-contract: :: ` (1.6.28) — the block + applies only where exists AND contains . The + clause must be real in THIS repo's copy at HEAD too. + `- # requires: ` (1.6.28) — per-file gate: the + fan-out skips this one copy where is absent, instead + of gating the whole block (clerkhook: a lockdown fork has no + lib/auth_demos.py, legitimately, and must still receive the + rest).""" fences = re.findall( r"^```yaml " + kind + r"[ \t]*\n(.*?)^```[ \t]*$", text, re.M | re.S ) @@ -52,10 +69,34 @@ def _machine_fence(kind: str, text: str, where: str) -> None: f"found {len(fences)}" ) for raw in fences[0].splitlines(): - required = re.match(r"#\s*requires:\s*(.+)$", raw.strip()) + stripped = raw.strip() + if re.match(r"#\s*requires-contract:", stripped): + gate = re.match( + r"#\s*requires-contract:\s*(.+?)\s*::\s*(.+)$", stripped + ) + assert gate, ( + f"{where} {kind}: {raw!r} — `# requires-contract:` takes " + "` :: `; a malformed gate gates nothing" + ) + req, clause = gate.group(1).strip(), gate.group(2).strip() + assert _in_repo(req), ( + f"{where} {kind}: `# requires-contract:` path {req!r} " + "escapes the repo" + ) + assert (REPO / req).is_file(), ( + f"{where} {kind}: `# requires-contract:` names {req!r} " + "which does not exist at HEAD — a typo'd gate gates nothing" + ) + assert clause in (REPO / req).read_text(), ( + f"{where} {kind}: `# requires-contract:` clause {clause!r} " + f"is not in this repo's own {req} — a typo'd clause gates " + "nothing" + ) + continue + required = re.match(r"#\s*requires:\s*(.+)$", stripped) if required: req = required.group(1).strip() - assert ".." not in req and not req.startswith("/"), ( + assert _in_repo(req), ( f"{where} {kind}: `# requires:` path {req!r} escapes the repo" ) assert (REPO / req).is_file(), ( @@ -63,20 +104,36 @@ def _machine_fence(kind: str, text: str, where: str) -> None: "not exist at HEAD — a typo'd gate gates nothing" ) continue - entry = raw.split("#", 1)[0].strip() + entry, _, comment = raw.partition("#") + entry = entry.strip() if not entry: continue assert entry.startswith("- "), ( f"{where} {kind}: {raw!r} is not a `- path` line" ) path = entry[2:].strip() - assert ".." not in path and not path.startswith("/"), ( + assert _in_repo(path), ( f"{where} {kind}: {path!r} escapes the repo" ) assert (REPO / path).is_file(), ( f"{where} {kind}: {path!r} does not exist at HEAD " "— the machine would act on nothing or the wrong thing" ) + # A per-file gate is the WHOLE trailing comment, `requires: ` + # from its first character; prose comments that merely mention the + # word stay prose. + per_file = re.match(r"\s*requires:\s*(.+)$", comment) + if per_file: + gate_path = per_file.group(1).strip() + assert _in_repo(gate_path), ( + f"{where} {kind}: per-file gate on {path!r} escapes the " + f"repo: {gate_path!r}" + ) + assert (REPO / gate_path).is_file(), ( + f"{where} {kind}: per-file gate on {path!r} names " + f"{gate_path!r} which does not exist at HEAD — a typo'd " + "gate gates nothing" + ) def test_kit_files_exist_and_are_not_ignored():