Files
othmanadi--planning-with-files/tests/test_seo_docs_pages.py
T
OthmanAdi b49ea488fb feat: make the Stop hook and session recovery fire everywhere, add long-run injection controls
The Stop scalar never dispatched on macOS or Linux (the PowerShell branch
was always selected because check-complete.ps1 ships on every platform)
and was a silent no-op anywhere CLAUDE_SKILL_DIR was unset (the :- install
path fallback can never substitute because the probed variable is always a
non-empty string). Both the legacy completion advisory and the v3
completion gate were dead in those environments. The scalar now selects
targets by file existence and dispatches by platform, PowerShell only
under MINGW/MSYS/CYGWIN, sh elsewhere. Windows output is unchanged.
Patched in all 14 SKILL.md variants that carry the scalar.

session-catchup probed a ~/.claude/projects name that Claude Code never
writes for POSIX paths (leading dash stripped) or paths containing an
underscore (replaced with a dash), so recovery after /clear silently
found nothing there. The mapper now probes the exact spelling first,
keeps both legacy spellings for stores created by older versions, and
settles ambiguity via the cwd recorded in the newest session file.
Propagated to every shipped copy including the older-generation root,
.hermes, and .mastracode scripts.

Also: the attestation SHA cache is keyed on the absolute plan path so two
projects can no longer share a slot and report a false PLAN TAMPERED;
resolve-plan-dir.ps1 reaches parity with the sh resolver (slug filter,
task_plan.md requirement in the newest-dir scan, fail-closed containment);
ledger-append.sh no longer truncates summaries mid-codepoint (UTF-8-safe
trim via iconv with a pure-sh fallback).

New long-run features, all opt-in or additive: structure-aware injection
(PWF_INJECT=smart or an inject-smart .mode token) keeps the active phase,
phase counts, and the last three decisions in the window late in long
plans; a Next Step section in both plan templates plus a sixth reboot
question; session-catchup annotates tool results (ok or FAILED with the
first error line) instead of only listing attempts.

Infrastructure: macos-latest joins the CI matrix, a BSD-userland
simulation harness runs the script fleet without realpath, readlink,
flock, or sha256sum on the Linux leg, and .gitattributes pins LF for
scripts. SEO surfaces: llms.txt rewritten as a Q&A page, three
problem-query docs pages, plugin.json and CITATION.cff keyword hygiene.

Suite grows from 217 to 301 passed, 11 skipped, on Windows and both
existing CI legs; default hook output stays byte-identical to v2.43
(legacy invariant proven by the existing invariant tests).
2026-07-21 21:17:19 +02:00

114 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Guards for the v3.8.0 problem-query docs pages.
The three pages under docs/ answer high-volume search queries and carry
strict content rules: fixed H1, 60-120 line budget, the two-route Install
section, cross-links between all three, links back to README and
docs/installation.md, internal-v1 framing on any recovery number, no
em/en dashes, and no competitor method names. This test pins each rule
so later edits cannot silently break them.
"""
from __future__ import annotations
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DOCS_DIR = REPO_ROOT / "docs"
# filename -> required exact H1 (first line of the file)
PAGES = {
"claude-code-lost-context-after-compaction.md": (
"# Claude Code lost context after compaction: how to recover and prevent it"
),
"agent-forgets-plan-after-clear.md": (
"# My coding agent forgets the plan after /clear: the file-based fix"
),
"long-running-agent-tasks.md": (
"# Long-running agent tasks: keeping a coding agent on track for hours"
),
}
PLUGIN_MARKETPLACE = "/plugin marketplace add OthmanAdi/planning-with-files"
PLUGIN_INSTALL = "/plugin install planning-with-files@planning-with-files"
NPX_ONE_LINER = (
"npx skills add OthmanAdi/planning-with-files --skill planning-with-files -g"
)
# Honesty rule for the release: competing planning methods are never named
# in SEO-facing docs pages.
COMPETITOR_NAMES = ("superpowers", "spec-kit", "memory-bank", "cline")
def read(name: str) -> str:
return (DOCS_DIR / name).read_text(encoding="utf-8")
class SeoDocsPagesTests(unittest.TestCase):
def test_pages_exist(self) -> None:
for name in PAGES:
self.assertTrue((DOCS_DIR / name).is_file(), name)
def test_line_budget_60_to_120(self) -> None:
for name in PAGES:
count = len(read(name).splitlines())
self.assertGreaterEqual(count, 60, f"{name}: {count} lines")
self.assertLessEqual(count, 120, f"{name}: {count} lines")
def test_exact_h1_on_first_line(self) -> None:
for name, h1 in PAGES.items():
first = read(name).splitlines()[0]
self.assertEqual(first, h1, name)
def test_install_section_with_both_routes(self) -> None:
for name in PAGES:
body = read(name)
self.assertIn("## Install", body, name)
install = body.split("## Install", 1)[1]
self.assertIn(PLUGIN_MARKETPLACE, install, name)
self.assertIn(PLUGIN_INSTALL, install, name)
self.assertIn(NPX_ONE_LINER, install, name)
def test_links_to_readme_and_installation_guide(self) -> None:
for name in PAGES:
body = read(name)
self.assertIn("../README.md", body, f"{name}: missing README link")
self.assertIn(
"(installation.md)", body, f"{name}: missing docs/installation.md link"
)
def test_pages_cross_link_each_other(self) -> None:
for name in PAGES:
body = read(name)
for other in PAGES:
if other == name:
continue
self.assertIn(other, body, f"{name} must link to {other}")
def test_no_em_or_en_dashes(self) -> None:
for name in PAGES:
body = read(name)
self.assertNotIn("—", body, f"{name}: em dash found")
self.assertNotIn("", body, f"{name}: en dash found")
def test_recovery_number_keeps_internal_v1_framing(self) -> None:
# Wherever the 5.0 vs 13.3 turn numbers appear, the internal-v1
# framing must appear on the same page.
for name in PAGES:
body = read(name)
if "5.0 turns" in body or "13.3" in body:
low = body.lower()
self.assertIn("internal", low, name)
self.assertIn("v1", low, name)
self.assertIn("author-run", low, name)
def test_no_competitor_names(self) -> None:
for name in PAGES:
low = read(name).lower()
for competitor in COMPETITOR_NAMES:
self.assertNotIn(competitor, low, f"{name}: names {competitor}")
if __name__ == "__main__":
unittest.main()