Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license

This commit is contained in:
2026-09-05 13:26:46 +03:00
commit 03634b1ca3
11340 changed files with 3442369 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
"""
Smoke tests for the actual-setup optional skill.
Validates:
- SKILL.md frontmatter conforms to the ≤60-char description standard
- Frontmatter has required fields
- The skill references the first-class ``actual`` provider (not the
legacy custom-provider config path that conflicts with it)
- The bundled OpenCode reference exists
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
import yaml
SKILL_DIR = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "devops"
/ "actual-setup"
)
@pytest.fixture(scope="module")
def skill_source() -> str:
return (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def frontmatter(skill_source) -> dict:
m = re.search(r"^---\n(.*?)\n---", skill_source, re.DOTALL)
assert m, "SKILL.md missing YAML frontmatter"
return yaml.safe_load(m.group(1))
def test_skill_dir_exists() -> None:
assert SKILL_DIR.is_dir(), f"missing skill dir: {SKILL_DIR}"
def test_description_under_60_chars(frontmatter) -> None:
desc = frontmatter["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars (limit ≤60): {desc!r}"
def test_has_required_frontmatter_fields(frontmatter) -> None:
for field in ("name", "description", "version", "license"):
assert field in frontmatter, f"missing required field: {field}"
def test_credits_contributor(frontmatter) -> None:
assert "shl0ms" in str(frontmatter.get("author", "")), (
"author must credit the human contributor first"
)
def test_uses_first_class_provider_not_custom_provider(skill_source) -> None:
# The legacy setup configured Actual as providers.actual.* custom entries,
# which now collides with the built-in provider of the same name.
assert "--provider actual" in skill_source
assert "hermes config set providers.actual.api " not in skill_source
assert "key_env" not in skill_source
def test_opencode_reference_exists() -> None:
assert (SKILL_DIR / "references" / "opencode.md").is_file()
@@ -0,0 +1,102 @@
"""Contract checks for the optional agent-merge-conflict-arbiter skill asset.
Reads only the SKILL.md markdown asset — no .py source reads, no network.
"""
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SKILL_PATH = (
REPO_ROOT
/ "optional-skills"
/ "autonomous-ai-agents"
/ "agent-merge-conflict-arbiter"
/ "SKILL.md"
)
REQUIRED_SECTIONS = [
"## When to Use",
"## Prerequisites",
"## How to Run",
"## Quick Reference",
"## Procedure",
"## Pitfalls",
"## Verification",
]
def _frontmatter_and_body():
content = SKILL_PATH.read_text(encoding="utf-8")
assert content.startswith("---"), "SKILL.md must open with frontmatter"
m = re.search(r"\n---\s*\n", content[3:])
assert m, "frontmatter must close with ---"
fm_text = content[3 : m.start() + 3]
body = content[m.end() + 3 :]
fm = {}
for line in fm_text.splitlines():
km = re.match(r"^(\w[\w-]*):\s*(.*)$", line)
if km:
fm[km.group(1)] = km.group(2).strip().strip('"')
return fm, body
def test_skill_file_exists():
assert SKILL_PATH.is_file()
def test_frontmatter_required_fields():
fm, _ = _frontmatter_and_body()
for field in ("name", "description", "version", "author", "license", "platforms"):
assert field in fm, f"missing frontmatter field: {field}"
assert fm["name"] == "agent-merge-conflict-arbiter"
def test_description_hardline():
fm, _ = _frontmatter_and_body()
desc = fm["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60"
assert desc.endswith("."), "description must end with a period"
assert desc.count(".") == 1, "description must be one sentence"
def test_required_sections_present_in_order():
_, body = _frontmatter_and_body()
positions = []
for section in REQUIRED_SECTIONS:
idx = body.find(section)
assert idx != -1, f"missing section: {section}"
positions.append(idx)
assert positions == sorted(positions), "sections out of required order"
def test_body_size_within_bundled_norms():
content = SKILL_PATH.read_text(encoding="utf-8")
lines = content.count("\n") + 1
assert 80 <= lines <= 260, f"SKILL.md is {lines} lines; expected ~100-200"
def test_references_native_hermes_tools():
_, body = _frontmatter_and_body()
for tool in ("`terminal`", "`read_file`", "`patch`", "`delegate_task`"):
assert tool in body, f"body must reference native tool {tool}"
def test_classification_taxonomy_present():
_, body = _frontmatter_and_body()
for cls in (
"disjoint-intent",
"same-question-different-answer",
"superseded",
):
assert cls in body, f"missing hunk class: {cls}"
def test_impartiality_contract_stated():
_, body = _frontmatter_and_body()
assert "never favor" in body.lower()
assert "drive-by" in body.lower()
def test_no_machine_local_paths():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "/home/" not in content
+149
View File
@@ -0,0 +1,149 @@
"""CI enforcement of the skill authoring standards (AGENTS.md hardline).
Every bundled (skills/) and optional (optional-skills/) SKILL.md must satisfy
the programmatically-checkable subset of the authoring standards. Judgment
calls (tier placement, router-skill smell, prose quality) stay with review;
everything here is mechanical.
Pre-existing violations that need non-trivial content work are grandfathered
in the GRANDFATHER dict below. Do NOT add new entries for new skills — fix
the skill instead. Remove entries as the debt is paid down.
"""
import re
from pathlib import Path
import pytest
import yaml
REPO = Path(__file__).resolve().parents[2]
MARKETING = re.compile(
r"\b(powerful|comprehensive|seamless|revolutionary|cutting-edge|state-of-the-art)\b",
re.I,
)
MACHINE_LOCAL = re.compile(r"/home/(?!runner\b)[a-z0-9_-]+/|[A-Z]:\\+Users\\+(?!<)")
# ---------------------------------------------------------------------------
# Grandfathered pre-existing debt. Shrink this list; never grow it.
# ---------------------------------------------------------------------------
GRANDFATHER: dict[str, set[str]] = {
# (empty — the Aug 2026 sweep cleared all mechanical violations)
}
def _skill_paths():
return sorted(
list(REPO.glob("skills/**/SKILL.md"))
+ list(REPO.glob("optional-skills/**/SKILL.md"))
)
def _rel(p: Path) -> str:
return str(p.parent.relative_to(REPO))
def _params():
return [pytest.param(p, id=_rel(p)) for p in _skill_paths()]
def _grandfathered(p: Path, rule: str) -> bool:
return rule in GRANDFATHER.get(_rel(p), set())
def _frontmatter(p: Path):
content = p.read_text(encoding="utf-8")
assert content.startswith("---"), f"{_rel(p)}: SKILL.md must start with ---"
m = re.search(r"\n---\s*\n", content[3:])
assert m, f"{_rel(p)}: unclosed frontmatter"
fm = yaml.safe_load(content[3 : m.start() + 3])
assert isinstance(fm, dict), f"{_rel(p)}: frontmatter must be a YAML mapping"
return fm, content
ALL_SKILL_NAMES = None
def _all_names():
global ALL_SKILL_NAMES
if ALL_SKILL_NAMES is None:
names = set()
for p in _skill_paths():
names.add(p.parent.name)
ALL_SKILL_NAMES = names
return ALL_SKILL_NAMES
def test_at_least_the_expected_population():
# sanity: the globs actually find the trees (not a count snapshot)
paths = _skill_paths()
assert any("optional-skills" in str(p) for p in paths)
assert any(str(p.parent).startswith(str(REPO / "skills")) for p in paths)
@pytest.mark.parametrize("p", _params())
def test_required_frontmatter_fields(p):
fm, _ = _frontmatter(p)
missing = [
f
for f in ("name", "description", "version", "author", "license", "platforms")
if f not in fm
]
if missing and not _grandfathered(p, "fields"):
pytest.fail(f"{_rel(p)}: missing frontmatter fields: {missing}")
hermes = (fm.get("metadata") or {}).get("hermes") or {}
if not (hermes.get("tags") or fm.get("tags")) and not _grandfathered(p, "tags"):
pytest.fail(f"{_rel(p)}: no tags (metadata.hermes.tags or top-level tags)")
@pytest.mark.parametrize("p", _params())
def test_name_matches_directory(p):
fm, _ = _frontmatter(p)
if fm.get("name") != p.parent.name and not _grandfathered(p, "name"):
pytest.fail(
f"{_rel(p)}: frontmatter name {fm.get('name')!r} != dir {p.parent.name!r}"
)
@pytest.mark.parametrize("p", _params())
def test_description_hardline(p):
fm, _ = _frontmatter(p)
desc = str(fm.get("description") or "")
if _grandfathered(p, "description"):
return
assert len(desc) <= 60, f"{_rel(p)}: description {len(desc)} chars (hardline 60)"
assert desc.rstrip().endswith("."), f"{_rel(p)}: description must end with a period"
m = MARKETING.search(desc)
assert not m, f"{_rel(p)}: marketing word in description: {m.group(0)!r}"
@pytest.mark.parametrize("p", _params())
def test_related_skills_resolve(p):
fm, _ = _frontmatter(p)
hermes = (fm.get("metadata") or {}).get("hermes") or {}
dangling = [
rs for rs in (hermes.get("related_skills") or []) if rs not in _all_names()
]
if dangling and not _grandfathered(p, "related"):
pytest.fail(f"{_rel(p)}: dangling related_skills: {dangling}")
@pytest.mark.parametrize("p", _params())
def test_no_machine_local_paths(p):
_, content = _frontmatter(p)
m = MACHINE_LOCAL.search(content)
if m and not _grandfathered(p, "paths"):
pytest.fail(f"{_rel(p)}: machine-local path {m.group(0)!r}")
@pytest.mark.parametrize("p", _params())
def test_size_limit(p):
_, content = _frontmatter(p)
if len(content) > 100_000 and not _grandfathered(p, "size"):
pytest.fail(
f"{_rel(p)}: {len(content)} chars > 100k — split into references/"
)
def test_grandfather_entries_still_needed():
"""A grandfather entry whose violation is fixed must be removed."""
for rel in GRANDFATHER:
assert (REPO / rel / "SKILL.md").exists(), f"stale grandfather entry: {rel}"
+76
View File
@@ -0,0 +1,76 @@
"""Durable integration contracts for the bundled Box productivity skill."""
from __future__ import annotations
import re
from pathlib import Path
from urllib.parse import unquote
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
SKILL_DIR = REPO_ROOT / "skills" / "productivity" / "box"
SKILL_MD = SKILL_DIR / "SKILL.md"
def _parse_frontmatter(content: str) -> dict:
from agent.skill_utils import parse_frontmatter
frontmatter, _ = parse_frontmatter(content)
return frontmatter
def _local_markdown_targets(path: Path) -> set[Path]:
targets: set[Path] = set()
for raw_target in re.findall(r"\[[^]]+\]\(([^)]+)\)", path.read_text(encoding="utf-8")):
target = raw_target.split("#", maxsplit=1)[0].strip("<>")
if not target or "://" in target or target.startswith("mailto:"):
continue
targets.add((path.parent / unquote(target)).resolve())
return targets
@pytest.fixture(scope="module")
def skill_text() -> str:
return SKILL_MD.read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def frontmatter(skill_text: str) -> dict:
return _parse_frontmatter(skill_text)
def test_skill_frontmatter_is_valid_and_discoverable(frontmatter: dict):
assert frontmatter.get("name") == "box"
description = frontmatter.get("description")
assert isinstance(description, str) and description.strip()
assert len(description) <= 60
assert description.endswith(".")
assert frontmatter.get("license") == "MIT"
assert "Chris Kim" in str(frontmatter.get("author"))
assert "iskysun96" in str(frontmatter.get("author"))
platforms = frontmatter.get("platforms")
assert isinstance(platforms, list)
assert {"linux", "macos", "windows"}.issubset(platforms)
def test_box_command_is_declared_without_universal_credential_gate(frontmatter: dict):
prerequisites = frontmatter.get("prerequisites") or {}
assert "box" in prerequisites.get("commands", [])
assert not prerequisites.get("env_vars")
def test_all_local_links_resolve_inside_the_skill():
markdown_files = list(SKILL_DIR.rglob("*.md"))
for source in markdown_files:
for target in _local_markdown_targets(source):
assert target.is_file(), f"broken link in {source.relative_to(SKILL_DIR)}: {target}"
assert target.is_relative_to(SKILL_DIR.resolve()), (
f"local link in {source.relative_to(SKILL_DIR)} escapes the skill: {target}"
)
def test_every_reference_is_reachable_from_skill_entrypoint():
entrypoint_targets = _local_markdown_targets(SKILL_MD)
reference_files = set((SKILL_DIR / "references").glob("*.md"))
assert reference_files <= entrypoint_targets
@@ -0,0 +1,150 @@
"""Tests for optional-skills/web-development/cloudflare-temporary-deploy/scripts/parse_deploy_output.py"""
import json
import sys
from pathlib import Path
from unittest import mock
import pytest
SCRIPTS_DIR = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "web-development"
/ "cloudflare-temporary-deploy"
/ "scripts"
)
sys.path.insert(0, str(SCRIPTS_DIR))
import parse_deploy_output as pdo
CREATED = """\
Continuing means you accept Cloudflare's Terms of Service and Privacy Policy.
Temporary account ready:
Account: swift-otter (created)
Claim within: 60 minutes
Claim URL: https://dash.cloudflare.com/claim-preview?claimToken=TOKEN_AAA
Uploaded my-worker
Deployed my-worker triggers
https://my-worker.swift-otter.workers.dev
"""
REUSED = """\
Temporary account ready:
Account: swift-otter (reused)
Claim within: 17 minutes
Claim URL: https://dash.cloudflare.com/claim-preview?claimToken=TOKEN_BBB
Deployed my-worker triggers
https://my-worker.swift-otter.workers.dev
"""
NOT_LOGGED_IN = """\
✘ [ERROR] You are not logged in.
To continue without logging in, rerun this command with `--temporary`.
"""
AUTH_PRESENT_ERROR = """\
✘ [ERROR] The --temporary flag cannot be used while Wrangler is authenticated.
Run `wrangler logout` first, or remove CLOUDFLARE_API_TOKEN.
"""
class TestParseCreated:
def test_live_url(self):
assert pdo.parse(CREATED)["live_url"] == "https://my-worker.swift-otter.workers.dev"
def test_claim_url(self):
assert (
pdo.parse(CREATED)["claim_url"]
== "https://dash.cloudflare.com/claim-preview?claimToken=TOKEN_AAA"
)
def test_account_and_state(self):
r = pdo.parse(CREATED)
assert r["account"] == "swift-otter"
assert r["account_state"] == "created"
def test_expiry_and_deployed(self):
r = pdo.parse(CREATED)
assert r["expires_minutes"] == 60
assert r["deployed"] is True
class TestParseReused:
def test_state_is_reused(self):
assert pdo.parse(REUSED)["account_state"] == "reused"
class TestNoDeploy:
def test_not_logged_in_has_no_urls(self):
r = pdo.parse(NOT_LOGGED_IN)
assert r["live_url"] is None
assert r["claim_url"] is None
assert r["account"] is None
assert r["deployed"] is False
def test_auth_present_error_has_no_urls(self):
r = pdo.parse(AUTH_PRESENT_ERROR)
assert r["live_url"] is None
assert r["claim_url"] is None
assert r["deployed"] is False
class TestRealWorldOutput:
"""Regression: real wrangler output uses tab-indent + multi-word account names."""
REAL = (
"⛅️ wrangler 4.103.0\n"
"Continuing means you accept Cloudflare's Terms of Service and Privacy Policy.\n"
"Solving proof-of-work challenge…\n"
"Temporary account ready:\n"
"\tAccount: Serene Temple (created)\n"
"\tClaim within: 60 minutes\n"
"\tClaim URL: https://dash.cloudflare.com/claim-preview?claimToken=fxLzyAD-vlTzMQmClpg\n"
"Total Upload: 0.19 KiB / gzip: 0.16 KiB\n"
"Uploaded hermes-temp-hello (0.74 sec)\n"
"Deployed hermes-temp-hello triggers (0.42 sec)\n"
" https://hermes-temp-hello.serene-temple.workers.dev\n"
)
def test_multiword_account_name(self):
r = pdo.parse(self.REAL)
assert r["account"] == "Serene Temple"
assert r["account_state"] == "created"
def test_all_fields_from_real_output(self):
r = pdo.parse(self.REAL)
assert r["live_url"] == "https://hermes-temp-hello.serene-temple.workers.dev"
assert r["claim_url"].endswith("claimToken=fxLzyAD-vlTzMQmClpg")
assert r["expires_minutes"] == 60
assert r["deployed"] is True
class TestUrlHygiene:
def test_trailing_punctuation_stripped(self):
text = "Deployed\n see https://w.acct.workers.dev. for details"
assert pdo.parse(text)["live_url"] == "https://w.acct.workers.dev"
class TestCli:
def test_selftest_exits_zero(self):
assert pdo.main(["--selftest"]) == 0
def test_main_prints_json_and_exit_zero_on_live(self, capsys):
with mock.patch.object(sys.stdin, "read", return_value=CREATED):
rc = pdo.main([])
out = json.loads(capsys.readouterr().out)
assert rc == 0
assert out["live_url"] == "https://my-worker.swift-otter.workers.dev"
if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-q"]))
+132
View File
@@ -0,0 +1,132 @@
"""Invariant tests for the bundled comfyui skill.
Covers optional-skills/creative/comfyui — the diffusion workflow runner. Tests assert
contracts (locale-independent file reads), not snapshots of skill content.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import textwrap
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent.parent
SCRIPTS = REPO / "optional-skills" / "creative" / "comfyui" / "scripts"
# Text reads that must not depend on the host locale. The workflow and schema
# JSON are user-authored files (exported by ComfyUI or hand-edited), so they
# are read BOM-tolerantly: Notepad prepends U+FEFF, which makes json.load
# raise JSONDecodeError. See the jobs.json regression in #66607. The /proc
# reads are Linux-gated and never carry a BOM, so they pin plain utf-8.
_ENCODING_SENSITIVE_READS = [
("hardware_check.py", 'with open("/proc/version", "r", encoding="utf-8") as fh:'),
("hardware_check.py", 'with open("/proc/meminfo", "r", encoding="utf-8") as fh:'),
("run_workflow.py", 'with open(schema_path, encoding="utf-8-sig") as f:'),
("run_workflow.py", 'with wf_path.open(encoding="utf-8-sig") as f:'),
# Sibling call paths: every other script that parses a user-authored
# workflow JSON reads it the same BOM-tolerant way (same bug class).
("auto_fix_deps.py", 'wf_path.open(encoding="utf-8-sig")'),
("check_deps.py", 'wf_path.open(encoding="utf-8-sig")'),
("extract_schema.py", 'wf_path.open(encoding="utf-8-sig")'),
("health_check.py", 'wf_path.open(encoding="utf-8-sig")'),
("run_batch.py", 'wf_path.open(encoding="utf-8-sig")'),
]
@pytest.mark.parametrize("rel_path,expected", _ENCODING_SENSITIVE_READS)
def test_readers_are_locale_independent(rel_path, expected):
"""Every text read of a user-supplied or system file pins its codec."""
source = (SCRIPTS / rel_path).read_text(encoding="utf-8")
assert expected in source, f"{rel_path}: locale-dependent read of a UTF-8 payload"
def _run_under_c_locale(snippet: str) -> subprocess.CompletedProcess:
"""Execute a snippet in a child interpreter forced to a non-UTF-8 locale.
The default text codec is resolved at interpreter startup, so the locale
has to be set on the child's environment. Patching os.environ in-process
would not change locale.getpreferredencoding(). PYTHONUTF8=0 disables
PEP 540 UTF-8 mode, which would otherwise mask the bug entirely.
"""
env = dict(os.environ)
env.update({
"LC_ALL": "C",
"LANG": "C",
"PYTHONUTF8": "0",
"PYTHONIOENCODING": "utf-8",
})
return subprocess.run(
[sys.executable, "-c", snippet],
capture_output=True,
text=True,
env=env,
timeout=60,
)
def test_load_schema_reads_non_ascii_under_non_utf8_locale(tmp_path):
"""A schema with non-ASCII labels loads under the C locale.
Without the explicit encoding the C-locale default codec is ASCII, so
json.load crashes with UnicodeDecodeError on any CJK/Cyrillic label.
"""
schema_path = tmp_path / "schema.json"
schema_path.write_bytes(
json.dumps(
{"prompt": {"label": "プロンプト", "type": "string"}},
ensure_ascii=False,
).encode("utf-8")
)
result = _run_under_c_locale(
textwrap.dedent(
f"""
import sys
sys.path.insert(0, {str(SCRIPTS)!r})
from run_workflow import load_schema
schema = load_schema({str(schema_path)!r}, {{}})
assert schema["prompt"]["label"] == "\\u30d7\\u30ed\\u30f3\\u30d7\\u30c8", schema
print("SUCCESS")
"""
)
)
assert result.returncode == 0, (
f"load_schema failed under non-UTF-8 locale:\n{result.stderr}"
)
assert "SUCCESS" in result.stdout
def test_load_schema_tolerates_utf8_bom(tmp_path):
"""A schema saved by a Windows GUI editor (UTF-8 BOM) still parses.
json.load rejects a leading U+FEFF with JSONDecodeError, so a BOM-blind
read turns "user edited the file in Notepad" into a hard failure.
"""
schema_path = tmp_path / "schema.json"
schema_path.write_bytes(
b"\xef\xbb\xbf" + json.dumps({"prompt": {"type": "string"}}).encode("utf-8")
)
result = _run_under_c_locale(
textwrap.dedent(
f"""
import sys
sys.path.insert(0, {str(SCRIPTS)!r})
from run_workflow import load_schema
schema = load_schema({str(schema_path)!r}, {{}})
assert schema["prompt"]["type"] == "string", schema
print("SUCCESS")
"""
)
)
assert result.returncode == 0, (
f"load_schema rejected a BOM-prefixed schema:\n{result.stderr}"
)
assert "SUCCESS" in result.stdout
@@ -0,0 +1,111 @@
"""Tests for the competitor-news-monitor skill and competitor-watch blueprint."""
import re
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parents[2]
SKILL_PATH = (
REPO_ROOT / "skills" / "research" / "competitor-news-monitor" / "SKILL.md"
)
def _frontmatter_and_body():
content = SKILL_PATH.read_text(encoding="utf-8")
assert content.startswith("---")
m = re.search(r"\n---\s*\n", content[3:])
assert m, "frontmatter must close with ---"
fm = yaml.safe_load(content[3 : m.start() + 3])
body = content[m.end() + 3 :]
return fm, body
def test_skill_file_exists():
assert SKILL_PATH.is_file()
def test_frontmatter_required_fields():
fm, _ = _frontmatter_and_body()
for field in ("name", "description", "version", "author", "license", "platforms"):
assert field in fm, f"missing frontmatter field: {field}"
assert fm["name"] == "competitor-news-monitor"
def test_description_hardline():
fm, _ = _frontmatter_and_body()
desc = fm["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60"
assert desc.endswith(".")
def test_author_credits_human_first():
fm, _ = _frontmatter_and_body()
assert not fm["author"].startswith("Hermes Agent")
assert "benbarclay" in fm["author"]
def test_related_skills_resolve_in_repo():
fm, _ = _frontmatter_and_body()
for name in fm["metadata"]["hermes"]["related_skills"]:
hits = (
list(REPO_ROOT.glob(f"skills/*/{name}/SKILL.md"))
+ list(REPO_ROOT.glob(f"optional-skills/*/{name}/SKILL.md"))
+ list(REPO_ROOT.glob(f"skills/*/*/{name}/SKILL.md"))
)
assert hits, f"related_skills entry does not resolve in-repo: {name}"
def test_no_phantom_skill_references():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "change-monitor-and-notify" not in content, "phantom skill ref must be gone"
def test_setup_tick_split():
_, body = _frontmatter_and_body()
assert "Setup (foreground, once)" in body
assert "Tick (each scheduled run)" in body
assert "cronjob(action=" in body, "must wire scheduling through the cronjob tool"
def test_coverage_honesty_discipline():
_, body = _frontmatter_and_body()
assert "unknown coverage" in body, "source failure != no news"
assert "cutoff advance" in body.replace("advances", "advance").replace(
"advanced", "advance"
), "cutoff must only advance on success"
def test_steps_have_completion_criteria():
_, body = _frontmatter_and_body()
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## )", body, re.MULTILINE | re.DOTALL)
assert len(steps) >= 5
for step in steps:
assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}"
def test_no_machine_local_paths():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "/home/" not in content
def test_competitor_watch_blueprint_registered():
from cron.blueprint_catalog import CATALOG
bp = next((b for b in CATALOG if b.key == "competitor-watch"), None)
assert bp is not None, "competitor-watch blueprint missing from catalog"
assert "competitor-news-monitor" in bp.skills
slot_names = {s.name for s in bp.slots}
assert {"companies", "categories", "time", "recurrence", "deliver"} <= slot_names
assert "[SILENT]" in bp.prompt_template
assert "{companies}" in bp.prompt_template and "{categories}" in bp.prompt_template
def test_every_blueprint_skill_resolves_in_repo():
from cron.blueprint_catalog import CATALOG
for bp in CATALOG:
for skill_name in bp.skills:
hits = list(REPO_ROOT.glob(f"skills/*/{skill_name}/SKILL.md")) + list(
REPO_ROOT.glob(f"skills/*/*/{skill_name}/SKILL.md")
)
assert hits, f"blueprint {bp.key!r} loads nonexistent skill {skill_name!r}"
@@ -0,0 +1,81 @@
"""
Smoke tests for the darwinian-evolver optional skill.
We can't actually run the evolution loop in CI (it needs network + a paid LLM),
so these tests verify:
- SKILL.md frontmatter conforms to the hardline format
- shipped scripts parse as valid Python
- the scripts reference the right env var / module paths
"""
from __future__ import annotations
import ast
import re
from pathlib import Path
import pytest
import yaml
SKILL_DIR = Path(__file__).resolve().parents[2] / "optional-skills" / "research" / "darwinian-evolver"
@pytest.fixture(scope="module")
def frontmatter() -> dict:
src = (SKILL_DIR / "SKILL.md").read_text()
m = re.search(r"^---\n(.*?)\n---", src, re.DOTALL)
assert m, "SKILL.md missing YAML frontmatter"
return yaml.safe_load(m.group(1))
def test_skill_dir_exists() -> None:
assert SKILL_DIR.is_dir(), f"missing skill dir: {SKILL_DIR}"
def test_description_under_60_chars(frontmatter) -> None:
desc = frontmatter["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars (hardline ≤60): {desc!r}"
def test_platforms_excludes_windows(frontmatter) -> None:
# Upstream uses func_timeout (POSIX signals) and uv subprocess pipelines; the
# skill is gated [linux, macos]. If we ever port to Windows, update this test
# to assert ["linux", "macos", "windows"].
assert "windows" not in frontmatter["platforms"]
assert set(frontmatter["platforms"]) >= {"linux", "macos"}
def test_author_credits_contributor(frontmatter) -> None:
author = frontmatter["author"]
assert "Bihruze" in author, f"author should credit the original contributor: {author!r}"
@pytest.mark.parametrize(
"path",
[
"scripts/parrot_openrouter.py",
"scripts/show_snapshot.py",
"templates/custom_problem_template.py",
],
)
def test_shipped_scripts_parse(path: str) -> None:
src = (SKILL_DIR / path).read_text()
ast.parse(src) # raises SyntaxError on broken Python
def test_parrot_script_uses_openrouter() -> None:
src = (SKILL_DIR / "scripts" / "parrot_openrouter.py").read_text()
assert "OPENROUTER_API_KEY" in src, "parrot driver should read OPENROUTER_API_KEY"
assert "openrouter.ai/api/v1" in src, "parrot driver should target OpenRouter"
assert "EVOLVER_MODEL" in src, "model should be overridable via EVOLVER_MODEL"
@@ -0,0 +1,62 @@
"""Tests for optional-skills/productivity/decision-questionnaire."""
import re
from pathlib import Path
import pytest
import yaml
SKILL_MD = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "productivity"
/ "decision-questionnaire"
/ "SKILL.md"
)
def _frontmatter():
text = SKILL_MD.read_text(encoding="utf-8")
m = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
assert m, "SKILL.md missing YAML frontmatter"
return yaml.safe_load(m.group(1))
class TestFrontmatter:
def test_name_matches_directory(self):
assert _frontmatter()["name"] == "decision-questionnaire"
def test_description_length_and_period(self):
desc = _frontmatter()["description"]
assert len(desc) <= 60
assert desc.endswith(".")
def test_license_and_platforms(self):
fm = _frontmatter()
assert fm["license"] == "MIT"
assert set(fm["platforms"]) == {"linux", "macos", "windows"}
class TestBody:
def _body(self):
return SKILL_MD.read_text(encoding="utf-8")
def test_template_sections_present(self):
body = self._body()
for section in ("## Context", "## How to answer", "## Anything else?"):
assert section in body, f"template missing {section}"
def test_output_filename_convention(self):
assert "decision-questionnaire-<slug>.md" in self._body()
def test_interview_the_send_principle(self):
assert "Interview the Send" in self._body()
def test_no_upstream_harness_residue(self):
lower = self._body().lower()
for token in ("claude", "slash command", "disable-model-invocation"):
assert token not in lower, f"upstream residue: {token}"
if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-q"]))
@@ -0,0 +1,81 @@
"""Tests for the document-to-action-items optional skill."""
import re
from pathlib import Path
import yaml
SKILL_PATH = (
Path(__file__).resolve().parents[2]
/ "skills"
/ "productivity"
/ "document-to-action-items"
/ "SKILL.md"
)
def _frontmatter_and_body():
content = SKILL_PATH.read_text(encoding="utf-8")
assert content.startswith("---")
m = re.search(r"\n---\s*\n", content[3:])
assert m, "frontmatter must close with ---"
fm = yaml.safe_load(content[3 : m.start() + 3])
body = content[m.end() + 3 :]
return fm, body
def test_skill_file_exists():
assert SKILL_PATH.is_file()
def test_frontmatter_required_fields():
fm, _ = _frontmatter_and_body()
for field in ("name", "description", "version", "author", "license", "platforms"):
assert field in fm, f"missing frontmatter field: {field}"
assert fm["name"] == "document-to-action-items"
hermes = fm["metadata"]["hermes"]
assert hermes["tags"]
assert "related_skills" in hermes
def test_description_hardline():
fm, _ = _frontmatter_and_body()
desc = fm["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60"
assert desc.endswith(".")
def test_author_credits_human_first():
fm, _ = _frontmatter_and_body()
assert not fm["author"].startswith("Hermes Agent"), "human contributor must be credited first"
assert "benbarclay" in fm["author"]
def test_related_skills_resolve_in_repo():
fm, _ = _frontmatter_and_body()
repo_root = SKILL_PATH.parents[3]
for name in fm["metadata"]["hermes"]["related_skills"]:
hits = list(repo_root.glob(f"skills/*/{name}/SKILL.md")) + list(
repo_root.glob(f"optional-skills/*/{name}/SKILL.md")
) + list(repo_root.glob(f"skills/*/*/{name}/SKILL.md"))
assert hits, f"related_skills entry does not resolve in-repo: {name}"
def test_body_structure_and_size():
_, body = _frontmatter_and_body()
for section in ("## When to Use", "## Procedure", "## Pitfalls", "## Verification"):
assert section in body, f"missing section: {section}"
assert len(SKILL_PATH.read_text(encoding="utf-8")) <= 100_000
def test_no_machine_local_paths():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "/home/" not in content
assert not re.search(r"[A-Z]:\\\\Users", content)
def test_steps_have_completion_criteria():
_, body = _frontmatter_and_body()
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## )", body, re.MULTILINE | re.DOTALL)
assert len(steps) >= 5
for step in steps:
assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}"
@@ -0,0 +1,89 @@
"""Tests for the email-inbox-triage bundled skill."""
import re
from pathlib import Path
import yaml
SKILL_PATH = (
Path(__file__).resolve().parents[2]
/ "skills"
/ "email"
/ "email-inbox-triage"
/ "SKILL.md"
)
def _frontmatter_and_body():
content = SKILL_PATH.read_text(encoding="utf-8")
assert content.startswith("---")
m = re.search(r"\n---\s*\n", content[3:])
assert m, "frontmatter must close with ---"
fm = yaml.safe_load(content[3 : m.start() + 3])
body = content[m.end() + 3 :]
return fm, body
def test_skill_file_exists():
assert SKILL_PATH.is_file()
def test_frontmatter_required_fields():
fm, _ = _frontmatter_and_body()
for field in ("name", "description", "version", "author", "license", "platforms"):
assert field in fm, f"missing frontmatter field: {field}"
assert fm["name"] == "email-inbox-triage"
hermes = fm["metadata"]["hermes"]
assert hermes["tags"]
assert "related_skills" in hermes
def test_description_hardline():
fm, _ = _frontmatter_and_body()
desc = fm["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60"
assert desc.endswith(".")
def test_author_credits_human_first():
fm, _ = _frontmatter_and_body()
assert not fm["author"].startswith("Hermes Agent"), "human contributor must be credited first"
assert "benbarclay" in fm["author"]
def test_related_skills_resolve_in_repo():
fm, _ = _frontmatter_and_body()
repo_root = SKILL_PATH.parents[3]
for name in fm["metadata"]["hermes"]["related_skills"]:
hits = (
list(repo_root.glob(f"skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"optional-skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"skills/*/*/{name}/SKILL.md"))
)
assert hits, f"related_skills entry does not resolve in-repo: {name}"
def test_body_structure_and_size():
_, body = _frontmatter_and_body()
for section in ("## When to Use", "## Procedure", "## Pitfalls", "## Verification"):
assert section in body, f"missing section: {section}"
assert len(SKILL_PATH.read_text(encoding="utf-8")) <= 100_000
def test_no_machine_local_paths():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "/home/" not in content
assert not re.search(r"[A-Z]:\\\\Users", content)
def test_steps_have_completion_criteria():
_, body = _frontmatter_and_body()
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## )", body, re.MULTILINE | re.DOTALL)
assert len(steps) >= 5
for step in steps:
assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}"
def test_mutation_boundary_is_default_safe():
_, body = _frontmatter_and_body()
assert "read + draft" in body, "scope step must default to read+draft, not send/delete"
assert "does not imply permission" in body
+56
View File
@@ -0,0 +1,56 @@
"""Tests for skills/media/youtube-content/scripts/fetch_transcript.py (issue #22243)."""
import sys
from pathlib import Path
from unittest import mock
import pytest
SCRIPTS_DIR = Path(__file__).resolve().parents[2] / "skills" / "media" / "youtube-content" / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))
import fetch_transcript
class TestExtractVideoId:
def test_standard_watch_url(self):
assert fetch_transcript.extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ") == "dQw4w9WgXcQ"
def test_short_url(self):
assert fetch_transcript.extract_video_id("https://youtu.be/dQw4w9WgXcQ") == "dQw4w9WgXcQ"
def test_shorts_url(self):
assert fetch_transcript.extract_video_id("https://www.youtube.com/shorts/dQw4w9WgXcQ") == "dQw4w9WgXcQ"
def test_with_extra_params(self):
assert fetch_transcript.extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42") == "dQw4w9WgXcQ"
class TestFormatTimestamp:
def test_seconds_only(self):
assert fetch_transcript.format_timestamp(90) == "1:30"
def test_zero(self):
assert fetch_transcript.format_timestamp(0) == "0:00"
def test_minutes_only(self):
assert fetch_transcript.format_timestamp(600) == "10:00"
class TestPyprojectDeclaresYoutubeExtra:
def test_youtube_extra_declared_in_pyproject(self):
"""youtube-transcript-api must be listed in pyproject.toml [youtube] extra (issue #22243)."""
import tomllib
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
with pyproject_path.open("rb") as f:
data = tomllib.load(f)
extras = data.get("project", {}).get("optional-dependencies", {})
assert "youtube" in extras, "Missing [youtube] extra in pyproject.toml"
youtube_deps = " ".join(extras["youtube"])
assert "youtube-transcript-api" in youtube_deps
@@ -0,0 +1,101 @@
"""Regression tests for Tirith-safe GitHub credential extraction (#22722)."""
from pathlib import Path
import subprocess
import sys
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
HELPER = REPO_ROOT / "skills/software-development/github/scripts/git-credential-token.py"
LEGACY_SED = r"sed 's|https://[^:]*:\([^@]*\)@.*|\1|'"
SHIPPED_TREES = (
REPO_ROOT / "skills/software-development/github",
REPO_ROOT / "website/docs/user-guide/skills/bundled/software-development",
REPO_ROOT
/ "website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/software-development",
)
def _extract(path: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(HELPER), str(path)],
capture_output=True,
text=True,
check=False,
)
def _credential_file(tmp_path: Path, value: str) -> Path:
credentials = tmp_path / "credentials"
credentials.write_text(value, encoding="utf-8", newline="")
return credentials
@pytest.mark.parametrize(
("credential", "token"),
[
("https://octocat:password-form-token@github.com\n", "password-form-token"),
("https://oauth-token:x-oauth-basic@github.com\n", "oauth-token"),
("https://ghp_token_only@github.com\n", "ghp_token_only"),
("https://github_pat_token_only@github.com\n", "github_pat_token_only"),
],
)
def test_extracts_supported_git_credential_url_forms(tmp_path, credential, token):
result = _extract(_credential_file(tmp_path, credential))
assert result.returncode == 0
assert result.stdout == f"{token}\n"
assert result.stderr == ""
def test_extracts_password_from_exact_github_https_credential(tmp_path):
credentials = _credential_file(
tmp_path,
"https://ignored:wrong@example.com\n"
"https://octocat:secret%2Ftoken@github.com\n",
)
result = _extract(credentials)
assert result.returncode == 0
assert result.stdout == "secret/token\n"
assert result.stderr == ""
@pytest.mark.parametrize(
"credential",
[
"https://octocat:stolen@github.com.attacker.example\n",
"https://octocat@github.com\n",
"https://%6fctocat@github.com\n",
"https://octocat:token@github.com%2eattacker.example\n",
"https://octocat:token%0D%0AX-Injected%3Ayes@github.com\n",
"https://ghp_token%0Ainjected@github.com\n",
"https://octocat:token%00suffix@github.com\n",
"https://octocat:token%09suffix@github.com\n",
"https://octocat:token%C2%85suffix@github.com\n",
"https://ghp_token%1Fsuffix@github.com\n",
"https://ghp_token%C2%9Fsuffix@github.com\n",
"https://octocat:bad%ZZtoken@github.com\n",
"https://octocat:token@github.com:bogus\n",
"http://octocat:token@github.com\n",
],
)
def test_rejects_ambiguous_lookalike_or_malformed_credentials(tmp_path, credential):
result = _extract(_credential_file(tmp_path, credential))
assert result.returncode == 1
assert result.stdout == ""
assert result.stderr == ""
def test_bundled_github_skills_and_docs_do_not_ship_legacy_sed_url_regex():
offenders = []
for tree in SHIPPED_TREES:
for path in tree.rglob("*"):
if path.suffix in {".md", ".sh", ".py"} and LEGACY_SED in path.read_text(encoding="utf-8"):
offenders.append(str(path.relative_to(REPO_ROOT)))
assert offenders == []
+130
View File
@@ -0,0 +1,130 @@
"""Tests for the github merged skill (formerly six github-* skills).
The issue-to-pr workflow (originally the github-issue-to-pr skill,
author benbarclay) now lives complete in references/issue-to-pr.md of the
merged software-development/github skill. Frontmatter contracts apply to
the merged SKILL.md; the content pins that guarded the issue-to-pr
disciplines now check the reference body.
"""
import re
from pathlib import Path
import yaml
SKILL_DIR = (
Path(__file__).resolve().parents[2]
/ "skills"
/ "software-development"
/ "github"
)
SKILL_PATH = SKILL_DIR / "SKILL.md"
ISSUE_TO_PR_REF = SKILL_DIR / "references" / "issue-to-pr.md"
def _frontmatter_and_body():
content = SKILL_PATH.read_text(encoding="utf-8")
assert content.startswith("---")
m = re.search(r"\n---\s*\n", content[3:])
assert m, "frontmatter must close with ---"
fm = yaml.safe_load(content[3 : m.start() + 3])
body = content[m.end() + 3 :]
return fm, body
def test_skill_file_exists():
assert SKILL_PATH.is_file()
def test_all_workflow_references_exist():
for ref in (
"auth.md",
"issues.md",
"pr-workflow.md",
"issue-to-pr.md",
"code-review.md",
"repo-management.md",
):
assert (SKILL_DIR / "references" / ref).is_file(), f"missing reference: {ref}"
def test_frontmatter_required_fields():
fm, _ = _frontmatter_and_body()
for field in ("name", "description", "version", "author", "license", "platforms"):
assert field in fm, f"missing frontmatter field: {field}"
assert fm["name"] == "github"
hermes = fm["metadata"]["hermes"]
assert hermes["tags"]
assert "related_skills" in hermes
def test_description_hardline():
fm, _ = _frontmatter_and_body()
desc = fm["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60"
assert desc.endswith(".")
def test_author_credits_human_first():
fm, _ = _frontmatter_and_body()
assert not fm["author"].startswith("Hermes Agent"), "human contributor must be credited first"
assert "benbarclay" in fm["author"]
def test_related_skills_resolve_in_repo():
fm, _ = _frontmatter_and_body()
repo_root = SKILL_PATH.parents[3]
for name in fm["metadata"]["hermes"]["related_skills"]:
hits = (
list(repo_root.glob(f"skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"optional-skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"skills/*/*/{name}/SKILL.md"))
)
assert hits, f"related_skills entry does not resolve in-repo: {name}"
def test_body_routes_every_workflow():
_, body = _frontmatter_and_body()
for ref in (
"references/auth.md",
"references/issues.md",
"references/pr-workflow.md",
"references/issue-to-pr.md",
"references/code-review.md",
"references/repo-management.md",
):
assert ref in body, f"routing table missing: {ref}"
assert len(SKILL_PATH.read_text(encoding="utf-8")) <= 100_000
def test_no_machine_local_paths():
for p in (SKILL_PATH, ISSUE_TO_PR_REF):
content = p.read_text(encoding="utf-8")
assert "/home/" not in content
assert not re.search(r"[A-Z]:\\\\Users", content)
def test_issue_to_pr_steps_have_completion_criteria():
body = ISSUE_TO_PR_REF.read_text(encoding="utf-8")
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## |\Z)", body, re.MULTILINE | re.DOTALL)
assert len(steps) >= 6
for step in steps:
assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}"
def test_issue_to_pr_core_disciplines_present():
"""The learnings folded in from maintainer practice must survive edits."""
body = ISSUE_TO_PR_REF.read_text(encoding="utf-8")
assert "--comments" in body, "must read the full issue thread"
assert "pr list --search" in body, "must sweep for duplicate PRs before coding"
assert re.search(r"git log -p -S", body), "must check design intent via history"
assert "sabotage" in body.lower() or "FAILS" in body, "must prove the regression test bites"
assert "sibling" in body, "must fix the class, not the site"
assert "dispatches CI" in body, "must open the PR immediately after work exists"
def test_issue_to_pr_not_a_router():
"""Reference steps must carry their own procedure, not route elsewhere."""
body = ISSUE_TO_PR_REF.read_text(encoding="utf-8")
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## |\Z)", body, re.MULTILINE | re.DOTALL)
routing = [s for s in steps if re.match(r"^### \d+\.[^\n]*\n+Load `", s)]
assert len(routing) == 0, "steps must not open by delegating to another skill"
+197
View File
@@ -0,0 +1,197 @@
"""Tests for Google Workspace gws bridge and CLI wrapper."""
import importlib.util
import json
import subprocess
import sys
import types
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
BRIDGE_PATH = (
Path(__file__).resolve().parents[2]
/ "skills/productivity/google-workspace/scripts/gws_bridge.py"
)
API_PATH = (
Path(__file__).resolve().parents[2]
/ "skills/productivity/google-workspace/scripts/google_api.py"
)
@pytest.fixture
def bridge_module(monkeypatch, tmp_path):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
spec = importlib.util.spec_from_file_location("gws_bridge_test", BRIDGE_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
@pytest.fixture
def api_module(monkeypatch, tmp_path):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
spec = importlib.util.spec_from_file_location("gws_api_test", API_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
# Ensure the gws CLI code path is taken even when the binary isn't
# installed (CI). Without this, calendar_list() falls through to the
# Python SDK path which imports ``googleapiclient`` — not in deps.
module._gws_binary = lambda: "/usr/bin/gws"
# Bypass authentication check — no real token file in CI.
module._ensure_authenticated = lambda: None
return module
def _write_token(path: Path, *, token="ya29.test", expiry=None, **extra):
data = {
"token": token,
"refresh_token": "1//refresh",
"client_id": "123.apps.googleusercontent.com",
"client_secret": "secret",
"token_uri": "https://oauth2.googleapis.com/token",
**extra,
}
if expiry is not None:
data["expiry"] = expiry
path.write_text(json.dumps(data))
def test_bridge_returns_valid_token(bridge_module, tmp_path):
"""Non-expired token is returned without refresh."""
future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
token_path = bridge_module.get_token_path()
_write_token(token_path, token="ya29.valid", expiry=future)
result = bridge_module.get_valid_token()
assert result == "ya29.valid"
def test_bridge_main_injects_token_env(bridge_module, tmp_path):
"""main() sets GOOGLE_WORKSPACE_CLI_TOKEN in subprocess env."""
future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
token_path = bridge_module.get_token_path()
_write_token(token_path, token="ya29.injected", expiry=future)
captured = {}
def capture_run(cmd, **kwargs):
captured["cmd"] = cmd
captured["env"] = kwargs.get("env", {})
return MagicMock(returncode=0)
with patch.object(sys, "argv", ["gws_bridge.py", "gmail", "+triage"]):
with patch.object(subprocess, "run", side_effect=capture_run):
with pytest.raises(SystemExit):
bridge_module.main()
assert captured["env"]["GOOGLE_WORKSPACE_CLI_TOKEN"] == "ya29.injected"
assert captured["cmd"] == ["gws", "gmail", "+triage"]
def test_api_calendar_list_uses_events_list(api_module):
"""calendar_list calls _run_gws with events list + params."""
captured = {}
def capture_run(cmd, **kwargs):
captured["cmd"] = cmd
return MagicMock(returncode=0, stdout="{}", stderr="")
args = api_module.argparse.Namespace(
start="", end="", max=25, calendar="primary", func=api_module.calendar_list,
)
with patch.object(api_module.subprocess, "run", side_effect=capture_run):
api_module.calendar_list(args)
cmd = captured["cmd"]
# _gws_binary() returns "/usr/bin/gws", so cmd[0] is that binary
assert cmd[0] == "/usr/bin/gws"
assert "calendar" in cmd
assert "events" in cmd
assert "list" in cmd
assert "--params" in cmd
params = json.loads(cmd[cmd.index("--params") + 1])
assert "timeMin" in params
assert "timeMax" in params
assert params["calendarId"] == "primary"
def test_api_get_credentials_refresh_persists_authorized_user_type(api_module, monkeypatch):
token_path = api_module.TOKEN_PATH
_write_token(token_path, token="ya29.old")
class FakeCredentials:
def __init__(self):
self.expired = True
self.refresh_token = "1//refresh"
self.valid = True
def refresh(self, request):
self.expired = False
def to_json(self):
return json.dumps({
"token": "ya29.refreshed",
"refresh_token": "1//refresh",
"client_id": "123.apps.googleusercontent.com",
"client_secret": "secret",
"token_uri": "https://oauth2.googleapis.com/token",
})
class FakeCredentialsModule:
@staticmethod
def from_authorized_user_file(filename, scopes):
assert filename == str(token_path)
assert scopes == api_module.SCOPES
return FakeCredentials()
google_module = types.ModuleType("google")
oauth2_module = types.ModuleType("google.oauth2")
credentials_module = types.ModuleType("google.oauth2.credentials")
credentials_module.Credentials = FakeCredentialsModule
transport_module = types.ModuleType("google.auth.transport")
requests_module = types.ModuleType("google.auth.transport.requests")
requests_module.Request = lambda: object()
monkeypatch.setitem(sys.modules, "google", google_module)
monkeypatch.setitem(sys.modules, "google.oauth2", oauth2_module)
monkeypatch.setitem(sys.modules, "google.oauth2.credentials", credentials_module)
monkeypatch.setitem(sys.modules, "google.auth.transport", transport_module)
monkeypatch.setitem(sys.modules, "google.auth.transport.requests", requests_module)
creds = api_module.get_credentials()
saved = json.loads(token_path.read_text())
assert isinstance(creds, FakeCredentials)
assert saved["token"] == "ya29.refreshed"
assert saved["type"] == "authorized_user"
@@ -0,0 +1,73 @@
"""Regression test: google-workspace SKILL.md must declare required_credential_files.
PR #9931 accidentally removed the required_credential_files header, which broke
credential file mounting in Docker/Modal remote backends (#16452). This test
prevents the regression from silently reappearing.
"""
from __future__ import annotations
import os
from pathlib import Path
from unittest.mock import patch
SKILL_MD = (
Path(__file__).resolve().parents[2]
/ "skills/productivity/google-workspace/SKILL.md"
)
_EXPECTED_PATHS = {"google_token.json", "google_client_secret.json"}
def _parse_frontmatter(content: str) -> dict:
from agent.skill_utils import parse_frontmatter
fm, _ = parse_frontmatter(content)
return fm
class TestGoogleWorkspaceCredentialFiles:
def test_required_credential_files_present_in_skill_md(self):
content = SKILL_MD.read_text(encoding="utf-8")
fm = _parse_frontmatter(content)
entries = fm.get("required_credential_files")
assert entries, "required_credential_files missing from google-workspace SKILL.md"
assert isinstance(entries, list), "required_credential_files must be a list"
paths = {
(e["path"] if isinstance(e, dict) else e)
for e in entries
}
assert _EXPECTED_PATHS <= paths, (
f"Missing entries in required_credential_files: {_EXPECTED_PATHS - paths}"
)
def test_entries_are_registered_when_files_exist(self, tmp_path):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "google_token.json").write_text("{}")
(hermes_home / "google_client_secret.json").write_text("{}")
from tools.credential_files import (
clear_credential_files,
get_credential_file_mounts,
register_credential_files,
)
clear_credential_files()
try:
content = SKILL_MD.read_text(encoding="utf-8")
fm = _parse_frontmatter(content)
entries = fm.get("required_credential_files", [])
with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}):
missing = register_credential_files(entries)
assert missing == [], f"Unexpected missing files: {missing}"
mounts = get_credential_file_mounts()
container_paths = {m["container_path"] for m in mounts}
assert "/root/.hermes/google_token.json" in container_paths
assert "/root/.hermes/google_client_secret.json" in container_paths
finally:
clear_credential_files()
@@ -0,0 +1,59 @@
"""Tests for the google-workspace daily-brief reference."""
import re
from pathlib import Path
GWS_DIR = (
Path(__file__).resolve().parents[2]
/ "skills"
/ "productivity"
/ "google-workspace"
)
REF_PATH = GWS_DIR / "references" / "daily-brief.md"
SKILL_PATH = GWS_DIR / "SKILL.md"
def test_reference_exists():
assert REF_PATH.is_file()
def test_skill_md_points_at_reference():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "references/daily-brief.md" in content, "SKILL.md must list the reference"
refs_section = content.split("## References", 1)[1].split("##", 1)[0]
assert "daily-brief" in refs_section
def test_reference_carries_load_trigger():
content = REF_PATH.read_text(encoding="utf-8")
assert "morning brief" in content.lower(), "reference must state when to load it"
def test_reference_credits_contributor():
content = REF_PATH.read_text(encoding="utf-8")
assert "benbarclay" in content
def test_steps_have_completion_criteria():
content = REF_PATH.read_text(encoding="utf-8")
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## )", content, re.MULTILINE | re.DOTALL)
assert len(steps) >= 5
for step in steps:
assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}"
def test_core_disciplines_present():
content = REF_PATH.read_text(encoding="utf-8")
assert "half-open window" in content or "[day_start, next_day_start)" in content
assert "no preparation found" in content
assert "not authorization to mutate" in content
def test_no_machine_local_paths():
content = REF_PATH.read_text(encoding="utf-8")
assert "/home/" not in content
def test_no_standalone_daily_brief_skill():
"""The brief ships as a reference, not a sibling skill."""
repo_root = GWS_DIR.parents[2]
assert not (repo_root / "skills" / "productivity" / "google-workspace-daily-brief").exists()
@@ -0,0 +1,90 @@
"""Security-floor tests for the Google Workspace runtime installer."""
from __future__ import annotations
import importlib.util
from importlib.metadata import PackageNotFoundError
from pathlib import Path
import pytest
SETUP_PATH = (
Path(__file__).resolve().parents[2]
/ "skills/productivity/google-workspace/scripts/setup.py"
)
@pytest.fixture()
def setup_module():
spec = importlib.util.spec_from_file_location(
"test_google_workspace_setup_module",
SETUP_PATH,
)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_stale_google_transitives_are_reported_missing(setup_module, monkeypatch):
installed = {
"google-api-python-client": "2.194.0",
"google-auth": "2.55.0",
"google-auth-oauthlib": "1.3.1",
"google-auth-httplib2": "0.3.1",
"httplib2": "0.31.2",
"pyasn1": "0.6.3",
}
def fake_version(name):
try:
return installed[name]
except KeyError:
raise PackageNotFoundError(name) from None
monkeypatch.setattr(setup_module, "_distribution_version", fake_version)
assert setup_module._missing_required_packages() == [
"google-auth==2.55.1",
"httplib2==0.32.0",
"pyasn1==0.6.4",
]
def test_installer_repairs_stale_transitives(setup_module, monkeypatch):
states = iter(
[
[
"google-auth==2.55.1",
"httplib2==0.32.0",
"pyasn1==0.6.4",
],
[],
]
)
monkeypatch.setattr(
setup_module,
"_missing_required_packages",
lambda: next(states),
)
calls = []
monkeypatch.setattr(
setup_module.subprocess,
"check_call",
lambda argv, **kwargs: calls.append(argv),
)
assert setup_module.install_deps() is True
assert calls == [
[
setup_module.sys.executable,
"-m",
"pip",
"install",
"--quiet",
"google-auth==2.55.1",
"httplib2==0.32.0",
"pyasn1==0.6.4",
]
]
@@ -0,0 +1,181 @@
"""Regression test: google-workspace setup.py REQUIRED_PACKAGES must pin httplib2.
GHSA-j5g9-f88f-gfj3 (HIGH) — Decompression Bomb DoS via unbounded gzip/deflate
response handling. Fixed in httplib2 0.32.0.
There are three install paths for google-workspace dependencies:
1. pyproject.toml [project.optional-dependencies].google
2. tools/lazy_deps.py LAZY_DEPS['skill.google_workspace']
3. skills/productivity/google-workspace/scripts/setup.py REQUIRED_PACKAGES
This test ensures path 3 stays pinned and consistent with the other two.
"""
from __future__ import annotations
import ast
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SETUP_PY = REPO_ROOT / "skills/productivity/google-workspace/scripts/setup.py"
PYPROJECT_TOML = REPO_ROOT / "pyproject.toml"
# ---------------------------------------------------------------------------
# Static parsers
# ---------------------------------------------------------------------------
_GOOGLE_EXTRA_KEY = "google"
_LAZY_DEPS_KEY = "skill.google_workspace"
def _parse_setup_py_required_packages() -> list[str]:
"""Parse setup.py and return the REQUIRED_PACKAGES list."""
tree = ast.parse(SETUP_PY.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "REQUIRED_PACKAGES":
if isinstance(node.value, ast.List):
return [elt.value for elt in node.value.elts if isinstance(elt, ast.Constant)]
raise AssertionError("REQUIRED_PACKAGES not found in setup.py")
def _parse_pyproject_google_extra() -> list[str]:
"""Parse pyproject.toml and return the google extra dependency list."""
try:
import tomllib
except ImportError:
import tomli as tomllib # type: ignore[no-redef]
data = tomllib.loads(PYPROJECT_TOML.read_text(encoding="utf-8"))
optional_deps = data["project"]["optional-dependencies"]
return list(optional_deps[_GOOGLE_EXTRA_KEY])
def _parse_lazy_deps_google_workspace() -> list[str]:
"""Return the real LAZY_DEPS entry for skill.google_workspace."""
from tools.lazy_deps import LAZY_DEPS
return list(LAZY_DEPS[_LAZY_DEPS_KEY])
def _extract_pins(packages: list[str]) -> dict[str, str]:
"""Extract pinned versions: {package_name: version} for entries with == pin."""
pins: dict[str, str] = {}
for pkg in packages:
if "==" in pkg:
name, version = pkg.split("==", 1)
pins[name.strip()] = version.strip()
return pins
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestGoogleWorkspaceSetupDepsPins:
"""Security pin consistency across all three google-workspace install paths."""
def test_setup_py_pins_httplib2(self):
"""setup.py REQUIRED_PACKAGES must pin httplib2 at or above the GHSA fix version."""
packages = _parse_setup_py_required_packages()
pins = _extract_pins(packages)
assert "httplib2" in pins, (
f"httplib2 not found in setup.py REQUIRED_PACKAGES.\n"
f" Current entries: {packages}"
)
# GHSA-j5g9-f88f-gfj3 is fixed in 0.32.0 — floor invariant, not a snapshot,
# so future bumps don't break this test.
pinned = tuple(int(part) for part in pins["httplib2"].split("."))
assert pinned >= (0, 32, 0), (
f"httplib2 pin {pins['httplib2']} in setup.py is below 0.32.0, the "
f"GHSA-j5g9-f88f-gfj3 fix version.\n"
f" Full REQUIRED_PACKAGES: {packages}"
)
def test_setup_py_pins_match_pyproject_toml(self):
"""httplib2 pin in setup.py must match pyproject.toml google extra."""
required_packages = _parse_setup_py_required_packages()
pyproject_packages = _parse_pyproject_google_extra()
required_pins = _extract_pins(required_packages)
pyproject_pins = _extract_pins(pyproject_packages)
for pkg in ("httplib2", "google-api-python-client", "google-auth-oauthlib", "google-auth-httplib2"):
setup_ver = required_pins.get(pkg)
toml_ver = pyproject_pins.get(pkg)
if setup_ver is None and toml_ver is None:
continue # neither path pins it, skip
assert toml_ver is not None, (
f"{pkg} is pinned in setup.py ({setup_ver}) but NOT in pyproject.toml google extra.\n"
f" setup.py: {required_pins}\n"
f" pyproject.toml google: {pyproject_pins}"
)
assert setup_ver is not None, (
f"{pkg} is pinned in pyproject.toml ({toml_ver}) but NOT in setup.py.\n"
f" pyproject.toml google: {pyproject_pins}\n"
f" setup.py: {required_pins}"
)
assert setup_ver == toml_ver, (
f"{pkg} pin mismatch: setup.py has {setup_ver}, pyproject.toml has {toml_ver}.\n"
f" setup.py: {required_pins}\n"
f" pyproject.toml google: {pyproject_pins}"
)
def test_setup_py_pins_match_lazy_deps(self):
"""httplib2 pin in setup.py must match tools/lazy_deps.py skill.google_workspace."""
required_packages = _parse_setup_py_required_packages()
lazy_packages = _parse_lazy_deps_google_workspace()
required_pins = _extract_pins(required_packages)
lazy_pins = _extract_pins(lazy_packages)
for pkg in ("httplib2", "google-api-python-client", "google-auth-oauthlib", "google-auth-httplib2"):
setup_ver = required_pins.get(pkg)
lazy_ver = lazy_pins.get(pkg)
if setup_ver is None and lazy_ver is None:
continue
assert lazy_ver is not None, (
f"{pkg} is pinned in setup.py ({setup_ver}) but NOT in lazy_deps.py.\n"
f" setup.py: {required_pins}\n"
f" lazy_deps.py: {lazy_pins}"
)
assert setup_ver is not None, (
f"{pkg} is pinned in lazy_deps.py ({lazy_ver}) but NOT in setup.py.\n"
f" lazy_deps.py: {lazy_pins}\n"
f" setup.py: {required_pins}"
)
assert setup_ver == lazy_ver, (
f"{pkg} pin mismatch: setup.py has {setup_ver}, lazy_deps.py has {lazy_ver}.\n"
f" setup.py: {required_pins}\n"
f" lazy_deps.py: {lazy_pins}"
)
def test_all_google_packages_are_pinned_in_all_paths(self):
"""Every google workspace package that is version-pinned in any path must appear in all three."""
pyproject_packages = _parse_pyproject_google_extra()
lazy_packages = _parse_lazy_deps_google_workspace()
setup_packages = _parse_setup_py_required_packages()
all_pins: dict[str, set[str]] = {}
for label, pkgs in [
("pyproject.toml", pyproject_packages),
("lazy_deps.py", lazy_packages),
("setup.py", setup_packages),
]:
for pkg in pkgs:
if "==" in pkg:
name, ver = pkg.split("==", 1)
all_pins.setdefault(name.strip(), set()).add(f"{label}={ver.strip()}")
for pkg, entries in sorted(all_pins.items()):
versions = {e.split("=", 1)[1] for e in entries}
assert len(versions) == 1, (
f"{pkg} has inconsistent pins across install paths:\n"
+ "\n".join(f" {e}" for e in sorted(entries))
)
assert len(entries) == 3, (
f"{pkg} is not pinned in all three install paths. Found {len(entries)}/3:\n"
+ "\n".join(f" {e}" for e in sorted(entries))
)
@@ -0,0 +1,606 @@
"""Tests for the grounded-citations bundled skill.
Covers the SKILL.md authoring standards (frontmatter shape, ≤60-char
description) and the behavior of ``scripts/sources.py`` — the citation ledger
that assigns stable ``url -> [n]`` ids, renders Sources blocks, and verifies a
draft's citations. The verify path is the load-bearing piece: it is what
catches a hallucinated or renumbered citation before delivery.
"""
from __future__ import annotations
import importlib.util
import json
import re
from pathlib import Path
import pytest
import yaml
SKILL_DIR = Path(__file__).resolve().parents[2] / "skills" / "research" / "grounded-citations"
SCRIPT = SKILL_DIR / "scripts" / "sources.py"
@pytest.fixture(scope="module")
def frontmatter() -> dict:
src = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
m = re.search(r"^---\n(.*?)\n---", src, re.DOTALL)
assert m, "SKILL.md missing YAML frontmatter"
return yaml.safe_load(m.group(1))
@pytest.fixture(scope="module")
def sources_mod():
spec = importlib.util.spec_from_file_location("gc_sources", SCRIPT)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.fixture
def ledger(tmp_path: Path) -> Path:
return tmp_path / "ledger.json"
# ---------------------------------------------------------------------------
# Authoring standards
# ---------------------------------------------------------------------------
def test_skill_files_present() -> None:
assert (SKILL_DIR / "SKILL.md").is_file()
assert SCRIPT.is_file()
assert (SKILL_DIR / "references" / "citation-formats.md").is_file()
assert (SKILL_DIR / "references" / "grounding-rationale.md").is_file()
def test_description_within_limit(frontmatter: dict) -> None:
desc = frontmatter["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars (limit 60): {desc!r}"
assert desc.endswith(".")
def test_required_frontmatter_fields(frontmatter: dict) -> None:
assert frontmatter["name"] == "grounded-citations"
for field in ("version", "author", "license", "platforms"):
assert frontmatter.get(field), f"missing frontmatter field: {field}"
assert frontmatter["metadata"]["hermes"]["category"] == "research"
def test_skill_body_has_modern_sections() -> None:
body = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
for heading in (
"## When to Use",
"## Prerequisites",
"## How to Run",
"## Quick Reference",
"## Procedure",
"## Pitfalls",
"## Verification",
):
assert heading in body, f"SKILL.md missing section: {heading}"
# ---------------------------------------------------------------------------
# Ledger identity
# ---------------------------------------------------------------------------
def test_ids_are_stable_and_sequential(sources_mod, ledger: Path) -> None:
first = sources_mod.add_sources(ledger, ["https://a.example"])
second = sources_mod.add_sources(ledger, ["https://b.example"])
assert (first[0]["id"], second[0]["id"]) == (1, 2)
again = sources_mod.add_sources(ledger, ["https://a.example"])
assert again[0]["id"] == 1
def test_url_normalization_collapses_fragment_and_trailing_slash(sources_mod, ledger: Path) -> None:
base = sources_mod.add_sources(ledger, ["https://x.example/page"])[0]["id"]
for variant in ("https://x.example/page/", "https://x.example/page#part"):
assert sources_mod.add_sources(ledger, [variant])[0]["id"] == base
def test_query_string_is_significant(sources_mod, ledger: Path) -> None:
a = sources_mod.add_sources(ledger, ["https://x.example/s?q=1"])[0]["id"]
b = sources_mod.add_sources(ledger, ["https://x.example/s?q=2"])[0]["id"]
assert a != b
def test_title_backfills_without_changing_id(sources_mod, ledger: Path) -> None:
first = sources_mod.add_sources(ledger, ["https://t.example"])[0]
assert first["title"] == ""
second = sources_mod.add_sources(ledger, ["https://t.example"], title="Later title")[0]
assert (second["id"], second["title"]) == (first["id"], "Later title")
def test_ingest_walks_search_and_extract_payloads(sources_mod) -> None:
payload = {
"data": {"web": [{"title": "One", "url": "https://n.example/1"}]},
"results": [
{"url": "https://n.example/1", "title": "One again"},
{"url": "https://n.example/2", "title": "Two"},
],
}
pairs = sources_mod.urls_from_json(payload)
assert [u for u, _ in pairs] == ["https://n.example/1", "https://n.example/2"]
def test_ingest_ignores_non_http_values(sources_mod) -> None:
payload = {"url": "file:///etc/passwd", "nested": {"link": "mailto:a@b.c"}}
assert sources_mod.urls_from_json(payload) == []
# ---------------------------------------------------------------------------
# Rendering
# ---------------------------------------------------------------------------
def _seed(sources_mod, ledger: Path) -> list[dict]:
sources_mod.add_sources(ledger, ["https://a.example"], title="Alpha")
sources_mod.add_sources(ledger, ["https://b.example"])
sources_mod.add_sources(ledger, ["https://c.example"], title="Gamma")
return json.loads(ledger.read_text(encoding="utf-8"))["sources"]
def test_render_markdown_lists_ids_and_urls(sources_mod, ledger: Path) -> None:
block = sources_mod.render_sources(_seed(sources_mod, ledger))
assert block.startswith("## Sources")
assert "[1] https://a.example — Alpha" in block
assert "[2] https://b.example" in block
def test_render_only_subset_and_ordering(sources_mod, ledger: Path) -> None:
block = sources_mod.render_sources(_seed(sources_mod, ledger), style="plain", only={3, 1})
lines = [ln for ln in block.splitlines() if ln.startswith("[")]
assert lines[0].startswith("[1]") and lines[1].startswith("[3]")
assert not any(ln.startswith("[2]") for ln in lines)
def test_render_bibtex_keys_match_ids(sources_mod, ledger: Path) -> None:
block = sources_mod.render_sources(_seed(sources_mod, ledger), style="bibtex", only={1})
assert "@misc{source1," in block
assert r"\url{https://a.example}" in block
def test_render_empty_selection_is_empty_string(sources_mod, ledger: Path) -> None:
assert sources_mod.render_sources(_seed(sources_mod, ledger), only=set()) == ""
# ---------------------------------------------------------------------------
# Verification — the guarantee
# ---------------------------------------------------------------------------
def _verify(sources_mod, ledger: Path, tmp_path: Path, text: str, **kw):
draft = tmp_path / "draft.md"
draft.write_text(text, encoding="utf-8")
sources = json.loads(ledger.read_text(encoding="utf-8"))["sources"]
return sources_mod.verify_draft(draft, sources, **kw)
def test_well_formed_draft_passes(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = (
"Ice is less dense than liquid water and floats.[1][2]\n\n"
"Sources:\n[1] https://a.example\n[2] https://b.example\n"
)
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text)
assert (code, errors) == (0, [])
def test_unknown_citation_id_fails(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = "A claim with an invented source id here.[42]\n\nSources:\n[42] https://fake.example\n"
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text)
assert code == 1
# Must be flagged as an inline citation the ledger never issued — not merely
# as a stray Sources-block line, which is a separate (weaker) error.
assert any("hallucinated or renumbered" in e for e in errors), errors
def test_sources_block_url_must_match_ledger(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = "A real claim carrying a real id.[1]\n\nSources:\n[1] https://wrong.example\n"
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text)
assert code == 1
assert any("does not match the ledger" in e for e in errors)
def test_missing_sources_block_fails(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
code, errors, _ = _verify(sources_mod, ledger, tmp_path, "A real claim carrying an id.[1]\n")
assert code == 1
assert any("no `Sources:` block" in e for e in errors)
def test_cited_but_absent_from_block_fails(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = (
"First claim about the topic at hand.[1]\n"
"Second claim about the topic at hand.[2]\n\n"
"Sources:\n[1] https://a.example\n"
)
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text)
assert code == 1
assert any("absent from the Sources block" in e for e in errors)
def test_brackets_inside_code_fences_are_not_citations(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = "Prose with no external claims in it.\n\n```python\nvalue = arr[42]\n```\n"
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text)
assert (code, errors) == (0, [])
def test_markdown_links_are_not_citations(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = "See the [docs](https://x.example) for the full option list.\n"
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text)
assert (code, errors) == (0, [])
def test_min_coverage_gate(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = (
"A cited claim about the subject matter.[1]\n"
"An uncited claim about the subject matter.\n"
"Another uncited claim about the subject matter.\n"
"A third uncited claim about the subject matter.\n\n"
"Sources:\n[1] https://a.example\n"
)
ok_code, _, _ = _verify(sources_mod, ledger, tmp_path, text)
assert ok_code == 0
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text, min_coverage=0.5)
assert code == 1
assert any("coverage" in e for e in errors)
def test_over_citation_warns_without_failing(sources_mod, ledger: Path, tmp_path: Path) -> None:
sources_mod.add_sources(
ledger, [f"https://s{i}.example" for i in range(1, 5)]
)
text = (
"One sentence leaning on far too many sources at once.[1][2][3][4]\n\n"
"Sources:\n"
+ "".join(f"[{i}] https://s{i}.example\n" for i in range(1, 5))
)
code, errors, warnings = _verify(sources_mod, ledger, tmp_path, text)
assert (code, errors) == (0, [])
assert any("more than 3 citations" in w for w in warnings)
def test_strict_mode_promotes_warnings_to_failure(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = "A cited claim about the subject.[1]\n\nSources:\n[1] https://a.example\n"
assert _verify(sources_mod, ledger, tmp_path, text)[0] == 0
assert _verify(sources_mod, ledger, tmp_path, text, strict=True)[0] == 1
# ---------------------------------------------------------------------------
# CLI surface
# ---------------------------------------------------------------------------
def test_cli_add_render_verify_roundtrip(sources_mod, tmp_path: Path, capsys) -> None:
ledger = tmp_path / "cli.json"
args = ["--ledger", str(ledger)]
assert sources_mod.main(args + ["add", "https://a.example", "https://b.example"]) == 0
assert "[1] https://a.example" in capsys.readouterr().out
draft = tmp_path / "d.md"
draft.write_text("Only the first source is used here.[1]\n", encoding="utf-8")
assert sources_mod.main(args + ["render", "--cited-in", str(draft)]) == 0
block = capsys.readouterr().out
assert "[1] https://a.example" in block and "[2]" not in block
with draft.open("a", encoding="utf-8") as fh:
fh.write("\n" + block)
assert sources_mod.main(args + ["verify", str(draft)]) == 0
def test_cli_reset_empties_the_ledger(sources_mod, tmp_path: Path, capsys) -> None:
ledger = tmp_path / "r.json"
args = ["--ledger", str(ledger)]
sources_mod.main(args + ["add", "https://a.example"])
capsys.readouterr()
assert sources_mod.main(args + ["reset"]) == 0
capsys.readouterr()
assert sources_mod.main(args + ["add", "https://z.example"]) == 0
assert "[1] https://z.example" in capsys.readouterr().out
def test_cli_verify_missing_draft_returns_2(sources_mod, tmp_path: Path) -> None:
ledger = tmp_path / "m.json"
code = sources_mod.main(["--ledger", str(ledger), "verify", str(tmp_path / "nope.md")])
assert code == 2
def test_cli_ledger_path_prefers_flag_over_env(sources_mod, tmp_path: Path, monkeypatch) -> None:
monkeypatch.setenv("HERMES_CITATION_LEDGER", str(tmp_path / "env.json"))
flagged = tmp_path / "flag.json"
assert sources_mod.resolve_ledger_path(str(flagged)) == flagged
assert sources_mod.resolve_ledger_path(None) == tmp_path / "env.json"
def test_cli_ledger_path_defaults_under_hermes_home(sources_mod, tmp_path: Path, monkeypatch) -> None:
monkeypatch.delenv("HERMES_CITATION_LEDGER", raising=False)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
path = sources_mod.resolve_ledger_path(None)
assert path.parts[-3:] == ("cache", "citations", "ledger.json")
assert str(tmp_path) in str(path)
def test_corrupt_ledger_raises_actionable_error(sources_mod, tmp_path: Path) -> None:
bad = tmp_path / "bad.json"
bad.write_text("not json", encoding="utf-8")
with pytest.raises(SystemExit) as exc:
sources_mod.load_ledger(bad)
assert "reset" in str(exc.value)
# ---------------------------------------------------------------------------
# Fact-checking: evidence quotes and [unverified] markers
# ---------------------------------------------------------------------------
_PAGE = (
"Water expands when it freezes.\n"
"Ice is about 9% less dense than liquid water,\n"
"which is why icebergs float.\n"
)
def test_quote_verbatim_match_is_whitespace_and_case_insensitive(sources_mod, ledger: Path) -> None:
sources_mod.add_sources(ledger, ["https://a.example"])
entry = sources_mod.attach_quote(
ledger, 1, "ice is about 9% less dense than liquid water,", _PAGE
)
assert len(entry["quotes"]) == 1
def test_quote_rejects_paraphrase(sources_mod, ledger: Path) -> None:
sources_mod.add_sources(ledger, ["https://a.example"])
with pytest.raises(SystemExit) as exc:
sources_mod.attach_quote(ledger, 1, "Frozen water is roughly 9% lighter", _PAGE)
assert "not found verbatim" in str(exc.value)
def test_quote_rejects_unknown_id_and_short_text(sources_mod, ledger: Path) -> None:
sources_mod.add_sources(ledger, ["https://a.example"])
with pytest.raises(SystemExit) as exc:
sources_mod.attach_quote(ledger, 7, "which is why icebergs float.", _PAGE)
assert "no source [7]" in str(exc.value)
with pytest.raises(SystemExit) as exc:
sources_mod.attach_quote(ledger, 1, "icebergs float.", _PAGE)
assert "too short" in str(exc.value)
def test_quote_is_idempotent(sources_mod, ledger: Path) -> None:
sources_mod.add_sources(ledger, ["https://a.example"])
sources_mod.attach_quote(ledger, 1, "Water expands when it freezes.", _PAGE)
entry = sources_mod.attach_quote(ledger, 1, "water expands when it freezes.", _PAGE)
assert len(entry["quotes"]) == 1
def test_verify_evidence_gate_requires_quotes(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = (
"A claim supported by the first source.[1]\n\n"
"Sources:\n[1] https://a.example\n"
)
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text, require_evidence=True)
assert code == 1
assert any("no verbatim evidence quote" in e for e in errors)
sources_mod.attach_quote(ledger, 1, "Water expands when it freezes.", _PAGE)
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text, require_evidence=True)
assert (code, errors) == (0, [])
def test_evidence_gate_only_applies_to_cited_sources(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
sources_mod.attach_quote(ledger, 1, "Water expands when it freezes.", _PAGE)
# [2] and [3] have no quotes but are not cited — the gate must not fail on them.
text = "A claim supported by the first source.[1]\n\nSources:\n[1] https://a.example\n"
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text, require_evidence=True)
assert (code, errors) == (0, [])
def test_unverified_marker_counts_toward_coverage(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = (
"A cited claim about the subject matter.[1]\n"
"A model-knowledge claim declared as such.[unverified]\n\n"
"Sources:\n[1] https://a.example\n"
)
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text, min_coverage=0.9)
assert (code, errors) == (0, [])
def test_unverified_marker_does_not_hide_uncited_sentences(sources_mod, ledger: Path, tmp_path: Path) -> None:
_seed(sources_mod, ledger)
text = (
"A cited claim about the subject matter.[1]\n"
"An uncited, unmarked claim about the subject.\n"
"Another uncited, unmarked claim about the subject.\n\n"
"Sources:\n[1] https://a.example\n"
)
code, errors, _ = _verify(sources_mod, ledger, tmp_path, text, min_coverage=0.9)
assert code == 1
assert any("coverage" in e for e in errors)
def test_render_evidence_style_includes_quotes(sources_mod, ledger: Path) -> None:
_seed(sources_mod, ledger)
sources_mod.attach_quote(ledger, 1, "Water expands when it freezes.", _PAGE)
sources = json.loads(ledger.read_text(encoding="utf-8"))["sources"]
block = sources_mod.render_sources(sources, style="evidence", only={1})
assert "[1] https://a.example" in block
assert '> "Water expands when it freezes."' in block
plain = sources_mod.render_sources(sources, style="markdown", only={1})
assert "Water expands" not in plain
def test_cli_quote_and_evidence_verify_roundtrip(sources_mod, tmp_path: Path, capsys) -> None:
ledger = tmp_path / "ev.json"
page = tmp_path / "page.txt"
page.write_text(_PAGE, encoding="utf-8")
args = ["--ledger", str(ledger)]
assert sources_mod.main(args + ["add", "https://a.example"]) == 0
capsys.readouterr()
assert (
sources_mod.main(
args + ["quote", "1", "--text", "Water expands when it freezes.", "--from", str(page)]
)
== 0
)
assert "evidence attached" in capsys.readouterr().out
draft = tmp_path / "d.md"
draft.write_text(
"A claim resting on the source page.[1]\n\nSources:\n[1] https://a.example\n",
encoding="utf-8",
)
assert sources_mod.main(args + ["verify", str(draft), "--evidence"]) == 0
capsys.readouterr()
assert sources_mod.main(args + ["render", "--style", "evidence"]) == 0
assert '> "Water expands when it freezes."' in capsys.readouterr().out
# ---------------------------------------------------------------------------
# Markdown-markup tolerance in the verbatim check
#
# Retrieval returns markdown, so the most citation-worthy sentences are the
# ones carrying inline links and emphasis around terms. Requiring the agent to
# reproduce that markup pushed a real live run toward a weaker evidence
# fragment, which is the opposite of the skill's purpose.
# ---------------------------------------------------------------------------
# Verbatim shape of the MedlinePlus sentence as web_extract returns it.
_MD_PAGE = (
"Variations in several additional genes, including "
"_[ERAP1](https://medlineplus.gov/genetics/gene/erap1/)_, "
"_[IL1A](https://medlineplus.gov/genetics/gene/il1a/)_, and "
"_[IL23R](https://medlineplus.gov/genetics/gene/il23r/)_, have also been\n"
"associated with ankylosing spondylitis.\n"
"While over 90% of AS patients have an HLA-B\\*27 haplotype, only around 5% develop AS.\n"
)
def test_quote_matches_through_inline_links_and_emphasis(sources_mod, ledger: Path) -> None:
sources_mod.add_sources(ledger, ["https://medlineplus.gov/x"])
entry = sources_mod.attach_quote(
ledger,
1,
"Variations in several additional genes, including ERAP1, IL1A, and IL23R, "
"have also been associated with ankylosing spondylitis.",
_MD_PAGE,
)
assert len(entry["quotes"]) == 1
# The stored quote keeps the caller's clean prose — no extractor artifacts
# leak into the rendered deliverable.
assert "medlineplus.gov/genetics/gene" not in entry["quotes"][0]["text"]
def test_quote_matches_through_escaped_asterisks(sources_mod, ledger: Path) -> None:
sources_mod.add_sources(ledger, ["https://frontiersin.org/x"])
entry = sources_mod.attach_quote(
ledger, 1, "over 90% of AS patients have an HLA-B*27 haplotype", _MD_PAGE
)
assert entry["quotes"][0]["text"] == "over 90% of AS patients have an HLA-B*27 haplotype"
def test_markup_tolerance_does_not_admit_paraphrase(sources_mod, ledger: Path) -> None:
"""Seeing through markup must not weaken the substantive check."""
sources_mod.add_sources(ledger, ["https://medlineplus.gov/x"])
with pytest.raises(SystemExit) as exc:
sources_mod.attach_quote(
ledger, 1, "Several other immune genes are also linked to the disease.", _MD_PAGE
)
assert "not found verbatim" in str(exc.value)
# ---------------------------------------------------------------------------
# render --replace-in
# ---------------------------------------------------------------------------
def test_render_replace_in_rewrites_block_idempotently(sources_mod, tmp_path: Path, capsys) -> None:
ledger = tmp_path / "rp.json"
page = tmp_path / "p.txt"
page.write_text(_PAGE, encoding="utf-8")
args = ["--ledger", str(ledger)]
sources_mod.main(args + ["add", "https://a.example", "https://b.example"])
sources_mod.main(
args + ["quote", "1", "--text", "Water expands when it freezes.", "--from", str(page)]
)
capsys.readouterr()
draft = tmp_path / "d.md"
draft.write_text(
"A claim resting on the first source.[1]\n\n## Sources\n\n[1] https://stale.example\n",
encoding="utf-8",
)
assert sources_mod.main(args + ["render", "--style", "evidence", "--replace-in", str(draft)]) == 0
first = draft.read_text(encoding="utf-8")
assert "stale.example" not in first
assert first.count("## Sources") == 1
assert '> "Water expands when it freezes."' in first
# [2] is registered but uncited — --replace-in filters to cited ids.
assert "b.example" not in first
assert sources_mod.main(args + ["render", "--style", "evidence", "--replace-in", str(draft)]) == 0
assert draft.read_text(encoding="utf-8") == first, "second run must be a no-op"
capsys.readouterr()
assert sources_mod.main(args + ["verify", str(draft), "--evidence"]) == 0
def test_render_replace_in_appends_when_no_block_exists(sources_mod, tmp_path: Path, capsys) -> None:
ledger = tmp_path / "ap.json"
args = ["--ledger", str(ledger)]
sources_mod.main(args + ["add", "https://a.example"])
draft = tmp_path / "d.md"
draft.write_text("A claim resting on the first source.[1]\n", encoding="utf-8")
assert sources_mod.main(args + ["render", "--replace-in", str(draft)]) == 0
body = draft.read_text(encoding="utf-8")
assert body.startswith("A claim resting on the first source.[1]")
assert "## Sources" in body and "[1] https://a.example" in body
capsys.readouterr()
# ---------------------------------------------------------------------------
# Output legibility
# ---------------------------------------------------------------------------
def test_stats_line_is_info_not_warn_on_success(sources_mod, tmp_path: Path, capsys) -> None:
ledger = tmp_path / "st.json"
args = ["--ledger", str(ledger)]
sources_mod.main(args + ["add", "https://a.example"])
draft = tmp_path / "d.md"
draft.write_text(
"A claim resting on the first source.[1]\n\nSources:\n[1] https://a.example\n",
encoding="utf-8",
)
capsys.readouterr()
assert sources_mod.main(args + ["verify", str(draft)]) == 0
out = capsys.readouterr().out
assert "info: stats:" in out
assert "warn: stats:" not in out
def test_stats_reports_provenance_total_matching_coverage(sources_mod, ledger: Path, tmp_path: Path) -> None:
"""The stats line's counts must reconcile with the percentage it prints."""
_seed(sources_mod, ledger)
text = (
"A cited claim about the subject matter.[1]\n"
"A claim both cited and hedged as uncertain.[2][unverified]\n"
"A model-knowledge claim declared as such.[unverified]\n"
"An uncited, unmarked claim about the subject.\n\n"
"Sources:\n[1] https://a.example\n[2] https://b.example\n"
)
_code, _errors, warnings = _verify(sources_mod, ledger, tmp_path, text)
stats = warnings[0]
# 4 sentences, 3 with provenance (the both-marked sentence counts once).
assert "4 prose sentence(s), 3 with declared provenance (75%)" in stats
@@ -0,0 +1,180 @@
"""Tests for the har-derived-api-client optional skill.
Two layers, both stdlib + pytest, no network:
1. Structural / frontmatter contract on SKILL.md (matches the maintainer
review checklist for optional skills).
2. Behavioral: run the real har_to_client.py logic against a synthetic HAR
fixture and assert it derives the endpoint, collapses id path segments,
filters static assets, and surfaces the User-Agent replay hint.
"""
import importlib.util
import json
import re
from pathlib import Path
import pytest
SKILL_DIR = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "web-development"
/ "har-derived-api-client"
)
SKILL_MD = SKILL_DIR / "SKILL.md"
CAPTURE = SKILL_DIR / "scripts" / "har_capture.py"
CAPTURE_CDP = SKILL_DIR / "scripts" / "har_capture_cdp.py"
DERIVE = SKILL_DIR / "scripts" / "har_to_client.py"
@pytest.fixture(scope="module")
def skill_text() -> str:
return SKILL_MD.read_text(encoding="utf-8")
def _load_module(path: Path, name: str):
spec = importlib.util.spec_from_file_location(name, path)
assert spec and spec.loader
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# --- structural contract ---------------------------------------------------
def test_skill_files_exist():
assert SKILL_MD.is_file()
assert CAPTURE.is_file()
assert CAPTURE_CDP.is_file()
assert DERIVE.is_file()
def test_frontmatter_present(skill_text: str):
assert skill_text.startswith("---\n")
assert skill_text.count("---") >= 2
def test_description_under_sixty_chars(skill_text: str):
m = re.search(r"^description: (.*)$", skill_text, re.MULTILINE)
assert m, "no description field"
desc = m.group(1).strip()
assert len(desc) <= 60, f"description is {len(desc)} chars (>60): {desc!r}"
assert desc.endswith("."), "description should end with a period"
def test_required_sections_present(skill_text: str):
for heading in (
"## When to Use",
"## Prerequisites",
"## How to Run",
"## Quick Reference",
"## Procedure",
"## Pitfalls",
"## Verification",
):
assert heading in skill_text, f"missing section: {heading}"
# --- behavioral: derivation logic -----------------------------------------
def _make_har() -> dict:
return {
"log": {
"entries": [
{ # a JSON API call we want derived, with an id path segment
"_resourceType": "fetch",
"request": {
"method": "GET",
"url": "https://api.example.com/v1/items/12345/reviews?limit=5",
"queryString": [{"name": "limit", "value": "5"}],
"headers": [
{"name": "User-Agent", "value": "Mozilla/5.0 TestBrowser/1.0"},
{"name": "accept", "value": "application/json"},
{"name": "referer", "value": "https://example.com/"},
],
},
"response": {
"status": 200,
"content": {
"mimeType": "application/json",
"text": '{"reviews":[{"id":1}]}',
},
},
},
{ # a static asset we must filter out by default
"_resourceType": "script",
"request": {
"method": "GET",
"url": "https://cdn.example.com/app.js",
"queryString": [],
"headers": [{"name": "User-Agent", "value": "Mozilla/5.0 TestBrowser/1.0"}],
},
"response": {"status": 200, "content": {"mimeType": "application/javascript"}},
},
]
}
}
def test_derives_endpoint_and_filters_static(tmp_path, capsys):
mod = _load_module(DERIVE, "har_to_client_undertest")
har = tmp_path / "t.har"
har.write_text(json.dumps(_make_har()), encoding="utf-8")
import sys
argv = sys.argv
try:
sys.argv = ["har_to_client.py", str(har), "--host", "example.com"]
rc = mod.main()
finally:
sys.argv = argv
out = capsys.readouterr().out
assert rc == 0
# id path segment collapsed to {id}
assert "GET https://api.example.com/v1/items/{id}/reviews" in out
# query param surfaced
assert "limit = 5" in out
# static JS filtered out
assert "app.js" not in out
# boring header dropped, useful one absent from list but UA promoted to hints
assert "referer" not in out
# replay hint carries the browser UA
assert "User-Agent (send this): Mozilla/5.0 TestBrowser/1.0" in out
def test_path_template_collapses_ids():
mod = _load_module(DERIVE, "har_to_client_undertest2")
assert mod.path_template("/v1/items/12345/x") == "/v1/items/{id}/x"
assert mod.path_template("/v1/items/abc/x") == "/v1/items/abc/x"
def test_capture_actions_parse_ok():
# har_capture imports playwright at module top; only assert the file is
# syntactically valid and exposes run_action without importing playwright.
src = CAPTURE.read_text(encoding="utf-8")
compile(src, str(CAPTURE), "exec")
assert "def run_action(" in src
assert 'record_har_content="embed"' in src
def test_cdp_capture_is_valid_and_attaches_not_launches():
# Covers the CDP pathway (cloud backends / /browser connect). Syntax-check
# without importing playwright, and assert it attaches (connect_over_cdp)
# and does NOT close a browser it doesn't own.
src = CAPTURE_CDP.read_text(encoding="utf-8")
compile(src, str(CAPTURE_CDP), "exec")
assert "connect_over_cdp(" in src
assert 'page.on("request"' in src and 'page.on("response"' in src
# must not tear down a browser it merely attached to
assert "browser.close()" not in src
def test_skill_documents_all_browser_pathways(skill_text: str):
# The skill must route every Hermes browser backend to the right capturer.
for token in ("Browserbase", "Browser-Use", "Firecrawl", "browser connect",
"har_capture_cdp.py", "connect_over_cdp"):
assert token in skill_text, f"pathway coverage missing: {token}"
+69
View File
@@ -0,0 +1,69 @@
"""The `hermes-agent` skill is what a running Hermes knows about itself.
`website/` is never packaged, so an installed Hermes has no local copy of the
user guide; skills ARE synced into `$HERMES_HOME/skills/`. The skill therefore
does not try to restate the product — it routes to the published `llms.txt`,
which is generated from the docs tree on every build and so can never be behind
the feature set. These tests keep that routing honest: the index has to be where
the skill says it is, and every reference has to be reachable, otherwise a
shipped feature is invisible and the agent answers "Hermes can't do that."
"""
from __future__ import annotations
import importlib.util
import re
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parents[2]
SKILL_DIR = REPO / "skills" / "autonomous-ai-agents" / "hermes-agent"
SKILL_MD = SKILL_DIR / "SKILL.md"
GENERATOR = REPO / "website" / "scripts" / "generate-llms-txt.py"
@pytest.fixture(scope="module")
def skill_text() -> str:
return SKILL_MD.read_text(encoding="utf-8")
def test_every_referenced_file_exists(skill_text):
"""Routing a question to a file that isn't there is a dead end."""
targets = set(re.findall(r"`((?:references|templates)/[^`]+)`", skill_text))
assert targets, "the skill's routing table no longer references any files"
for target in sorted(targets):
assert (SKILL_DIR / target).exists(), f"SKILL.md routes to missing {target}"
def test_every_reference_is_reachable_from_the_skill(skill_text):
"""An unrouted reference is one the agent will never think to open.
This is the failure that produced the original complaint: content can exist
and still be invisible because nothing points at it.
"""
on_disk = {f"references/{path.name}" for path in (SKILL_DIR / "references").glob("*.md")}
routed = set(re.findall(r"`(references/[^`]+)`", skill_text))
assert not (on_disk - routed), (
f"reference files no reader will ever reach: {sorted(on_disk - routed)}"
"add a routing-table row in SKILL.md"
)
def test_unknown_features_route_to_the_published_index(skill_text):
"""The catch-all is what makes coverage of the whole product possible."""
assert "/docs/llms.txt" in skill_text
# web_extract can be disabled; terminal never is.
assert "curl" in skill_text, "no way to reach the index without web tools"
def test_the_index_is_published_where_the_skill_says_it_is(skill_text):
"""A skill pointing at a URL nobody generates is worse than no routing."""
spec = importlib.util.spec_from_file_location("generate_llms_txt", GENERATOR)
assert spec is not None and spec.loader is not None
gen = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gen)
assert f"{gen.SITE_BASE}/llms.txt" in skill_text
+135
View File
@@ -0,0 +1,135 @@
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
from unittest.mock import patch
SCRIPT_PATH = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "blockchain"
/ "hyperliquid"
/ "scripts"
/ "hyperliquid_client.py"
)
def load_module():
spec = importlib.util.spec_from_file_location("hyperliquid_skill", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def test_normalize_perp_markets_extracts_change_and_volume():
mod = load_module()
payload = [
{
"universe": [
{"name": "BTC", "szDecimals": 5, "maxLeverage": 50},
{"name": "ETH", "szDecimals": 4, "maxLeverage": 25, "isDelisted": True},
]
},
[
{
"markPx": "100000",
"prevDayPx": "95000",
"funding": "0.0001",
"openInterest": "123456789",
"dayNtlVlm": "999999999",
},
{
"markPx": "2500",
"prevDayPx": "2600",
"funding": "-0.0002",
"openInterest": "20000000",
"dayNtlVlm": "11111111",
},
],
]
rows = mod._normalize_perp_markets(payload)
assert len(rows) == 2
assert rows[0]["coin"] == "BTC"
assert round(rows[0]["change_pct"], 2) == 5.26
assert rows[0]["day_ntl_vlm"] == "999999999"
assert rows[1]["is_delisted"] is True
def test_main_markets_json_prints_normalized_payload(capsys):
mod = load_module()
payload = [
{"universe": [{"name": "BTC", "szDecimals": 5, "maxLeverage": 50}]},
[{"markPx": "101000", "prevDayPx": "100000", "dayNtlVlm": "10"}],
]
with patch.object(mod, "_post_info", return_value=payload):
exit_code = mod.main(["markets", "--limit", "1", "--json"])
stdout = capsys.readouterr().out
rendered = json.loads(stdout)
assert exit_code == 0
assert rendered["count"] == 1
assert rendered["markets"][0]["coin"] == "BTC"
assert round(rendered["markets"][0]["change_pct"], 2) == 1.0
def test_env_lookup_reads_hermes_dotenv(tmp_path, monkeypatch):
mod = load_module()
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir(parents=True)
(hermes_home / ".env").write_text(
"HYPERLIQUID_USER_ADDRESS=0xdotenv123\nHYPERLIQUID_API_URL=https://api.hyperliquid-testnet.xyz\n",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("HYPERLIQUID_USER_ADDRESS", raising=False)
monkeypatch.delenv("HYPERLIQUID_API_URL", raising=False)
assert mod._env_lookup("HYPERLIQUID_USER_ADDRESS") == "0xdotenv123"
assert mod._resolve_user("") == "0xdotenv123"
assert mod._info_url() == "https://api.hyperliquid-testnet.xyz/info"
def test_user_dotenv_overrides_project_dotenv(tmp_path, monkeypatch):
mod = load_module()
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / ".env").write_text("HYPERLIQUID_USER_ADDRESS=0xproject\n", encoding="utf-8")
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / ".env").write_text("HYPERLIQUID_USER_ADDRESS=0xuserhome\n", encoding="utf-8")
monkeypatch.chdir(project_dir)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("HYPERLIQUID_USER_ADDRESS", raising=False)
assert mod._env_lookup("HYPERLIQUID_USER_ADDRESS") == "0xuserhome"
@@ -0,0 +1,155 @@
"""Tests for the mcp-oauth-remote-gateway optional skill.
Covers the diagnose-oauth-mcp.py decision tree (TOKEN_OK / REFRESH_FIXED /
SESSION_REVOKED / REFRESH_DEAD), the HERMES_HOME resolution fallback, the
atomic --write persistence path, and SKILL.md frontmatter invariants.
No live network calls — urllib is mocked throughout.
"""
from __future__ import annotations
import importlib.util
import io
import json
import re
import sys
import urllib.error
from pathlib import Path
from unittest.mock import patch
import pytest
SKILL_DIR = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "mcp"
/ "mcp-oauth-remote-gateway"
)
SCRIPT_PATH = SKILL_DIR / "scripts" / "diagnose-oauth-mcp.py"
SKILL_MD = SKILL_DIR / "SKILL.md"
def load_module():
spec = importlib.util.spec_from_file_location("diagnose_oauth_mcp", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class FakeResponse:
def __init__(self, status=200, body=b"{}", headers=None):
self.status = status
self._body = body
self.headers = headers or {}
def read(self):
return self._body
def _write_token_files(tokens_dir: Path, server="stripe", resource="https://mcp.example.com",
refresh_token="rt-1"):
tokens_dir.mkdir(parents=True, exist_ok=True)
tok = {
"access_token": "at-stored",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": refresh_token,
"scope": "read",
"resource": resource,
"expires_at": 0,
}
if refresh_token is None:
del tok["refresh_token"]
(tokens_dir / f"{server}.json").write_text(json.dumps(tok))
(tokens_dir / f"{server}.client.json").write_text(
json.dumps({"client_id": "cid-1", "token_endpoint_auth_method": "none"})
)
return tok
def _run_main(mod, tokens_dir, argv, responses):
"""Run mod.main() with urlopen mocked; returns captured stdout.
``responses`` is a list consumed in call order; each item is either a
FakeResponse or an Exception to raise.
"""
calls = []
def fake_urlopen(req, timeout=None):
calls.append(req)
item = responses.pop(0)
if isinstance(item, Exception):
raise item
return item
with patch.object(mod.os, "environ", dict(mod.os.environ, HERMES_HOME=str(tokens_dir.parent))), \
patch.object(mod.urllib.request, "urlopen", side_effect=fake_urlopen), \
patch.object(sys, "argv", ["diagnose-oauth-mcp.py", *argv]):
# Force the env-var fallback path (ignore any importable hermes_constants).
with patch.object(mod, "_hermes_home", lambda: str(tokens_dir.parent)):
buf = io.StringIO()
from contextlib import redirect_stdout
with redirect_stdout(buf):
mod.main()
return buf.getvalue(), calls
def _init_ok_body():
return json.dumps({"jsonrpc": "2.0", "id": 1,
"result": {"serverInfo": {"name": "x"}, "capabilities": {}}}).encode()
def _init_revoked_error(code=401):
body = json.dumps({"error": {"code": -32002, "message": "Session expired. Please re-authenticate."}}).encode()
return urllib.error.HTTPError("https://mcp.example.com", code, "Unauthorized",
{"WWW-Authenticate": 'Bearer error="invalid_token"'},
io.BytesIO(body))
def test_token_ok_branch(tmp_path):
mod = load_module()
tokens_dir = tmp_path / "mcp-tokens"
_write_token_files(tokens_dir)
out, calls = _run_main(mod, tokens_dir, ["stripe"], [FakeResponse(200, _init_ok_body())])
assert "BRANCH=TOKEN_OK" in out
assert len(calls) == 1 # never touched the token endpoint
def test_refresh_dead_no_refresh_token(tmp_path):
mod = load_module()
tokens_dir = tmp_path / "mcp-tokens"
_write_token_files(tokens_dir, refresh_token=None)
out, _ = _run_main(mod, tokens_dir, ["stripe"], [_init_revoked_error()])
assert "BRANCH=REFRESH_DEAD" in out
def test_requests_send_httpx_user_agent(tmp_path):
"""Cloudflare 403s bare urllib UAs — every request must carry the httpx UA."""
mod = load_module()
tokens_dir = tmp_path / "mcp-tokens"
_write_token_files(tokens_dir)
_, calls = _run_main(mod, tokens_dir, ["stripe"], [FakeResponse(200, _init_ok_body())])
for req in calls:
assert req.get_header("User-agent") == mod.UA
def test_skill_md_frontmatter_invariants():
yaml = pytest.importorskip("yaml")
content = SKILL_MD.read_text()
assert content.startswith("---\n")
fm = yaml.safe_load(re.search(r"^---\n(.*?)\n---", content, re.DOTALL).group(1))
assert len(fm["description"]) <= 60
assert fm["description"].endswith(".")
assert "platforms" in fm and len(fm["platforms"]) >= 1
assert fm["author"].split(",")[0].strip() != "Hermes Agent" # human credited first
@@ -0,0 +1,96 @@
"""Tests for the meeting-action-items bundled skill."""
import re
from pathlib import Path
import yaml
SKILL_PATH = (
Path(__file__).resolve().parents[2]
/ "skills"
/ "productivity"
/ "meeting-action-items"
/ "SKILL.md"
)
def _frontmatter_and_body():
content = SKILL_PATH.read_text(encoding="utf-8")
assert content.startswith("---")
m = re.search(r"\n---\s*\n", content[3:])
assert m, "frontmatter must close with ---"
fm = yaml.safe_load(content[3 : m.start() + 3])
body = content[m.end() + 3 :]
return fm, body
def test_skill_file_exists():
assert SKILL_PATH.is_file()
def test_frontmatter_required_fields():
fm, _ = _frontmatter_and_body()
for field in ("name", "description", "version", "author", "license", "platforms"):
assert field in fm, f"missing frontmatter field: {field}"
assert fm["name"] == "meeting-action-items"
hermes = fm["metadata"]["hermes"]
assert hermes["tags"]
assert "related_skills" in hermes
def test_description_hardline():
fm, _ = _frontmatter_and_body()
desc = fm["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60"
assert desc.endswith(".")
def test_author_credits_human_first():
fm, _ = _frontmatter_and_body()
assert not fm["author"].startswith("Hermes Agent"), "human contributor must be credited first"
assert "benbarclay" in fm["author"]
def test_related_skills_resolve_in_repo():
fm, _ = _frontmatter_and_body()
repo_root = SKILL_PATH.parents[3]
for name in fm["metadata"]["hermes"]["related_skills"]:
hits = (
list(repo_root.glob(f"skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"optional-skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"skills/*/*/{name}/SKILL.md"))
)
assert hits, f"related_skills entry does not resolve in-repo: {name}"
def test_no_phantom_connectors_in_prose():
"""Prose must not name tracker connectors that don't exist as skills."""
_, body = _frontmatter_and_body()
assert not re.search(r"\bLinear\b", body), "no phantom 'Linear' connector references"
def test_body_structure_and_size():
_, body = _frontmatter_and_body()
for section in ("## When to Use", "## Procedure", "## Pitfalls", "## Verification"):
assert section in body, f"missing section: {section}"
assert len(SKILL_PATH.read_text(encoding="utf-8")) <= 100_000
def test_no_machine_local_paths():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "/home/" not in content
assert not re.search(r"[A-Z]:\\\\Users", content)
def test_steps_have_completion_criteria():
_, body = _frontmatter_and_body()
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## )", body, re.MULTILINE | re.DOTALL)
assert len(steps) >= 5
for step in steps:
assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}"
def test_core_disciplines_present():
_, body = _frontmatter_and_body()
assert "never invent" in body, "due-date invention ban must be present"
assert "before creating anything" in body, "reconcile-before-create must be present"
assert "`unresolved`" in body, "missing owners/dates stay visibly unresolved"
+242
View File
@@ -0,0 +1,242 @@
"""Tests for optional-skills/productivity/memento-flashcards/scripts/memento_cards.py"""
import csv
import json
import sys
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest import mock
import pytest
# Add the scripts dir so we can import the module directly
SCRIPTS_DIR = Path(__file__).resolve().parents[2] / "optional-skills" / "productivity" / "memento-flashcards" / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))
import memento_cards
@pytest.fixture(autouse=True)
def isolated_data(tmp_path, monkeypatch):
"""Redirect card storage to a temp directory for every test."""
data_dir = tmp_path / "data"
data_dir.mkdir()
monkeypatch.setattr(memento_cards, "DATA_DIR", data_dir)
monkeypatch.setattr(memento_cards, "CARDS_FILE", data_dir / "cards.json")
return data_dir
def _run(capsys, argv: list[str]) -> dict:
"""Run main() with given argv and return parsed JSON output."""
with mock.patch("sys.argv", ["memento_cards"] + argv):
memento_cards.main()
captured = capsys.readouterr()
return json.loads(captured.out)
# ── Add / List / Delete ──────────────────────────────────────────────────────
class TestCardCRUD:
def test_add_creates_card(self, capsys):
result = _run(capsys, ["add", "--question", "What is 2+2?", "--answer", "4", "--collection", "Math"])
assert result["ok"] is True
card = result["card"]
assert card["question"] == "What is 2+2?"
assert card["answer"] == "4"
assert card["collection"] == "Math"
assert card["status"] == "learning"
assert card["ease_streak"] == 0
uuid.UUID(card["id"]) # validates it's a real UUID
def test_list_all(self, capsys):
_run(capsys, ["add", "--question", "Q1", "--answer", "A1", "--collection", "C1"])
_run(capsys, ["add", "--question", "Q2", "--answer", "A2", "--collection", "C2"])
result = _run(capsys, ["list"])
assert result["count"] == 2
def test_delete_card(self, capsys):
result = _run(capsys, ["add", "--question", "Q", "--answer", "A"])
card_id = result["card"]["id"]
del_result = _run(capsys, ["delete", "--id", card_id])
assert del_result["ok"] is True
assert del_result["deleted"] == card_id
# Verify gone
list_result = _run(capsys, ["list"])
assert list_result["count"] == 0
# ── Due Filtering ────────────────────────────────────────────────────────────
class TestDueFiltering:
def test_new_card_is_due(self, capsys):
_run(capsys, ["add", "--question", "Q", "--answer", "A"])
result = _run(capsys, ["due"])
assert result["count"] == 1
def test_future_card_not_due(self, capsys, monkeypatch):
_run(capsys, ["add", "--question", "Q", "--answer", "A"])
# Rate it good (pushes next_review_at to +3 days)
card_id = _run(capsys, ["list"])["cards"][0]["id"]
_run(capsys, ["rate", "--id", card_id, "--rating", "good"])
result = _run(capsys, ["due"])
assert result["count"] == 0
def test_retired_card_not_due(self, capsys):
_run(capsys, ["add", "--question", "Q", "--answer", "A"])
card_id = _run(capsys, ["list"])["cards"][0]["id"]
_run(capsys, ["rate", "--id", card_id, "--rating", "retire"])
result = _run(capsys, ["due"])
assert result["count"] == 0
def test_due_with_collection_filter(self, capsys):
_run(capsys, ["add", "--question", "Q1", "--answer", "A1", "--collection", "C1"])
_run(capsys, ["add", "--question", "Q2", "--answer", "A2", "--collection", "C2"])
result = _run(capsys, ["due", "--collection", "C1"])
assert result["count"] == 1
assert result["cards"][0]["collection"] == "C1"
# ── Rating and Rescheduling ──────────────────────────────────────────────────
class TestRating:
def test_hard_adds_1_day(self, capsys):
_run(capsys, ["add", "--question", "Q", "--answer", "A"])
card_id = _run(capsys, ["list"])["cards"][0]["id"]
before = datetime.now(timezone.utc)
result = _run(capsys, ["rate", "--id", card_id, "--rating", "hard"])
after = datetime.now(timezone.utc)
next_review = datetime.fromisoformat(result["card"]["next_review_at"])
assert before + timedelta(days=1) <= next_review <= after + timedelta(days=1)
assert result["card"]["ease_streak"] == 0
def test_good_adds_3_days(self, capsys):
_run(capsys, ["add", "--question", "Q", "--answer", "A"])
card_id = _run(capsys, ["list"])["cards"][0]["id"]
before = datetime.now(timezone.utc)
result = _run(capsys, ["rate", "--id", card_id, "--rating", "good"])
next_review = datetime.fromisoformat(result["card"]["next_review_at"])
assert next_review >= before + timedelta(days=3)
assert result["card"]["ease_streak"] == 0
def test_retire_sets_retired(self, capsys):
_run(capsys, ["add", "--question", "Q", "--answer", "A"])
card_id = _run(capsys, ["list"])["cards"][0]["id"]
result = _run(capsys, ["rate", "--id", card_id, "--rating", "retire"])
assert result["card"]["status"] == "retired"
assert result["card"]["ease_streak"] == 0
def test_auto_retire_after_3_easys(self, capsys):
_run(capsys, ["add", "--question", "Q", "--answer", "A"])
card_id = _run(capsys, ["list"])["cards"][0]["id"]
# Force card to be due by manipulating next_review_at through rate
for i in range(3):
# Load and directly set next_review_at to now so it's ratable
data = memento_cards._load()
for c in data["cards"]:
if c["id"] == card_id:
c["next_review_at"] = memento_cards._iso(memento_cards._now())
memento_cards._save(data)
result = _run(capsys, ["rate", "--id", card_id, "--rating", "easy"])
assert result["card"]["ease_streak"] == 3
assert result["card"]["status"] == "retired"
# ── CSV Export/Import ────────────────────────────────────────────────────────
class TestCSV:
def test_export_import_roundtrip(self, capsys, tmp_path):
_run(capsys, ["add", "--question", "Q1", "--answer", "A1", "--collection", "C1"])
_run(capsys, ["add", "--question", "Q2", "--answer", "A2", "--collection", "C2"])
csv_path = str(tmp_path / "export.csv")
result = _run(capsys, ["export", "--output", csv_path])
assert result["ok"] is True
assert result["exported"] == 2
# Verify CSV content
with open(csv_path, "r") as f:
reader = csv.reader(f)
rows = list(reader)
assert len(rows) == 2
assert rows[0] == ["Q1", "A1", "C1"]
assert rows[1] == ["Q2", "A2", "C2"]
# Delete all and reimport
data = memento_cards._load()
data["cards"] = []
memento_cards._save(data)
result = _run(capsys, ["import", "--file", csv_path, "--collection", "Fallback"])
assert result["ok"] is True
assert result["imported"] == 2
# Verify imported cards use CSV collection column
list_result = _run(capsys, ["list"])
collections = {c["collection"] for c in list_result["cards"]}
assert collections == {"C1", "C2"}
# ── Quiz Batch Add ───────────────────────────────────────────────────────────
# ── Statistics ───────────────────────────────────────────────────────────────
# ── Edge Cases ───────────────────────────────────────────────────────────────
class TestEdgeCases:
def test_corrupt_json_recovery(self, capsys):
"""Corrupt JSON file should be treated as empty."""
memento_cards.DATA_DIR.mkdir(parents=True, exist_ok=True)
with open(memento_cards.CARDS_FILE, "w") as f:
f.write("{corrupted json...")
result = _run(capsys, ["list"])
assert result["count"] == 0
# Can still add
result = _run(capsys, ["add", "--question", "Q", "--answer", "A"])
assert result["ok"] is True
def test_atomic_write_creates_dir(self, capsys):
"""Data dir is created automatically if missing."""
import shutil
if memento_cards.DATA_DIR.exists():
shutil.rmtree(memento_cards.DATA_DIR)
result = _run(capsys, ["add", "--question", "Q", "--answer", "A"])
assert result["ok"] is True
assert memento_cards.CARDS_FILE.exists()
# ── User Answer Tracking ────────────────────────────────────────────────────
class TestUserAnswer:
def test_user_answer_persists_in_list(self, capsys):
_run(capsys, ["add", "--question", "Q", "--answer", "A"])
card_id = _run(capsys, ["list"])["cards"][0]["id"]
_run(capsys, ["rate", "--id", card_id, "--rating", "easy",
"--user-answer", "my answer"])
result = _run(capsys, ["list"])
assert result["cards"][0]["last_user_answer"] == "my answer"
+149
View File
@@ -0,0 +1,149 @@
"""Invariant tests for the bundled office/document skills.
Covers skills/productivity/{docx,xlsx,pdf,powerpoint} — the clean-room
MIT office document suite. Tests assert contracts (frontmatter shape,
referenced scripts exist, script CLI conventions, UTF-8-explicit I/O),
not snapshots of skill content.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
import yaml
REPO = Path(__file__).resolve().parent.parent.parent
SKILLS = REPO / "skills"
OFFICE_SKILLS = ["docx", "xlsx", "pdf", "powerpoint"]
def _skill_dir(name: str) -> Path:
return SKILLS / "productivity" / name
def _frontmatter(skill_md: Path) -> dict:
text = skill_md.read_text(encoding="utf-8")
match = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
assert match, f"{skill_md} has no YAML frontmatter"
return yaml.safe_load(match.group(1))
@pytest.mark.parametrize("name", OFFICE_SKILLS)
def test_skill_exists_with_frontmatter(name):
skill_md = _skill_dir(name) / "SKILL.md"
assert skill_md.exists(), f"missing {skill_md}"
fm = _frontmatter(skill_md)
assert fm["name"] == name
assert fm["description"].strip()
assert len(fm["description"]) <= 60, (
f"{name}: description is {len(fm['description'])} chars (max 60)"
)
assert fm["description"].rstrip('"').endswith(".")
platforms = fm.get("platforms")
assert platforms, f"{name}: missing platforms gating"
assert set(platforms) <= {"linux", "macos", "windows"}
@pytest.mark.parametrize("name", OFFICE_SKILLS)
def test_mit_licensed_clean_room(name):
"""The office suite is the clean-room rewrite: MIT, no Anthropic
license text, no proprietary license markers anywhere in the dir."""
skill_dir = _skill_dir(name)
fm = _frontmatter(skill_dir / "SKILL.md")
assert str(fm.get("license", "")).strip() == "MIT", (
f"{name}: license must be MIT, got {fm.get('license')!r}"
)
assert not (skill_dir / "LICENSE.txt").exists(), (
f"{name}: legacy LICENSE.txt present — clean-room dirs ship LICENSE (MIT)"
)
license_file = skill_dir / "LICENSE"
assert license_file.exists(), f"{name}: missing MIT LICENSE file"
text = license_file.read_text(encoding="utf-8")
assert "MIT License" in text
assert "Anthropic" not in text
for path in skill_dir.rglob("*"):
if path.is_file() and path.suffix in (".md", ".py"):
content = path.read_text(encoding="utf-8", errors="replace")
assert "Anthropic" not in content, (
f"{name}: {path.relative_to(skill_dir)} references Anthropic — "
"clean-room provenance violation"
)
@pytest.mark.parametrize("name", OFFICE_SKILLS)
def test_referenced_scripts_exist(name):
"""Every scripts/... path mentioned in SKILL.md must exist on disk."""
skill_dir = _skill_dir(name)
body = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
refs = set(re.findall(r"scripts/[\w./-]+\.py", body))
assert refs, f"{name}: SKILL.md references no helper scripts"
for ref in refs:
assert (skill_dir / ref).exists(), f"{name}: SKILL.md references missing {ref}"
@pytest.mark.parametrize("name", OFFICE_SKILLS)
def test_all_shipped_scripts_are_documented(name):
"""Every shipped scripts/*.py is mentioned in SKILL.md (no dead cargo).
Shared/internal modules (underscore-prefixed or *_common.py) are exempt."""
skill_dir = _skill_dir(name)
body = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
for script in (skill_dir / "scripts").glob("*.py"):
if script.name.startswith("_") or script.stem.endswith("_common"):
continue
assert script.name in body, (
f"{name}: scripts/{script.name} is shipped but never mentioned in SKILL.md"
)
@pytest.mark.parametrize("name", OFFICE_SKILLS)
def test_scripts_use_explicit_utf8_text_io(name):
"""No locale-default text-mode I/O in helper scripts: every text-mode
open() must pass encoding=. Binary-mode opens are exempt. This is the
class of bug that mojibake'd form fills on cp1251/GBK/cp932 hosts."""
skill_dir = _skill_dir(name)
offenders = []
for script in (skill_dir / "scripts").rglob("*.py"):
content = script.read_text(encoding="utf-8")
for m in re.finditer(r"(?<![\w.])open\(([^)]*)\)", content):
args = m.group(1)
if re.search(r"['\"][rwaxt+]*b[rwaxt+]*['\"]", args):
continue # binary mode
if "encoding" not in args:
line = content[: m.start()].count("\n") + 1
offenders.append(f"{script.relative_to(skill_dir)}:{line}: open({args})")
assert not offenders, (
f"{name}: text-mode open() without explicit encoding:\n" + "\n".join(offenders)
)
@pytest.mark.parametrize("name", OFFICE_SKILLS)
def test_scripts_are_argparse_clis(name):
"""Helper scripts are argparse CLIs: importable arg parsing + a main
guard, so `python scripts/x.py --help` works everywhere."""
skill_dir = _skill_dir(name)
for script in (skill_dir / "scripts").glob("*.py"):
if script.name.startswith("_") or script.stem.endswith("_common"):
continue
content = script.read_text(encoding="utf-8")
assert "argparse" in content, f"{name}: scripts/{script.name} is not an argparse CLI"
assert '__name__' in content, f"{name}: scripts/{script.name} lacks a __main__ guard"
@pytest.mark.parametrize("name", OFFICE_SKILLS)
def test_skill_has_tests(name):
"""Each office skill ships its own e2e pytest suite."""
tests_dir = _skill_dir(name) / "tests"
assert tests_dir.is_dir(), f"{name}: missing tests/ directory"
assert list(tests_dir.glob("test_*.py")), f"{name}: no test files in tests/"
def test_docs_pages_generated():
"""Each bundled office skill has a generated docs-site page."""
docs_dir = REPO / "website" / "docs" / "user-guide" / "skills" / "bundled" / "productivity"
for name in OFFICE_SKILLS:
assert (docs_dir / f"productivity-{name}.md").exists(), (
f"missing generated docs page for {name}; run website/scripts/generate-skill-docs.py"
)
+750
View File
@@ -0,0 +1,750 @@
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
SCRIPT_PATH = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "migration"
/ "openclaw-migration"
/ "scripts"
/ "openclaw_to_hermes.py"
)
def load_module():
spec = importlib.util.spec_from_file_location("openclaw_to_hermes", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def load_skills_guard():
spec = importlib.util.spec_from_file_location(
"skills_guard_local",
Path(__file__).resolve().parents[2] / "tools" / "skills_guard.py",
)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def test_extract_markdown_entries_promotes_heading_context():
mod = load_module()
text = """# MEMORY.md - Long-Term Memory
## Tyler Williams
- Founder of VANTA Research
- Timezone: America/Los_Angeles
### Active Projects
- Hermes Agent
"""
entries = mod.extract_markdown_entries(text)
assert "Tyler Williams: Founder of VANTA Research" in entries
assert "Tyler Williams: Timezone: America/Los_Angeles" in entries
assert "Tyler Williams > Active Projects: Hermes Agent" in entries
def test_merge_entries_respects_limit_and_reports_overflow():
mod = load_module()
existing = ["alpha"]
incoming = ["beta", "gamma is too long"]
merged, stats, overflowed = mod.merge_entries(existing, incoming, limit=12)
assert merged == ["alpha", "beta"]
assert stats["added"] == 1
assert stats["overflowed"] == 1
assert overflowed == ["gamma is too long"]
def test_migrator_copies_skill_and_merges_allowlist(tmp_path: Path):
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
target.mkdir()
(source / "workspace" / "skills" / "demo-skill").mkdir(parents=True)
(source / "workspace" / "skills" / "demo-skill" / "SKILL.md").write_text(
"---\nname: demo-skill\ndescription: demo\n---\n\nbody\n",
encoding="utf-8",
)
(source / "exec-approvals.json").write_text(
json.dumps(
{
"agents": {
"*": {
"allowlist": [
{"pattern": "/usr/bin/*"},
{"pattern": "/home/test/**"},
]
}
}
}
),
encoding="utf-8",
)
(target / "config.yaml").write_text("command_allowlist:\n - /usr/bin/*\n", encoding="utf-8")
migrator = mod.Migrator(
source_root=source,
target_root=target,
execute=True,
workspace_target=None,
overwrite=False,
migrate_secrets=False,
output_dir=target / "migration-report",
)
report = migrator.migrate()
imported_skill = target / "skills" / mod.SKILL_CATEGORY_DIRNAME / "demo-skill" / "SKILL.md"
assert imported_skill.exists()
assert "/home/test/**" in (target / "config.yaml").read_text(encoding="utf-8")
assert report["summary"]["migrated"] >= 2
# The merge is written atomically — no temp file survives the run.
assert [p.name for p in target.glob(".tmp*")] == []
def _allowlist_migrator(mod, tmp_path: Path, existing_config: str):
"""Migrator wired to merge an exec-approvals allowlist into config.yaml."""
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
target.mkdir()
source.mkdir(parents=True)
(source / "exec-approvals.json").write_text(
json.dumps({"agents": {"*": {"allowlist": [{"pattern": "/home/test/**"}]}}}),
encoding="utf-8",
)
(target / "config.yaml").write_text(existing_config, encoding="utf-8")
return mod.Migrator(
source_root=source,
target_root=target,
execute=True,
workspace_target=None,
overwrite=False,
migrate_secrets=False,
output_dir=target / "migration-report",
), target / "config.yaml"
MALFORMED_HERMES_CONFIG = """\
model: hermes-4-405b
api_key_env: OPENROUTER_API_KEY
command_allowlist:
- /usr/bin/*
approvals:
deny: [shutdown *
telegram:
enabled: true
"""
def test_unreadable_config_is_refused_not_overwritten(tmp_path: Path):
"""A present-but-unparseable config.yaml must survive the migration.
``load_yaml_file`` returned ``{}`` for an absent file AND for one it could
not parse; the config-mutating steps read, merge and write the whole
mapping back, so a YAML syntax error meant every existing setting was
replaced by just the merged section. Same defect as the ported twin in
``hermes_cli/agent_import.py``.
"""
mod = load_module()
migrator, config_path = _allowlist_migrator(
mod, tmp_path, MALFORMED_HERMES_CONFIG)
before = config_path.read_bytes()
report = migrator.migrate()
assert config_path.read_bytes() == before
allowlist = [i for i in report["items"] if i["kind"] == "command-allowlist"]
assert allowlist and allowlist[0]["status"] == mod.STATUS_ERROR
assert "not valid YAML" in allowlist[0]["reason"]
def test_unreadable_config_blocks_later_config_steps_instead_of_partial_writes(
tmp_path: Path):
"""One refusal flips the existing _config_apply_blocked short-circuit."""
mod = load_module()
migrator, config_path = _allowlist_migrator(
mod, tmp_path, MALFORMED_HERMES_CONFIG)
report = migrator.migrate()
assert migrator._config_apply_blocked is True
statuses = {
i["status"] for i in report["items"]
if i["kind"] in mod.Migrator._CONFIG_MUTATING_OPTIONS
}
# Nothing claimed a successful config write.
assert "migrated" not in statuses
assert config_path.read_text(encoding="utf-8") == MALFORMED_HERMES_CONFIG
def test_readable_config_keeps_every_pre_existing_key(tmp_path: Path):
mod = load_module()
migrator, config_path = _allowlist_migrator(
mod,
tmp_path,
"model: hermes-4-405b\n"
"api_key_env: OPENROUTER_API_KEY\n"
"command_allowlist:\n - /usr/bin/*\n",
)
migrator.migrate()
import yaml
merged = yaml.safe_load(config_path.read_text(encoding="utf-8"))
assert merged["model"] == "hermes-4-405b"
assert merged["api_key_env"] == "OPENROUTER_API_KEY"
assert "/usr/bin/*" in merged["command_allowlist"]
assert "/home/test/**" in merged["command_allowlist"]
def test_absent_config_is_still_created(tmp_path: Path):
"""The guard must not break first-time creation.
Only ``absent`` may read as ``{}``; ``model-config`` creates config.yaml
from scratch when the target has none.
"""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
target.mkdir()
source.mkdir()
(source / "openclaw.json").write_text(
json.dumps({"agents": {"defaults": {"model": "anthropic/claude-sonnet-4"}}}),
encoding="utf-8",
)
config_path = target / "config.yaml"
assert not config_path.exists()
mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=True, migrate_secrets=False,
output_dir=None, selected_options={"model-config"},
).migrate()
assert "anthropic/claude-sonnet-4" in config_path.read_text(encoding="utf-8")
def test_symlinked_config_stays_a_symlink(tmp_path: Path):
"""Managed deployments symlink ~/.hermes/config.yaml into a dotfiles repo.
A plain ``os.replace`` onto the link would detach it into a regular file;
``dump_yaml_file`` resolves the link first, as ``utils.atomic_replace`` does.
"""
mod = load_module()
real = tmp_path / "dotfiles" / "config.yaml"
real.parent.mkdir(parents=True)
real.write_text("model: hermes-4-405b\ncommand_allowlist:\n - /usr/bin/*\n",
encoding="utf-8")
migrator, config_path = _allowlist_migrator(mod, tmp_path, "placeholder: true\n")
config_path.unlink()
config_path.symlink_to(real)
migrator.migrate()
assert config_path.is_symlink()
assert config_path.resolve() == real.resolve()
assert "/home/test/**" in real.read_text(encoding="utf-8")
assert "hermes-4-405b" in real.read_text(encoding="utf-8")
def test_unreadable_config_refused_by_model_config_too(tmp_path: Path):
"""The refusal is at the shared helper, so every config step inherits it."""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
target.mkdir()
source.mkdir()
(source / "openclaw.json").write_text(
json.dumps({"agents": {"defaults": {"model": "anthropic/claude-sonnet-4"}}}),
encoding="utf-8",
)
config_path = target / "config.yaml"
config_path.write_text(MALFORMED_HERMES_CONFIG, encoding="utf-8")
report = mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=True, migrate_secrets=False,
output_dir=None, selected_options={"model-config"},
).migrate()
assert config_path.read_text(encoding="utf-8") == MALFORMED_HERMES_CONFIG
items = [i for i in report["items"] if i["kind"] == "model-config"]
assert items and items[0]["status"] == mod.STATUS_ERROR
def test_migrator_optionally_imports_supported_secrets_and_messaging_settings(tmp_path: Path):
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
(source / "credentials").mkdir(parents=True)
(source / "openclaw.json").write_text(
json.dumps(
{
"agents": {"defaults": {"workspace": "/tmp/openclaw-workspace"}},
"channels": {"telegram": {"botToken": "123:abc"}},
}
),
encoding="utf-8",
)
(source / "credentials" / "telegram-default-allowFrom.json").write_text(
json.dumps({"allowFrom": ["111", "222"]}),
encoding="utf-8",
)
target.mkdir()
migrator = mod.Migrator(
source_root=source,
target_root=target,
execute=True,
workspace_target=None,
overwrite=False,
migrate_secrets=True,
output_dir=target / "migration-report",
)
migrator.migrate()
env_text = (target / ".env").read_text(encoding="utf-8")
assert "MESSAGING_CWD=/tmp/openclaw-workspace" in env_text
assert "TELEGRAM_ALLOWED_USERS=111,222" in env_text
assert "TELEGRAM_BOT_TOKEN=123:abc" in env_text
def test_source_candidate_finds_files_in_custom_workspace(tmp_path: Path):
"""When agents.defaults.workspace points outside ~/.openclaw, files should
be discovered there as a fallback."""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
custom_ws = tmp_path / "my-custom-workspace"
target.mkdir()
source.mkdir()
custom_ws.mkdir()
# No workspace/ directory inside .openclaw — files live in custom workspace
(custom_ws / "MEMORY.md").write_text("# Memory\n\n- custom workspace entry\n", encoding="utf-8")
(custom_ws / "SOUL.md").write_text("# Soul\n\nI am me.\n", encoding="utf-8")
(custom_ws / "skills" / "my-skill").mkdir(parents=True)
(custom_ws / "skills" / "my-skill" / "SKILL.md").write_text(
"---\nname: my-skill\ndescription: test\n---\n\nbody\n",
encoding="utf-8",
)
(custom_ws / "memory").mkdir()
(custom_ws / "memory" / "2026-01-01.md").write_text("- daily note\n", encoding="utf-8")
(source / "openclaw.json").write_text(
json.dumps({"agents": {"defaults": {"workspace": str(custom_ws)}}}),
encoding="utf-8",
)
migrator = mod.Migrator(
source_root=source,
target_root=target,
execute=True,
workspace_target=None,
overwrite=False,
migrate_secrets=False,
output_dir=target / "migration-report",
selected_options={"soul", "memory", "skills", "daily-memory"},
)
report = migrator.migrate()
# SOUL.md should have been found and migrated
assert (target / "SOUL.md").exists()
# MEMORY.md should have been found and migrated
assert (target / "memories" / "MEMORY.md").exists()
mem_content = (target / "memories" / "MEMORY.md").read_text(encoding="utf-8")
assert "custom workspace entry" in mem_content
# Skills should have been found and migrated
imported_skill = target / "skills" / mod.SKILL_CATEGORY_DIRNAME / "my-skill" / "SKILL.md"
assert imported_skill.exists()
migrated_kinds = {item["kind"] for item in report["items"] if item["status"] == "migrated"}
assert "soul" in migrated_kinds
assert "memory" in migrated_kinds
assert "skill" in migrated_kinds
def test_slack_settings_migrated(tmp_path: Path):
"""Slack bot/app tokens and allowlist migrate to .env."""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
target.mkdir()
source.mkdir()
(source / "openclaw.json").write_text(
json.dumps({
"channels": {
"slack": {
"botToken": "xoxb-slack-bot",
"appToken": "xapp-slack-app",
"allowFrom": ["U111", "U222"],
}
}
}),
encoding="utf-8",
)
migrator = mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=False, migrate_secrets=False, output_dir=None,
selected_options={"slack-settings"},
)
report = migrator.migrate()
env_text = (target / ".env").read_text(encoding="utf-8")
assert "SLACK_BOT_TOKEN=xoxb-slack-bot" in env_text
assert "SLACK_APP_TOKEN=xapp-slack-app" in env_text
assert "SLACK_ALLOWED_USERS=U111,U222" in env_text
def test_model_config_migrated(tmp_path: Path):
"""Default model setting migrates to config.yaml."""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
target.mkdir()
source.mkdir()
(source / "openclaw.json").write_text(
json.dumps({
"agents": {"defaults": {"model": "anthropic/claude-sonnet-4"}}
}),
encoding="utf-8",
)
# config.yaml must exist for YAML merge to work
(target / "config.yaml").write_text("model: openrouter/auto\n", encoding="utf-8")
migrator = mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=True, migrate_secrets=False, output_dir=None,
selected_options={"model-config"},
)
report = migrator.migrate()
config_text = (target / "config.yaml").read_text(encoding="utf-8")
assert "anthropic/claude-sonnet-4" in config_text
def test_shared_skills_migrated(tmp_path: Path):
"""Shared skills from ~/.openclaw/skills/ are migrated."""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
target.mkdir()
# Create a shared skill (not in workspace/skills/)
(source / "skills" / "my-shared-skill").mkdir(parents=True)
(source / "skills" / "my-shared-skill" / "SKILL.md").write_text(
"---\nname: my-shared-skill\ndescription: shared\n---\n\nbody\n",
encoding="utf-8",
)
migrator = mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=False, migrate_secrets=False, output_dir=None,
selected_options={"shared-skills"},
)
report = migrator.migrate()
imported = target / "skills" / mod.SKILL_CATEGORY_DIRNAME / "my-shared-skill" / "SKILL.md"
assert imported.exists()
def test_daily_memory_merged(tmp_path: Path):
"""Daily memory notes from workspace/memory/*.md are merged into MEMORY.md."""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
target.mkdir()
mem_dir = source / "workspace" / "memory"
mem_dir.mkdir(parents=True)
(mem_dir / "2026-03-01.md").write_text(
"# March 1 Notes\n\n- User prefers dark mode\n- Timezone: PST\n",
encoding="utf-8",
)
(mem_dir / "2026-03-02.md").write_text(
"# March 2 Notes\n\n- Working on migration project\n",
encoding="utf-8",
)
migrator = mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=False, migrate_secrets=False, output_dir=None,
selected_options={"daily-memory"},
)
report = migrator.migrate()
mem_path = target / "memories" / "MEMORY.md"
assert mem_path.exists()
content = mem_path.read_text(encoding="utf-8")
assert "dark mode" in content
assert "migration project" in content
def test_provider_keys_require_migrate_secrets_flag(tmp_path: Path):
"""Provider keys migration is double-gated: needs option + --migrate-secrets."""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
target.mkdir()
source.mkdir()
(source / "openclaw.json").write_text(
json.dumps({
"models": {
"providers": {
"openrouter": {
"apiKey": "sk-or-test-key",
"baseUrl": "https://openrouter.ai/api/v1",
}
}
}
}),
encoding="utf-8",
)
# Without --migrate-secrets: should skip
migrator = mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=False, migrate_secrets=False, output_dir=None,
selected_options={"provider-keys"},
)
report = migrator.migrate()
env_path = target / ".env"
if env_path.exists():
assert "sk-or-test-key" not in env_path.read_text(encoding="utf-8")
# With --migrate-secrets: should import
migrator2 = mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=False, migrate_secrets=True, output_dir=None,
selected_options={"provider-keys"},
)
report2 = migrator2.migrate()
env_text = (target / ".env").read_text(encoding="utf-8")
assert "OPENROUTER_API_KEY=sk-or-test-key" in env_text
def test_skill_installs_cleanly_under_skills_guard():
skills_guard = load_skills_guard()
result = skills_guard.scan_skill(
SCRIPT_PATH.parents[1],
source="official/migration/openclaw-migration",
)
# The migration script's references to agent config files are legitimate:
# it mentions AGENTS.md to migrate workspace instructions and points the
# user at ~/.hermes/config.yaml in its post-migration summary — it never
# writes to either. Under skills-guard-v2 (#92021) these score as
# informational _ref findings (the old critical agent_config_mod /
# hermes_config_mod findings no longer fire for bare mentions), so the
# verdict is "safe" with no modification-intent findings at all.
assert result.verdict == "safe", f"Unexpected verdict: {result.verdict}"
KNOWN_FALSE_POSITIVES = {"agent_config_ref", "hermes_config_ref"}
for f in result.findings:
assert f.pattern_id in KNOWN_FALSE_POSITIVES, f"Unexpected finding: {f}"
# ── rebrand_text tests ────────────────────────────────────────
def test_rebrand_text_replaces_openclaw_variants():
mod = load_module()
# Mixed-case / capitalized matches → capital-H ``Hermes``.
assert mod.rebrand_text("OpenClaw prefers Python 3.11") == "Hermes prefers Python 3.11"
assert mod.rebrand_text("I told Open Claw to use dark mode") == "I told Hermes to use dark mode"
assert mod.rebrand_text("Open-Claw config is great") == "Hermes config is great"
assert mod.rebrand_text("OPENCLAW uses tools well") == "Hermes uses tools well"
# All-lowercase matches → lowercase ``hermes``; this preserves the
# real filesystem path ``~/.hermes`` (Hermes home) when rebranding
# memory entries that reference ``~/.openclaw`` or ``openclaw`` prose.
assert mod.rebrand_text("openclaw should always respond concisely") == "hermes should always respond concisely"
# ── migrate_model_config: alias resolution (issue #16745) ──────────────────
def _run_model_migration(tmp_path: Path, openclaw_json: dict) -> dict:
"""Helper: run just migrate_model_config on an openclaw.json and return
the parsed destination config.yaml."""
import yaml
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
source.mkdir(parents=True)
target.mkdir(parents=True)
(source / "openclaw.json").write_text(json.dumps(openclaw_json), encoding="utf-8")
migrator = mod.Migrator(
source_root=source,
target_root=target,
execute=True,
workspace_target=None,
overwrite=True,
migrate_secrets=False,
output_dir=target / "migration-report",
)
migrator.migrate_model_config()
cfg_path = target / "config.yaml"
if not cfg_path.exists():
return {}
return yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
def _extract_model(parsed: dict) -> str | None:
model = parsed.get("model")
if isinstance(model, dict):
return model.get("default")
return model
# ── non-UTF-8 tolerance (issue #8901) ───────────────────────────────────────
def _write_invalid_utf8_json(path: Path, prefix: bytes, valid_value: bytes, suffix: bytes) -> None:
"""Write a JSON-shaped file containing one invalid UTF-8 byte (0xB3) inside
a string value, alongside a separate, validly-encoded value. Used to check
that a single bad byte does not prevent the rest of the file's data from
being read (relies on read_text(..., errors="replace"))."""
path.write_bytes(prefix + b"\xb3" + valid_value + suffix)
def test_command_allowlist_handles_invalid_utf8_bytes(tmp_path: Path):
"""exec-approvals.json with a non-UTF-8 byte should not abort migration;
valid patterns elsewhere in the same file must still be imported."""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
source.mkdir()
target.mkdir()
_write_invalid_utf8_json(
source / "exec-approvals.json",
prefix=b'{"agents": {"*": {"allowlist": [{"pattern": "/bad',
valid_value=b'"}, {"pattern": "/usr/bin/*"}]}}}',
suffix=b"",
)
(target / "config.yaml").write_text("command_allowlist: []\n", encoding="utf-8")
migrator = mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=False, migrate_secrets=False, output_dir=None,
selected_options={"command-allowlist"},
)
report = migrator.migrate()
items = [i for i in report["items"] if i["kind"] == "command-allowlist"]
assert items and items[0]["status"] == "migrated"
config_text = (target / "config.yaml").read_text(encoding="utf-8")
assert "/usr/bin/*" in config_text
def test_messaging_settings_handles_invalid_utf8_in_telegram_allowlist(tmp_path: Path):
"""Telegram allowFrom file with a non-UTF-8 byte should not abort migration;
valid user IDs elsewhere in the same file must still be imported."""
mod = load_module()
source = tmp_path / ".openclaw"
target = tmp_path / ".hermes"
source.mkdir()
target.mkdir()
creds_dir = source / "credentials"
creds_dir.mkdir()
_write_invalid_utf8_json(
creds_dir / "telegram-default-allowFrom.json",
prefix=b'{"allowFrom": ["bad',
valid_value=b'", "123456789"]}',
suffix=b"",
)
migrator = mod.Migrator(
source_root=source, target_root=target, execute=True,
workspace_target=None, overwrite=False, migrate_secrets=False, output_dir=None,
selected_options={"messaging-settings"},
)
report = migrator.migrate()
items = [i for i in report["items"] if i["kind"] == "messaging-settings"]
assert items and items[0]["status"] == "migrated"
env_text = (target / ".env").read_text(encoding="utf-8")
assert "123456789" in env_text
@@ -0,0 +1,323 @@
"""Tests for the OpenClaw→Hermes migration hardening features.
Covers the changes in the "claw migrate hardening" PR:
- secret redaction (engine-level, applied to report JSON)
- warnings[] / next_steps[] on the report
- blocked-by-earlier-conflict sequencing for config.yaml mutations
- --json output mode on the migration script
- enum-like constants and ItemResult.sensitive field
"""
from __future__ import annotations
import importlib.util
import json
import subprocess
import sys
from pathlib import Path
SCRIPT_PATH = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "migration"
/ "openclaw-migration"
/ "scripts"
/ "openclaw_to_hermes.py"
)
def _load():
spec = importlib.util.spec_from_file_location("openclaw_to_hermes_hard", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
# ───────────────────────────────────────────────────────────────────────
# Redaction
# ───────────────────────────────────────────────────────────────────────
def test_redact_replaces_secret_by_key_name():
mod = _load()
out = mod.redact_migration_value({"OPENROUTER_API_KEY": "sk-or-v1-abcdef12345678"})
assert out["OPENROUTER_API_KEY"] == mod.REDACTED_MIGRATION_VALUE
def test_redact_handles_github_token_pattern():
mod = _load()
out = mod.redact_migration_value({"detail": "token: ghp_1234567890abcdef1234"})
assert "ghp_" not in out["detail"]
assert mod.REDACTED_MIGRATION_VALUE in out["detail"]
def test_redact_is_recursive():
mod = _load()
nested = {
"outer": {
"items": [
{"password": "hunter2"},
{"details": {"apiKey": "my-key"}},
],
},
}
out = mod.redact_migration_value(nested)
assert out["outer"]["items"][0]["password"] == mod.REDACTED_MIGRATION_VALUE
assert out["outer"]["items"][1]["details"]["apiKey"] == mod.REDACTED_MIGRATION_VALUE
def test_redact_preserves_non_secret_keys_and_values():
mod = _load()
input_data = {"name": "hermes", "count": 42, "tags": ["a", "b"]}
out = mod.redact_migration_value(input_data)
assert out == input_data
def test_redact_normalizes_key_case_and_punctuation():
mod = _load()
# "Api Key", "api-key", "API_KEY" all normalize the same way.
for key in ("Api Key", "api-key", "API_KEY", "apikey"):
out = mod.redact_migration_value({key: "secret"})
assert out[key] == mod.REDACTED_MIGRATION_VALUE, f"failed to redact: {key}"
def test_redact_leaves_env_secretref_alone():
"""SecretRef-like shapes ({source: env, id: ...}) are pointers, not secrets."""
mod = _load()
ref = {"source": "env", "id": "OPENAI_API_KEY"}
out = mod.redact_migration_value({"apiKey": ref})
# The key "apiKey" itself triggers redaction today — this test locks that in.
# If we later want to exempt SecretRef values the way OpenClaw does, update
# both this test and _redact_internal together.
assert out["apiKey"] == mod.REDACTED_MIGRATION_VALUE
def test_write_report_redacts_api_keys_on_disk(tmp_path):
mod = _load()
report = {
"timestamp": "20260427T120000",
"mode": "execute",
"source_root": "/src",
"target_root": "/tgt",
"summary": {"migrated": 1, "conflict": 0, "error": 0, "skipped": 0, "archived": 0},
"items": [
{
"kind": "provider-keys",
"source": "openclaw.json",
"destination": "/tgt/.env",
"status": "migrated",
"reason": "",
"details": {"OPENROUTER_API_KEY": "sk-or-v1-1234567890abcdef"},
},
],
}
mod.write_report(tmp_path, report)
persisted = json.loads((tmp_path / "report.json").read_text())
# The raw secret must not appear anywhere in the persisted JSON.
assert "sk-or-v1-1234567890abcdef" not in (tmp_path / "report.json").read_text()
assert persisted["items"][0]["details"]["OPENROUTER_API_KEY"] == mod.REDACTED_MIGRATION_VALUE
# ───────────────────────────────────────────────────────────────────────
# Warnings and next-steps
# ───────────────────────────────────────────────────────────────────────
def _make_minimal_migrator(mod, tmp_path, **overrides):
source = tmp_path / "openclaw"
source.mkdir()
# Minimal valid OpenClaw layout so the Migrator constructor doesn't choke.
(source / "openclaw.json").write_text("{}", encoding="utf-8")
target = tmp_path / "hermes"
target.mkdir()
defaults = dict(
source_root=source,
target_root=target,
execute=False,
workspace_target=None,
overwrite=False,
migrate_secrets=False,
output_dir=None,
selected_options=set(),
)
defaults.update(overrides)
return mod.Migrator(**defaults)
def test_conflict_produces_overwrite_warning(tmp_path):
mod = _load()
migrator = _make_minimal_migrator(mod, tmp_path, execute=True)
# Inject a conflict on a config.yaml target to exercise the warning pathway.
migrator.record(
"tts-config",
source=None,
destination=migrator.target_root / "config.yaml",
status=mod.STATUS_CONFLICT,
reason="TTS already configured",
)
report = migrator.build_report()
assert any("--overwrite" in w for w in report["warnings"])
# The conflict on config.yaml should have flipped the block flag too.
assert migrator._config_apply_blocked is True
def test_provider_keys_skipped_warning_when_secrets_disabled(tmp_path):
mod = _load()
migrator = _make_minimal_migrator(mod, tmp_path, execute=True, migrate_secrets=False)
migrator.record(
"provider-keys",
source=None,
destination=None,
status=mod.STATUS_SKIPPED,
reason="--migrate-secrets not set",
)
report = migrator.build_report()
assert any("--migrate-secrets" in w for w in report["warnings"])
# ───────────────────────────────────────────────────────────────────────
# Blocked-by-earlier-conflict sequencing
# ───────────────────────────────────────────────────────────────────────
def test_config_apply_block_flips_on_config_yaml_conflict(tmp_path):
mod = _load()
migrator = _make_minimal_migrator(mod, tmp_path, execute=True)
assert migrator._config_apply_blocked is False
migrator.record(
"model-config",
source=None,
destination=migrator.target_root / "config.yaml",
status=mod.STATUS_CONFLICT,
)
assert migrator._config_apply_blocked is True
def test_run_if_selected_skips_config_ops_after_block(tmp_path):
mod = _load()
migrator = _make_minimal_migrator(
mod, tmp_path, execute=True, selected_options={"model-config", "tts-config"}
)
migrator._config_apply_blocked = True
called = []
migrator.run_if_selected("tts-config", lambda: called.append(True))
assert called == []
# The skipped record uses the blocked reason.
blocked = [i for i in migrator.items if i.kind == "tts-config"]
assert len(blocked) == 1
assert blocked[0].status == mod.STATUS_SKIPPED
assert blocked[0].reason == mod.REASON_BLOCKED_BY_APPLY_CONFLICT
def test_dry_run_never_blocks_even_after_conflict(tmp_path):
"""Dry runs must preview the full plan — blocking mid-preview would hide
conflicts and mislead the user about what would actually happen."""
mod = _load()
migrator = _make_minimal_migrator(
mod, tmp_path, execute=False, selected_options={"tts-config"}
)
migrator._config_apply_blocked = True
called = []
migrator.run_if_selected("tts-config", lambda: called.append(True))
assert called == [True]
# ───────────────────────────────────────────────────────────────────────
# --json output mode
# ───────────────────────────────────────────────────────────────────────
def test_json_mode_emits_structured_report(tmp_path):
"""End-to-end: run the CLI with --json and no --execute, parse stdout."""
source = tmp_path / "openclaw"
source.mkdir()
(source / "openclaw.json").write_text(
json.dumps({"agents": {"defaults": {"model": "openrouter/anthropic/claude-sonnet-4"}}}),
encoding="utf-8",
)
target = tmp_path / "hermes"
target.mkdir()
result = subprocess.run(
[
sys.executable,
str(SCRIPT_PATH),
"--source", str(source),
"--target", str(target),
"--json",
],
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert "summary" in payload
assert "warnings" in payload
assert "next_steps" in payload
assert payload["mode"] == "dry-run"
def test_json_mode_redacts_secrets_in_output(tmp_path):
"""Even plan-only JSON output goes through the redactor — the stdout
capture path is what gets piped into CI / support tickets."""
source = tmp_path / "openclaw"
source.mkdir()
(source / "openclaw.json").write_text("{}", encoding="utf-8")
# Plant a fake OpenClaw .env with a recognizably-shaped key.
(source / ".env").write_text(
"OPENROUTER_API_KEY=sk-or-v1-abcdef1234567890abcdef\n", encoding="utf-8"
)
target = tmp_path / "hermes"
target.mkdir()
result = subprocess.run(
[
sys.executable,
str(SCRIPT_PATH),
"--source", str(source),
"--target", str(target),
"--migrate-secrets", # so provider-keys surface in the plan
"--json",
],
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, result.stderr
# The raw key value must never appear in the JSON output.
assert "sk-or-v1-abcdef1234567890abcdef" not in result.stdout
# ───────────────────────────────────────────────────────────────────────
# ItemResult schema additions
# ───────────────────────────────────────────────────────────────────────
def test_record_honors_sensitive_flag(tmp_path):
mod = _load()
migrator = _make_minimal_migrator(mod, tmp_path)
migrator.record("x", None, None, "migrated", sensitive=True)
assert migrator.items[0].sensitive is True
def test_status_constants_match_historical_strings():
"""Downstream consumers (claw.py, tests, docs) depend on these string values."""
mod = _load()
assert mod.STATUS_MIGRATED == "migrated"
assert mod.STATUS_SKIPPED == "skipped"
assert mod.STATUS_CONFLICT == "conflict"
assert mod.STATUS_ERROR == "error"
assert mod.STATUS_ARCHIVED == "archived"
@@ -0,0 +1,82 @@
"""
Smoke tests for the pinecone-research optional skill.
Validates:
- SKILL.md frontmatter conforms to the ≤60-char description standard
- The skill name is distinct from the existing mlops/pinecone skill
- Frontmatter has required fields
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
import yaml
SKILL_DIR = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "research"
/ "pinecone-research"
)
MLOPS_PINECONE_DIR = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "mlops"
/ "pinecone"
)
@pytest.fixture(scope="module")
def frontmatter() -> dict:
src = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
m = re.search(r"^---\n(.*?)\n---", src, re.DOTALL)
assert m, "SKILL.md missing YAML frontmatter"
return yaml.safe_load(m.group(1))
def test_skill_dir_exists() -> None:
assert SKILL_DIR.is_dir(), f"missing skill dir: {SKILL_DIR}"
def test_description_under_60_chars(frontmatter) -> None:
desc = frontmatter["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars (limit ≤60): {desc!r}"
def test_has_required_frontmatter_fields(frontmatter) -> None:
for field in ("name", "description", "version", "license"):
assert field in frontmatter, f"missing required field: {field}"
@pytest.mark.parametrize(
"path",
[
"scripts/rag_pipeline.py",
"scripts/memory_manager.py",
],
)
def test_shipped_scripts_parse(path: str) -> None:
"""Shipped scripts must be valid Python (ast.parse raises SyntaxError otherwise)."""
import ast
src = (SKILL_DIR / path).read_text(encoding="utf-8")
ast.parse(src) # raises SyntaxError on broken Python
def test_scripts_use_pinecone_client() -> None:
"""Both scripts should reference the Pinecone SDK."""
for script in ("scripts/rag_pipeline.py", "scripts/memory_manager.py"):
src = (SKILL_DIR / script).read_text(encoding="utf-8")
assert "PINECONE_API_KEY" in src, f"{script} should read PINECONE_API_KEY"
assert "Pinecone" in src, f"{script} should import/use Pinecone client"
@@ -0,0 +1,117 @@
"""Tests for the product-price-monitor skill and its price-watch blueprint."""
import re
from pathlib import Path
import yaml
SKILL_PATH = (
Path(__file__).resolve().parents[2]
/ "skills"
/ "productivity"
/ "product-price-monitor"
/ "SKILL.md"
)
def _frontmatter_and_body():
content = SKILL_PATH.read_text(encoding="utf-8")
assert content.startswith("---")
m = re.search(r"\n---\s*\n", content[3:])
assert m, "frontmatter must close with ---"
fm = yaml.safe_load(content[3 : m.start() + 3])
body = content[m.end() + 3 :]
return fm, body
def test_skill_file_exists():
assert SKILL_PATH.is_file()
def test_frontmatter_required_fields():
fm, _ = _frontmatter_and_body()
for field in ("name", "description", "version", "author", "license", "platforms"):
assert field in fm, f"missing frontmatter field: {field}"
assert fm["name"] == "product-price-monitor"
def test_description_hardline():
fm, _ = _frontmatter_and_body()
desc = fm["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60"
assert desc.endswith(".")
def test_author_credits_human_first():
fm, _ = _frontmatter_and_body()
assert not fm["author"].startswith("Hermes Agent")
assert "benbarclay" in fm["author"]
def test_related_skills_resolve_in_repo():
fm, _ = _frontmatter_and_body()
repo_root = SKILL_PATH.parents[3]
for name in fm["metadata"]["hermes"]["related_skills"]:
hits = (
list(repo_root.glob(f"skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"optional-skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"skills/*/*/{name}/SKILL.md"))
)
assert hits, f"related_skills entry does not resolve in-repo: {name}"
def test_no_phantom_connectors_in_prose():
_, body = _frontmatter_and_body()
assert "flight-research" not in body, "no phantom 'flight-research' skill references"
def test_setup_tick_split():
"""The skill must separate one-time setup from the recurring cron tick."""
_, body = _frontmatter_and_body()
assert "Setup (foreground, once)" in body
assert "Tick (each scheduled run)" in body
assert "cronjob(action=" in body, "must wire scheduling through the cronjob tool"
assert "Do not schedule until one foreground fetch works" in body
def test_state_discipline_present():
_, body = _frontmatter_and_body()
assert "never overwrite the last good observation" in body
assert "fingerprint" in body
def test_steps_have_completion_criteria():
_, body = _frontmatter_and_body()
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## )", body, re.MULTILINE | re.DOTALL)
assert len(steps) >= 5
for step in steps:
assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}"
def test_no_machine_local_paths():
content = SKILL_PATH.read_text(encoding="utf-8")
assert not re.search(r"/home/(?!.*price-watches)", content)
assert "/home/bb" not in content
def test_price_watch_blueprint_registered():
from cron.blueprint_catalog import CATALOG
bp = next((b for b in CATALOG if b.key == "price-watch"), None)
assert bp is not None, "price-watch blueprint missing from catalog"
assert "product-price-monitor" in bp.skills, "blueprint must load the skill"
slot_names = {s.name for s in bp.slots}
assert {"item", "condition", "interval_h", "deliver"} <= slot_names
assert "[SILENT]" in bp.prompt_template, "silent path must be explicit"
assert "{item}" in bp.prompt_template and "{condition}" in bp.prompt_template
def test_price_watch_blueprint_schedule_resolves():
from cron.blueprint_catalog import CATALOG
bp = next(b for b in CATALOG if b.key == "price-watch")
interval_slot = next(s for s in bp.slots if s.name == "interval_h")
for opt in interval_slot.options:
expr = bp.schedule_template.format(interval_h=opt)
fields = expr.split()
assert len(fields) == 5, f"invalid cron expr: {expr}"
assert fields[1] == f"*/{opt}"
+143
View File
@@ -0,0 +1,143 @@
"""Tests for the optional-skills/web-development/publish-site skill.
Structural + internal-consistency checks only (stdlib + pytest, no network).
"""
import re
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parents[2]
SKILL_DIR = REPO / "optional-skills" / "web-development" / "publish-site"
SKILL_MD = SKILL_DIR / "SKILL.md"
VALID_PLATFORMS = {"linux", "macos", "windows"}
@pytest.fixture(scope="module")
def skill_text() -> str:
return SKILL_MD.read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def frontmatter(skill_text: str) -> dict:
"""Parse the YAML frontmatter with stdlib only (flat key: value fields)."""
m = re.match(r"^---\n(.*?)\n---\n", skill_text, re.DOTALL)
assert m, "SKILL.md must open with '---' delimited YAML frontmatter"
fm: dict[str, str] = {}
for line in m.group(1).splitlines():
if line.startswith((" ", "\t")) or ":" not in line:
continue # nested metadata keys — not needed here
key, _, value = line.partition(":")
fm[key.strip()] = value.strip()
return fm
def test_skill_file_exists():
assert SKILL_MD.is_file(), f"missing {SKILL_MD}"
def test_frontmatter_parses(frontmatter: dict):
assert frontmatter.get("name") == "publish-site"
assert frontmatter.get("version"), "version field required"
assert frontmatter.get("license") == "MIT"
assert "Hermes Agent" in frontmatter.get("author", "")
def test_description_length_and_period(frontmatter: dict):
desc = frontmatter.get("description", "").strip().strip('"')
assert desc, "no description field"
assert len(desc) <= 60, f"description is {len(desc)} chars (>60): {desc!r}"
assert desc.endswith("."), "description must end with a period"
def test_platforms_list_valid(frontmatter: dict):
raw = frontmatter.get("platforms", "")
platforms = [p.strip() for p in raw.strip("[]").split(",") if p.strip()]
assert platforms, "platforms list must be non-empty"
assert set(platforms) <= VALID_PLATFORMS, f"invalid platforms: {platforms}"
# gh/wrangler/netlify are all cross-platform — the skill keeps all three.
assert set(platforms) == VALID_PLATFORMS
def test_required_sections_present(skill_text: str):
for heading in (
"## When to Use",
"## Prerequisites",
"## How to Run",
"## Quick Reference",
"## Procedure",
"## Pitfalls",
"## Verification",
):
assert heading in skill_text, f"missing section: {heading}"
def test_no_dangling_skill_view_references(skill_text: str):
"""Every skill_view(name='...') reference must target a shipped skill."""
targets = re.findall(r"skill_view\(\s*name\s*=\s*['\"]([^'\"]+)['\"]", skill_text)
for name in targets:
hits = list((REPO / "skills").glob(f"**/{name}/SKILL.md")) + list(
(REPO / "optional-skills").glob(f"**/{name}/SKILL.md")
)
assert hits, f"skill_view reference to non-shipped skill: {name}"
def test_referenced_sibling_skills_ship(skill_text: str):
"""Prose-referenced companion skills must exist in the shipped trees."""
for name in ("cloudflare-temporary-deploy",):
if name in skill_text:
hits = list((REPO / "skills").glob(f"**/{name}/SKILL.md")) + list(
(REPO / "optional-skills").glob(f"**/{name}/SKILL.md")
)
assert hits, f"referenced skill does not ship: {name}"
def test_provider_ladder_documented(skill_text: str):
"""All three rungs of the provider ladder with their deploy commands."""
assert "gh repo create" in skill_text
assert "gh-pages" in skill_text
assert "wrangler@latest pages deploy" in skill_text
assert "netlify deploy --prod" in skill_text
def test_version_before_deploy_discipline(skill_text: str):
assert "git tag" in skill_text, "deploys must be tagged"
assert "Never deploy uncommitted files" in skill_text
def test_rollback_documented(skill_text: str):
assert "Rollback" in skill_text
assert re.search(r"git checkout deploy-", skill_text), "rollback must redeploy a previous tag"
def test_secrets_never_in_repo(skill_text: str):
assert "NEVER commit secrets" in skill_text
assert ".gitignore" in skill_text
def test_spa_404_pitfall_documented(skill_text: str):
assert "404.html" in skill_text
assert "_redirects" in skill_text
def test_verification_uses_real_http_check(skill_text: str):
assert "%{http_code}" in skill_text
assert "200" in skill_text
assert "deploy log alone" in skill_text
def test_preview_before_deploy(skill_text: str):
assert "http.server" in skill_text
assert "cloudflared tunnel --url" in skill_text
assert "trycloudflare.com" in skill_text
def test_category_description_exists():
desc = REPO / "optional-skills" / "web-development" / "DESCRIPTION.md"
assert desc.is_file(), "optional web-development category needs a DESCRIPTION.md"
if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-q"]))
+95
View File
@@ -0,0 +1,95 @@
"""Contract tests for the bundled SDLC review skill."""
from __future__ import annotations
import re
from pathlib import Path
import pytest
SKILL_MD = (
Path(__file__).resolve().parents[2]
/ "skills"
/ "devops"
/ "sdlc-review"
/ "SKILL.md"
)
REQUIRED_SECTIONS = [
"## When to Use",
"## Prerequisites",
"## How to Run",
"## Quick Reference",
"## Review Lenses",
"## Procedure",
"## Pitfalls",
"## Verification",
]
REVIEW_ACTIONS = {
"kanban_show",
"kanban_comment",
"kanban_complete",
"kanban_request_changes",
"kanban_block",
}
@pytest.fixture(scope="module")
def skill_text() -> str:
return SKILL_MD.read_text(encoding="utf-8")
def _frontmatter_value(text: str, key: str) -> str:
match = re.search(rf"^{re.escape(key)}:\s*(.+)$", text, re.MULTILINE)
assert match, f"missing frontmatter field: {key}"
return match.group(1).strip()
def test_frontmatter_meets_hardline_standard(skill_text: str) -> None:
assert skill_text.startswith("---\n")
assert _frontmatter_value(skill_text, "name") == "sdlc-review"
description = _frontmatter_value(skill_text, "description")
assert len(description) <= 60
assert description.endswith(".")
for field in ("version", "author", "license", "platforms"):
assert _frontmatter_value(skill_text, field)
assert not _frontmatter_value(skill_text, "author").startswith("Hermes Agent")
def test_body_uses_required_modern_section_order(skill_text: str) -> None:
assert "# SDLC Review Skill" in skill_text
positions = [skill_text.index(section) for section in REQUIRED_SECTIONS]
assert positions == sorted(positions)
@pytest.mark.parametrize("tool_name", sorted(REVIEW_ACTIONS))
def test_skill_documents_native_review_actions(
skill_text: str,
tool_name: str,
) -> None:
assert f"`{tool_name}`" in skill_text
def test_verdicts_route_through_distinct_terminal_actions(skill_text: str) -> None:
quick_reference = skill_text.split("## Quick Reference", 1)[1].split(
"## Review Lenses", 1
)[0]
assert "Approve" in quick_reference and "`kanban_complete`" in quick_reference
assert "Request changes" in quick_reference
assert "`kanban_request_changes`" in quick_reference
assert "Escalate" in quick_reference and "`kanban_block`" in quick_reference
def test_review_lenses_vary_per_round(skill_text: str) -> None:
lenses = skill_text.split("## Review Lenses", 1)[1].split("## Procedure", 1)[0]
# Round derivation must key off history the reviewer actually sees.
assert "`changes_requested`" in lenses
assert "Prior attempts on this task" in lenses
# One distinct lens per round.
for lens in ("Artifact", "Execution", "Contract"):
assert lens in lenses
# Execution lens must direct empirical verification via the terminal.
assert "`terminal`" in lenses
# Fan-out note: parallel reviewers get different briefs.
assert "`delegate_task`" in lenses
@@ -0,0 +1,87 @@
"""Tests for optional-skills/devops/setup-wizard-generator (template integrity)."""
import re
import subprocess
from pathlib import Path
import pytest
import yaml
SKILL_DIR = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "devops"
/ "setup-wizard-generator"
)
SKILL_MD = SKILL_DIR / "SKILL.md"
TEMPLATE = SKILL_DIR / "templates" / "template.sh"
class TestFrontmatter:
def _fm(self):
text = SKILL_MD.read_text(encoding="utf-8")
m = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
assert m, "SKILL.md missing YAML frontmatter"
return yaml.safe_load(m.group(1))
def test_name_matches_directory(self):
assert self._fm()["name"] == "setup-wizard-generator"
def test_description_length(self):
assert len(self._fm()["description"]) <= 60
def test_license_and_platforms(self):
fm = self._fm()
assert fm["license"] == "MIT"
assert "linux" in fm["platforms"]
class TestTemplate:
def test_template_exists(self):
assert TEMPLATE.is_file()
def test_bash_syntax(self):
proc = subprocess.run(
["bash", "-n", str(TEMPLATE)], capture_output=True, text=True
)
assert proc.returncode == 0, proc.stderr
def test_stages_marker_present(self):
text = TEMPLATE.read_text(encoding="utf-8")
assert "STAGES" in text, "authoring marker missing"
def test_library_helpers_defined(self):
text = TEMPLATE.read_text(encoding="utf-8")
for helper in (
"stage()",
"say()",
"step()",
"open_url()",
"write_env()",
"set_secret()",
"finish()",
):
assert helper in text, f"missing library helper {helper}"
def test_ask_secret_defined(self):
# secret entry must exist (hidden input path)
assert "ask_secret" in TEMPLATE.read_text(encoding="utf-8")
def test_total_stages_variable(self):
assert re.search(
r"^TOTAL_STAGES=", TEMPLATE.read_text(encoding="utf-8"), re.MULTILINE
)
class TestSkillBody:
def test_references_template_path(self):
assert "templates/template.sh" in SKILL_MD.read_text(encoding="utf-8")
def test_no_upstream_harness_residue(self):
text = SKILL_MD.read_text(encoding="utf-8").lower()
for token in ("claude", "/wizard", "slash command"):
assert token not in text, f"upstream residue: {token}"
if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-q"]))
@@ -0,0 +1,91 @@
"""Tests for the social-media-content-calendar optional skill."""
import re
from pathlib import Path
import yaml
SKILL_PATH = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "creative"
/ "social-media-content-calendar"
/ "SKILL.md"
)
def _frontmatter_and_body():
content = SKILL_PATH.read_text(encoding="utf-8")
assert content.startswith("---")
m = re.search(r"\n---\s*\n", content[3:])
assert m, "frontmatter must close with ---"
fm = yaml.safe_load(content[3 : m.start() + 3])
body = content[m.end() + 3 :]
return fm, body
def test_skill_file_exists():
assert SKILL_PATH.is_file()
def test_frontmatter_required_fields():
fm, _ = _frontmatter_and_body()
for field in ("name", "description", "version", "author", "license", "platforms"):
assert field in fm, f"missing frontmatter field: {field}"
assert fm["name"] == "social-media-content-calendar"
def test_description_hardline():
fm, _ = _frontmatter_and_body()
desc = fm["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60"
assert desc.endswith(".")
def test_author_credits_human_first():
fm, _ = _frontmatter_and_body()
assert not fm["author"].startswith("Hermes Agent")
assert "benbarclay" in fm["author"]
def test_related_skills_resolve_in_repo():
fm, _ = _frontmatter_and_body()
repo_root = SKILL_PATH.parents[3]
for name in fm["metadata"]["hermes"]["related_skills"]:
hits = (
list(repo_root.glob(f"skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"optional-skills/*/{name}/SKILL.md"))
+ list(repo_root.glob(f"skills/*/*/{name}/SKILL.md"))
)
assert hits, f"related_skills entry does not resolve in-repo: {name}"
def test_no_phantom_skill_references():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "image-generation-workflow" not in content, "phantom skill ref must be gone"
def test_honest_handoff_language():
_, body = _frontmatter_and_body()
assert "handed-off, not published" in body or "handed-off slots" in body, (
"platforms without connectors must end at handoff, not claimed publication"
)
def test_body_structure_and_size():
_, body = _frontmatter_and_body()
for section in ("## When to Use", "## Procedure", "## Pitfalls", "## Verification"):
assert section in body, f"missing section: {section}"
assert len(SKILL_PATH.read_text(encoding="utf-8")) <= 100_000
def test_no_machine_local_paths():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "/home/" not in content
def test_steps_have_completion_criteria():
_, body = _frontmatter_and_body()
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## )", body, re.MULTILINE | re.DOTALL)
assert len(steps) >= 6
for step in steps:
assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}"
+134
View File
@@ -0,0 +1,134 @@
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
SCRIPT_PATH = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "productivity"
/ "telephony"
/ "scripts"
/ "telephony.py"
)
def load_module():
spec = importlib.util.spec_from_file_location("telephony_skill", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def test_save_twilio_writes_env_and_state(tmp_path: Path, monkeypatch):
mod = load_module()
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
result = mod.save_twilio(
"AC123",
"secret-token",
phone_number="+1 (702) 555-1234",
phone_sid="PN123",
)
env_text = (tmp_path / ".hermes" / ".env").read_text(encoding="utf-8")
state = json.loads((tmp_path / ".hermes" / "telephony_state.json").read_text(encoding="utf-8"))
assert result["success"] is True
assert "TWILIO_ACCOUNT_SID=AC123" in env_text
assert "TWILIO_AUTH_TOKEN=secret-token" in env_text
assert "TWILIO_PHONE_NUMBER=+17025551234" in env_text
assert "TWILIO_PHONE_NUMBER_SID=PN123" in env_text
assert state["twilio"]["default_phone_number"] == "+17025551234"
assert state["twilio"]["default_phone_sid"] == "PN123"
def test_upsert_env_updates_existing_values(tmp_path: Path):
mod = load_module()
env_path = tmp_path / ".env"
env_path.write_text("TWILIO_PHONE_NUMBER=+15550000000\nOTHER=keep\n", encoding="utf-8")
mod._upsert_env_file(
{
"TWILIO_PHONE_NUMBER": "+15551112222",
"TWILIO_PHONE_NUMBER_SID": "PN999",
},
env_path=env_path,
)
env_text = env_path.read_text(encoding="utf-8")
assert "TWILIO_PHONE_NUMBER=+15551112222" in env_text
assert "TWILIO_PHONE_NUMBER_SID=PN999" in env_text
assert "OTHER=keep" in env_text
def test_twilio_buy_number_saves_env_and_state(tmp_path: Path):
mod = load_module()
state_path = tmp_path / "telephony_state.json"
env_path = tmp_path / ".env"
mod._twilio_request = lambda method, path, params=None, form=None: {
"sid": "PN111",
"phone_number": "+17025550123",
"friendly_name": "Test Number",
"capabilities": {"voice": True, "sms": True},
}
result = mod._twilio_buy_number(
"+17025550123",
save_env=True,
state_path=state_path,
env_path=env_path,
)
state = json.loads(state_path.read_text(encoding="utf-8"))
env_text = env_path.read_text(encoding="utf-8")
assert result["phone_sid"] == "PN111"
assert state["twilio"]["default_phone_number"] == "+17025550123"
assert state["twilio"]["default_phone_sid"] == "PN111"
assert "TWILIO_PHONE_NUMBER=+17025550123" in env_text
assert "TWILIO_PHONE_NUMBER_SID=PN111" in env_text
def test_diagnose_includes_decision_tree_and_saved_state(tmp_path: Path, monkeypatch):
mod = load_module()
hermes_home = tmp_path / ".hermes"
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
mod._save_state(
{
"version": 1,
"twilio": {
"default_phone_number": "+17025550123",
"last_inbound_message_sid": "SM123",
},
"vapi": {
"phone_number_id": "vapi-abc",
},
},
hermes_home / "telephony_state.json",
)
(hermes_home / ".env").parent.mkdir(parents=True, exist_ok=True)
(hermes_home / ".env").write_text(
"TWILIO_ACCOUNT_SID=AC123\nTWILIO_AUTH_TOKEN=token\nBLAND_API_KEY=bland\n",
encoding="utf-8",
)
result = mod.diagnose()
assert result["providers"]["twilio"]["default_phone_number"] == "+17025550123"
assert result["providers"]["twilio"]["last_inbound_message_sid"] == "SM123"
assert result["providers"]["bland"]["configured"] is True
assert result["providers"]["vapi"]["phone_number_id"] == "vapi-abc"
assert any(item["use"] == "Twilio" for item in result["decision_tree"])
+113
View File
@@ -0,0 +1,113 @@
"""Tests for the tldraw-offline optional skill.
Structural + internal-consistency checks only (stdlib + pytest, no network).
The skill's runtime claims were validated live against the real tldraw offline
app (headless) and its bundled script-context.d.ts; scripts/validate_shapes.mjs
re-checks the shape schema against the tldraw SDK.
"""
import re
from pathlib import Path
import pytest
SKILL_DIR = (
Path(__file__).resolve().parents[2]
/ "optional-skills"
/ "creative"
/ "tldraw-offline"
)
SKILL_MD = SKILL_DIR / "SKILL.md"
MAIN_JS = SKILL_DIR / "scripts" / "main.js"
@pytest.fixture(scope="module")
def skill_text() -> str:
return SKILL_MD.read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def main_js() -> str:
return MAIN_JS.read_text(encoding="utf-8")
def test_skill_file_exists():
assert SKILL_MD.is_file(), f"missing {SKILL_MD}"
def test_frontmatter_present(skill_text: str):
assert skill_text.startswith("---\n"), "SKILL.md must open with YAML frontmatter"
assert skill_text.count("---") >= 2, "frontmatter must be delimited by two '---'"
def test_required_sections_present(skill_text: str):
for heading in (
"## When to Use",
"## Prerequisites",
"## How to Run",
"## Quick Reference",
"## Procedure",
"## Pitfalls",
"## Verification",
):
assert heading in skill_text, f"missing section: {heading}"
def test_counter_example_is_interactive_and_safe():
"""The counter.js example must show the verified interactive-UI pattern:
ctx contract, pointer_down handling, and REQUIRED signal-based cleanup
(whose absence causes the double-fire bug found live)."""
counter = (SKILL_DIR / "scripts" / "counter.js").read_text(encoding="utf-8")
assert "export default function ({ editor, helpers, signal })" in counter
assert "pointer_down" in counter
assert "editor.on('event'" in counter
# the cleanup that prevents the click-doubling leak
assert "signal.addEventListener('abort'" in counter
assert "editor.off('event'" in counter
# state kept in meta, rendered as label
assert "meta" in counter and "count" in counter
def test_uses_richtext_not_bare_string(skill_text: str):
assert "toRichText" in skill_text
assert "richText" in skill_text
def test_main_js_matches_verified_contract(main_js: str):
# main.js must use the real contract learned from the running app.
assert "export default function ({ editor, helpers, signal })" in main_js
# primitives imported from tldraw, not used as globals
assert "from 'tldraw'" in main_js
assert "createShapeId" in main_js and "toRichText" in main_js
# idempotent furniture
assert "createShapeIfMissing" in main_js
# batched writes
assert "editor.run(" in main_js
# reactive + REQUIRED signal cleanup
assert "editor.store.listen" in main_js
assert "signal.addEventListener('abort'" in main_js
# script-owned writes kept out of undo
assert "history: 'ignore'" in main_js
def test_platforms_declared(skill_text: str):
m = re.search(r"^platforms: (.*)$", skill_text, re.MULTILINE)
assert m, "platforms field required (cross-platform desktop app)"
for os_name in ("linux", "macos", "windows"):
assert os_name in m.group(1)
+772
View File
@@ -0,0 +1,772 @@
"""Hermetic tests for the unbroker skill.
Stdlib + pytest only; NO live network, NO browser, NO email. Each test runs against
an isolated temp PDD_DATA_DIR. Runnable with pytest or directly:
python3 -m pytest tests/test_unbroker_skill.py -q
python3 tests/test_unbroker_skill.py # portable fallback runner
"""
from __future__ import annotations
import contextlib
import os
import shutil
import sys
import tempfile
from pathlib import Path
# Resolve the skill's scripts dir across layouts: standalone dev repo (tests/) and hermes-agent
# (tests/skills/ -> optional-skills/security/unbroker/scripts).
_HERE = Path(__file__).resolve()
_REL = ("optional-skills", "security", "unbroker", "scripts")
_CANDIDATES = [
_HERE.parent.parent / "skill" / "scripts", # standalone dev repo
_HERE.parent.parent.joinpath(*_REL), # standalone layout
_HERE.parent.parent.parent.joinpath(*_REL), # hermes-agent (tests/skills/)
]
SCRIPTS = next((c for c in _CANDIDATES if (c / "pdd.py").exists()), _CANDIDATES[0])
sys.path.insert(0, str(SCRIPTS))
import autopilot # noqa: E402
import contextlib as _ctx # noqa: E402
import io as _io # noqa: E402
import json as _json # noqa: E402
import smtplib as _smtplib # noqa: E402
import time as _time # noqa: E402
import badbool # noqa: E402
import brokers # noqa: E402
import cdp # noqa: E402
import config # noqa: E402
import crypto # noqa: E402
import dossier # noqa: E402
import email_modes # noqa: E402
import emailer # noqa: E402
import pdd # noqa: E402
import legal # noqa: E402
import ledger # noqa: E402
import paths # noqa: E402
import registry # noqa: E402
import report # noqa: E402
import storage # noqa: E402
import tiers # noqa: E402
import vectors # noqa: E402
_AGE = bool(shutil.which("age") and shutil.which("age-keygen"))
@contextlib.contextmanager
def temp_env():
"""Isolate every test in a fresh PDD_DATA_DIR."""
prev = os.environ.get("PDD_DATA_DIR")
with tempfile.TemporaryDirectory() as d:
os.environ["PDD_DATA_DIR"] = str(Path(d) / "pdd")
try:
yield Path(os.environ["PDD_DATA_DIR"])
finally:
if prev is None:
os.environ.pop("PDD_DATA_DIR", None)
else:
os.environ["PDD_DATA_DIR"] = prev
def _consenting(full_name="Jane Q. Public"):
return {
"subject_id": "sub_test01",
"consent": {"authorized": True, "method": "self"},
"identity": {
"full_name": full_name,
"emails": ["jane@example.com"],
"phones": ["+1-415-555-0137"],
"date_of_birth": "1987-04-12",
"current_address": {"city": "Oakland", "state": "CA", "postal": "94601"},
},
"preferences": {"email_mode": "draft_only"},
}
# --- config -------------------------------------------------------------------
def test_browser_clears_captcha_logic():
assert config.browser_clears_captcha({"browser_backend": "browserbase"}) is True
assert config.browser_clears_captcha({"browser_backend": "agent-browser"}) is False
assert config.browser_clears_captcha({"browser_backend": "auto"}, env={}) is False
assert config.browser_clears_captcha({"browser_backend": "auto"}, env={"BROWSERBASE_API_KEY": "x"}) is True
# --- storage ------------------------------------------------------------------
def test_storage_json_and_jsonl_roundtrip():
with temp_env() as data:
p = data / "x.json"
storage.write_json(p, {"a": 1})
assert storage.read_json(p) == {"a": 1}
assert storage.read_json(data / "missing.json", []) == []
log = data / "audit.jsonl"
storage.append_jsonl(log, {"e": 1})
storage.append_jsonl(log, {"e": 2})
assert [r["e"] for r in storage.read_jsonl(log)] == [1, 2]
# --- at-rest encryption -------------------------------------------------------
# --- broker DB ----------------------------------------------------------------
def test_seed_broker_db_loads_and_is_well_formed():
everyone = brokers.load_all()
assert len(everyone) >= 10
ids = {b["id"] for b in everyone}
assert {"spokeo", "whitepages", "mylife"} <= ids
for b in everyone:
assert b.get("id") and b.get("name") and b.get("priority") in {"crucial", "high", "standard", "long_tail"}
assert (b.get("optout") or {}).get("method")
def test_blocked_pass_records_and_cluster_coverage():
# Records added from the blocked-tail pass load, resolve, and dedupe correctly.
ids = {b["id"] for b in brokers.load_all()}
assert {"addresses", "socialcatfish"} <= ids
# addresses.com is a PeopleConnect/Intelius front-end -> covered by the intelius cluster (deduped).
assert "addresses" in brokers.clusters().get("intelius", [])
for bid in ("addresses", "socialcatfish"):
b = brokers.get(bid)
assert tiers.select_tier(b) in {"T0", "T1", "T2", "T3"}
assert b["optout"]["method"]
# --- tier selection -----------------------------------------------------------
def test_every_broker_resolves_to_valid_tier():
for b in brokers.load_all():
assert tiers.select_tier(b) in {"T0", "T1", "T2", "T3"}
def test_captcha_tier_shifts_with_browser():
tps = brokers.get("truepeoplesearch")
assert tiers.select_tier(tps, "programmatic", browser_clears_captcha=False) == "T2"
assert tiers.select_tier(tps, "programmatic", browser_clears_captcha=True) == "T1"
def test_plan_excludes_disallowed_fields():
d = _consenting()
actions = tiers.plan(d, brokers.load_all(), config.DEFAULT_CONFIG)
for a in actions:
assert "ssn" not in a["disclosure_fields"]
assert "profile_url" not in a["disclosure_fields"]
def _mini_broker(bid, owns=None, requires=None, notes="", quirks=None):
return {"id": bid, "name": bid.title(), "priority": "high",
"search": {"by": ["name"]},
"optout": {"method": "web_form", "url": f"https://{bid}.example/optout",
"requires": requires or {}, "inputs": ["full_name"], "owns": owns or [],
"notes": notes, "quirks": quirks or []},
"owns": owns or []}
def test_batch_plan_groups_by_ledger_state():
d = _consenting()
bl = [_mini_broker("aaa"), _mini_broker("bbb"), _mini_broker("ccc"), _mini_broker("ddd")]
ledger = {
"aaa": {"state": "found"},
"bbb": {"state": "not_found"},
"ccc": {"state": "blocked"},
# ddd absent -> unscanned/new
}
bp = tiers.batch_plan(d, bl, config.DEFAULT_CONFIG, ledger)
assert bp["phase"] == "discover" # ddd is unscanned
assert bp["counts"]["found"] == 1
assert bp["counts"]["not_found"] == 1
assert bp["counts"]["blocked"] == 1
assert bp["counts"]["unscanned"] == 1
assert any("PHASE 1" in t for t in bp["next_actions"])
# --- ledger / state machine ---------------------------------------------------
def test_ledger_valid_transition_and_audit():
with temp_env():
sid = "sub_test01"
ledger.transition(sid, "spokeo", "searching")
case = ledger.transition(sid, "spokeo", "found", found=True)
assert case["state"] == "found" and case["found"] is True
# found -> submitted must be allowed directly (action_selected is optional)
case = ledger.transition(sid, "spokeo", "submitted")
assert case["state"] == "submitted"
audit = storage.read_jsonl(__import__("paths").audit_path(sid))
assert any(e["to"] == "found" for e in audit)
def test_indirect_exposure_state_and_transitions():
with temp_env():
sid = "sub_test01"
# a scan can land directly on indirect_exposure (PII on a relative's record)
case = ledger.transition(sid, "thatsthem", "indirect_exposure",
evidence={"summary": "email on relative record"})
assert case["state"] == "indirect_exposure"
# the lever from there is a targeted delete-my-PII request (-> submitted)
assert ledger.transition(sid, "thatsthem", "submitted")["state"] == "submitted"
# and a separate broker: not_found -> indirect_exposure is allowed (found on re-read)
ledger.transition(sid, "radaris", "not_found")
assert ledger.transition(sid, "radaris", "indirect_exposure")["state"] == "indirect_exposure"
# re-scan can clear it
assert ledger.transition(sid, "radaris", "not_found")["state"] == "not_found"
# --- dossier / consent / least-disclosure ------------------------------------
def test_least_disclosure_selection():
d = _consenting()
got = dossier.select_disclosure(d, ["full_name", "contact_email", "profile_url", "ssn", "date_of_birth"])
assert set(got) == {"full_name", "contact_email", "date_of_birth"}
assert "ssn" not in got and "profile_url" not in got
# --- alternates / search vectors ---------------------------------------------
def test_all_names_and_locations_dedupe():
d = _consenting()
d["identity"]["also_known_as"] = ["Jane Public", "Jane Q. Public"] # 2nd dups primary
d["identity"]["prior_addresses"] = [{"city": "Berkeley", "state": "CA"}, {"city": "Oakland", "state": "CA"}]
assert dossier.all_names(d) == ["Jane Q. Public", "Jane Public"]
assert [loc["city"] for loc in dossier.all_locations(d)] == ["Oakland", "Berkeley"] # current first, deduped
# --- opaque ids / fan-out / antibot ------------------------------------------
def test_subject_id_is_opaque_no_name_leak():
sid = dossier.new_subject_id("Maiden Married Person")
assert sid.startswith("sub_")
assert "maiden" not in sid.lower() and "person" not in sid.lower()
assert dossier.new_subject_id("Maiden Married Person") != sid # not derived from the name
def test_fanout_batches_large_runs():
g = tiers.fanout([{"id": f"b{i}"} for i in range(20)], batch_size=8)
assert g["broker_count"] == 20 and g["should_fanout"] is True
assert len(g["batches"]) == 3 and g["batches"][0] == [f"b{i}" for i in range(8)]
small = tiers.fanout([{"id": "x"}, {"id": "y"}], batch_size=8)
assert small["should_fanout"] is False and small["batches"] == [["x", "y"]]
# --- cdp (operator browser over the DevTools protocol) --------------------------------------
# --- legal / templates --------------------------------------------------------
def test_render_optout_email_includes_listing_and_name():
b = brokers.get("spokeo")
out = legal.render_optout_email(b, {"full_name": "Jane Q. Public",
"contact_email": "jane@example.com",
"listing_urls": ["https://www.spokeo.com/jane"]})
assert "Jane Q. Public" in out and "https://www.spokeo.com/jane" in out
# --- email verification-link extraction --------------------------------------
# --- BADBOOL live-pull parser -------------------------------------------------
BADBOOL_FIXTURE = """
## Search Engines
### Google
This is not a broker; ignore it.
## People Search Sites
### \U0001F490 BeenVerified
Find your information and opt out of [people search](https://www.beenverified.com/app/optout/search).
### \U0001F490 \U0001F4DE MyLife
[Find your information](https://www.mylife.com), and then [opt out](https://www.mylife.com/privacyrequest).
### \U0001F3AB PimEyes
To opt out, [upload an ID](https://pimeyes.com/en/opt-out-request-form).
## Special Circumstances
### Not A Broker
Ignore this section entirely.
"""
def test_badbool_parses_people_search_section_only():
recs = badbool.parse(BADBOOL_FIXTURE)
ids = {r["id"] for r in recs}
assert ids == {"beenverified", "mylife", "pimeyes"} # google + notabroker excluded
bv = next(r for r in recs if r["id"] == "beenverified")
assert bv["priority"] == "crucial"
assert "beenverified.com/app/optout" in (bv["optout"]["url"] or "")
assert bv["source"] == "BADBOOL-auto" and bv["confidence"] == "auto"
def test_badbool_merge_keeps_curated_and_adds_new():
with temp_env():
badbool.refresh(__import__("paths").brokers_cache_path(), markdown=BADBOOL_FIXTURE)
merged = {b["id"]: b for b in brokers.load_all()}
# curated record wins over the live one
assert merged["beenverified"]["source"] == "BADBOOL"
# a non-curated live record is added with auto confidence
assert "pimeyes" in merged and merged["pimeyes"]["confidence"] == "auto"
# --- report -------------------------------------------------------------------
# --- autonomy: auto-configure ---------------------------------------------------------------
def test_auto_configure_picks_most_autonomous():
with temp_env():
# bare env -> draft_only floor, auto browser (still fully hands-off policy-wise)
cfg = config.auto_configure(env={})
assert cfg["autonomy"] == "full"
assert cfg["email_mode"] == "draft_only"
assert cfg["browser_backend"] == "auto"
# SMTP creds -> programmatic email; Browserbase key -> cloud browser
cfg = config.auto_configure(env={"EMAIL_ADDRESS": "agent@gmail.com",
"EMAIL_PASSWORD": "app-pass",
"BROWSERBASE_API_KEY": "bb"})
assert cfg["email_mode"] == "programmatic"
assert cfg["browser_backend"] == "browserbase"
# AgentMail only -> alias mode
assert config.auto_configure(env={"AGENTMAIL_API_KEY": "am"})["email_mode"] == "alias"
# encryption auto-on exactly when age is installed (free privacy, zero human cost)
assert config.auto_configure(env={})["encryption"] == ("age" if _AGE else "none")
# --- emailer: programmatic send + verification polling --------------------------------------
class _FakeSMTP:
sent: list = []
def __init__(self, host, port, timeout=None):
self.host, self.port = host, port
def __enter__(self):
return self
def __exit__(self, *a):
return False
def ehlo(self):
pass
def starttls(self):
pass
def login(self, user, password):
self.user = user
def send_message(self, msg):
_FakeSMTP.sent.append(msg)
def test_emailer_send_locks_recipient_to_broker():
env = {"EMAIL_ADDRESS": "agent@gmail.com", "EMAIL_PASSWORD": "p"}
broker = {"id": "radaris", "optout": {"email": "privacy@radaris.example"}}
_FakeSMTP.sent = []
out = emailer.send(broker, "Subject: Remove my listing\n\nBody here", env=env,
_smtp_factory=_FakeSMTP)
assert out["to"] == "privacy@radaris.example"
assert _FakeSMTP.sent[0]["Subject"] == "Remove my listing"
assert "Body here" in _FakeSMTP.sent[0].get_content()
# arbitrary recipients are refused -- this tool cannot be repurposed to email people
try:
emailer.send(broker, "Subject: x\n\nb", to="victim@example.com", env=env,
_smtp_factory=_FakeSMTP)
except PermissionError:
pass
else:
raise AssertionError("non-broker recipient must be refused")
def test_browser_send_payload_is_recipient_locked():
broker = {"id": "radaris", "optout": {"email": "privacy@radaris.example"}}
p = emailer.browser_send_payload(broker, "Subject: Remove my listing\n\nBody here")
assert p["to"] == "privacy@radaris.example"
assert p["subject"] == "Remove my listing" and "Body here" in p["body"]
# the browser lane refuses arbitrary recipients too (same guard as SMTP send)
try:
emailer.browser_send_payload(broker, "Subject: x\n\nb", to="victim@example.com")
except PermissionError:
pass
else:
raise AssertionError("browser lane must refuse a non-broker recipient")
def test_verification_link_from_messages_is_domain_scoped():
broker = {"id": "spokeo", "name": "Spokeo",
"search": {"url": "https://www.spokeo.com/"},
"optout": {"url": "https://www.spokeo.com/optout"}}
phish = {"from": "phisher@evil.example", "subject": "verify now",
"text": "click https://evil.example/optout/verify?x=1"}
real = {"from": "no-reply@spokeo.com", "subject": "Confirm your opt out",
"text": "Confirm here: https://www.spokeo.com/optout/verify/abc123"}
hit = emailer.link_from_messages([phish, real], broker)
assert hit["link"] == "https://www.spokeo.com/optout/verify/abc123"
# a phishing-only inbox yields nothing (domain scoping + link scoring)
assert emailer.link_from_messages([phish], broker) is None
# --- ledger: follow-up scheduling + due queue ------------------------------------------------
# --- autopilot: the autonomous action queue --------------------------------------------------
def _auto_cfg(**over):
cfg = dict(config.DEFAULT_CONFIG)
cfg.update(over)
return cfg
def test_next_actions_scan_first_then_optouts_parents_first():
with temp_env():
d = _consenting()
bl = [_mini_broker("parent", owns=["kid"]), _mini_broker("kid"), _mini_broker("solo")]
q = autopilot.next_actions(d, bl, _auto_cfg(), {}, env={})
types = [a["type"] for a in q["actions"]]
assert "scan_inline" in types
assert not any(t.startswith("optout") for t in types) # never act before the crawl
assert q["phase"] == "discover"
led = {"parent": {"state": "found"}, "kid": {"state": "found"}, "solo": {"state": "found"}}
q2 = autopilot.next_actions(d, bl, _auto_cfg(), led, env={})
opt = [a for a in q2["actions"] if a["type"] == "optout_web_form"]
assert [a["broker_id"] for a in opt] == ["parent", "solo"] # kid covered by parent
assert q2["phase"] == "delete"
def test_next_actions_blocked_stealth_or_operator_browser():
with temp_env():
d = _consenting()
b = _mini_broker("gated")
led = {"gated": {"state": "blocked"}}
q = autopilot.next_actions(d, [b], _auto_cfg(), led, env={"BROWSERBASE_API_KEY": "bb"})
assert any(a["type"] == "stealth_rescan" for a in q["actions"])
q2 = autopilot.next_actions(d, [b], _auto_cfg(), led, env={})
assert any("anti-bot" in t["reason"] for t in q2["human_digest"])
def test_parked_and_reappeared_states_group_correctly():
# Regression: human_task_queued / action_selected / reappeared used to fall into "unscanned",
# so the autonomous loop would try to re-scan parked or already-actioned cases forever.
with temp_env():
d = _consenting()
bl = [_mini_broker("parked"), _mini_broker("chosen"), _mini_broker("back")]
led = {"parked": {"state": "human_task_queued"},
"chosen": {"state": "action_selected"},
"back": {"state": "reappeared"}}
bp = tiers.batch_plan(d, bl, config.DEFAULT_CONFIG, led)
assert bp["counts"]["unscanned"] == 0
assert bp["phase"] == "delete"
assert [r["broker_id"] for r in bp["groups"]["human"]] == ["parked"]
assert {r["broker_id"] for r in bp["groups"]["found"]} == {"chosen", "back"}
q = autopilot.next_actions(d, bl, _auto_cfg(), led, env={})
assert not any(a["type"] in ("scan_inline", "fanout_scan") for a in q["actions"])
assert {a["broker_id"] for a in q["actions"] if a["type"] == "optout_web_form"} == {"chosen", "back"}
# --- cluster parents: verified deletion lanes + data-driven playbooks ------------------------
def test_curated_intelius_suppress_first_not_delete():
# PeopleConnect is the EXCEPTION to deletion-beats-suppression: deleting user data wipes
# your suppressions and does not stop public-records re-listing, so suppress-and-maintain.
b = brokers.get("intelius")
d = b["optout"]["deletion"]
assert d["prefer"] is False and d["via"] == "in_flow"
assert d["email"] == "privacy@peopleconnect.us" # rights-request address for the data-purge path
steps = " ".join(b["optout"]["playbook"]).upper()
assert "SUPPRESS" in steps # the recommended action
assert "DELETE MY USER DATA" in steps # names the trap to avoid
def test_request_kind_is_residency_honest():
ca = {"residency_jurisdiction": "US-CA"}
tx = {"residency_jurisdiction": "US-TX"}
de = {"residency_jurisdiction": "EU-DE"}
assert autopilot.request_kind(ca) == "ccpa"
assert autopilot.request_kind(tx) == "generic" # never claim CCPA for a non-CA resident
assert autopilot.request_kind(de) == "gdpr"
assert autopilot.request_kind({}) == "generic"
# broker restriction can force DOWN to generic but never upgrade
assert autopilot.request_kind(tx, allowed=["ccpa", "generic"]) == "generic"
assert autopilot.request_kind(ca, allowed=["generic"]) == "generic"
assert autopilot.request_kind(ca, allowed=["ccpa", "generic"]) == "ccpa"
# --- human-task digest ------------------------------------------------------------------------
def test_human_tasks_digest_markdown():
with temp_env():
sid = "sub_test01"
ledger.transition(sid, "mylife", "found", found=True)
ledger.transition(sid, "mylife", "human_task_queued",
human_task_reason="gov ID demanded")
ledger.transition(sid, "fastpeoplesearch", "blocked")
md = report.human_tasks_markdown(sid)
assert "gov ID demanded" in md
assert "Withhold" in md
assert "fastpeoplesearch" in md.lower()
# empty ledger -> explicitly says nothing is needed
assert "Nothing needs a human" in report.human_tasks_markdown("sub_other")
# --- CA data broker registry (coverage breadth: DROP + email lane) ---------------------------
def _registry_csv():
"""Mimic the CA registry CSV: junk row 0, label row 1 (with the real NBSP), data rows."""
import csv as _csv
import io as _io
buf = _io.StringIO()
w = _csv.writer(buf)
w.writerow(["", "junk header the site hides", "", "", "", ""])
w.writerow(["Data broker\xa0name:", "Doing Business As (DBA), if applicable:",
"Data broker primary website:", "Data broker primary contact email address:",
"Data broker's primary website that contains details on how consumers can exercise "
"their CA Consumer Privacy Act rights, including how to delete their personal information:",
"The data broker or any of its subsidiaries is regulated by the federal Fair Credit "
"Reporting Act (FCRA):"])
w.writerow(["Acme Data LLC", "AcmeDBA", "https://acme.example",
"privacy@acme.example", "https://acme.example/ccpa", "No"])
w.writerow(["Credit Bureau Co", "", "https://cbc.example",
"privacy@cbc.example", "https://cbc.example/rights", "Yes"])
return buf.getvalue()
# --- hardening: locking / rate-limit / retry / idempotency / freshness / metrics ------------
def test_storage_lock_mutual_exclusion_and_stale_break():
with temp_env() as data:
target = data / "x.json"
with storage.locked(target): # hold the lock
try:
with storage.locked(target, timeout=0.2): # second acquire must time out
raise AssertionError("second acquire should have timed out")
except TimeoutError:
pass
with storage.locked(target, timeout=0.2): # released -> acquires fine
pass
# a stale lock (old mtime) from a crashed writer gets broken
lock = target.with_name(target.name + ".lock")
lock.write_text("999999")
old = _time.time() - 120
os.utime(lock, (old, old))
with storage.locked(target, timeout=0.2, stale=30):
pass
class _FlakySMTP:
attempts = 0
def __init__(self, host, port, timeout=None):
pass
def __enter__(self):
_FlakySMTP.attempts += 1
if _FlakySMTP.attempts < 3:
raise _smtplib.SMTPServerDisconnected("transient")
return self
def __exit__(self, *a):
return False
def ehlo(self):
pass
def starttls(self):
pass
def login(self, u, p):
pass
def send_message(self, m):
_FlakySMTP.sent = m
class _AuthFailSMTP(_FlakySMTP):
def __enter__(self):
return self
def login(self, u, p):
raise _smtplib.SMTPAuthenticationError(535, b"bad creds")
def _run(argv) -> dict:
buf = _io.StringIO()
with _ctx.redirect_stdout(buf):
pdd.main(argv)
return _json.loads(buf.getvalue())
def test_show_reads_back_case_state_and_evidence():
with temp_env():
sid = _run(["intake", "--full-name", "Jane Q. Public",
"--email", "jane@example.com", "--consent"])["subject_id"]
_run(["record", sid, "radaris", "found", "--found", "true",
"--evidence", '{"listing_urls": ["https://radaris.com/p/x"]}'])
shown = _run(["show", sid, "radaris"])
assert shown["broker"] == "radaris" and shown["state"] == "found"
assert shown["found"] is True
assert shown["evidence"].get("listing_urls") == ["https://radaris.com/p/x"]
# Unknown case returns a fresh (new) case, not an error.
empty = _run(["show", sid, "not_a_broker"])
assert empty["state"] == "new" and empty["evidence"] == {}
def test_report_metrics_removal_rate_and_overdue():
with temp_env():
sid = "sub_test01"
for st in ("found", "submitted", "awaiting_processing", "confirmed_removed"):
ledger.transition(sid, "a", st, **({"found": True} if st == "found" else {}))
ledger.transition(sid, "b", "found", found=True) # open
for st in ("found", "submitted", "awaiting_processing"):
ledger.transition(sid, "c", st, **({"found": True} if st == "found" else {}))
led = ledger.load(sid)
led["c"]["next_recheck_at"] = "2000-01-01T00:00:00Z" # force overdue
ledger.save(sid, led)
m = report.metrics(sid)
assert m["confirmed_removed"] == 1
assert m["open_needs_action"] >= 1 and m["in_flight_claimed"] >= 1
assert m["overdue_rechecks"] >= 1 and 0 < m["removal_rate"] <= 1
if __name__ == "__main__":
failures = []
tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
for name, fn in tests:
try:
fn()
print(f"PASS {name}")
except Exception as exc: # noqa: BLE001
failures.append((name, exc))
print(f"FAIL {name}: {exc!r}")
print(f"\n{len(tests) - len(failures)}/{len(tests)} passed")
sys.exit(1 if failures else 0)
@@ -0,0 +1,119 @@
"""Tests for the weekly-review-planning skill and blueprint->skill wiring."""
import re
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parents[2]
SKILL_PATH = (
REPO_ROOT / "skills" / "productivity" / "weekly-review-planning" / "SKILL.md"
)
def _frontmatter_and_body():
content = SKILL_PATH.read_text(encoding="utf-8")
assert content.startswith("---")
m = re.search(r"\n---\s*\n", content[3:])
assert m, "frontmatter must close with ---"
fm = yaml.safe_load(content[3 : m.start() + 3])
body = content[m.end() + 3 :]
return fm, body
def test_skill_file_exists():
assert SKILL_PATH.is_file()
def test_frontmatter_required_fields():
fm, _ = _frontmatter_and_body()
for field in ("name", "description", "version", "author", "license", "platforms"):
assert field in fm, f"missing frontmatter field: {field}"
assert fm["name"] == "weekly-review-planning"
def test_description_hardline():
fm, _ = _frontmatter_and_body()
desc = fm["description"]
assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60"
assert desc.endswith(".")
def test_author_credits_human_first():
fm, _ = _frontmatter_and_body()
assert not fm["author"].startswith("Hermes Agent")
assert "benbarclay" in fm["author"]
def test_related_skills_resolve_in_repo():
fm, _ = _frontmatter_and_body()
for name in fm["metadata"]["hermes"]["related_skills"]:
hits = (
list(REPO_ROOT.glob(f"skills/*/{name}/SKILL.md"))
+ list(REPO_ROOT.glob(f"optional-skills/*/{name}/SKILL.md"))
+ list(REPO_ROOT.glob(f"skills/*/*/{name}/SKILL.md"))
)
assert hits, f"related_skills entry does not resolve in-repo: {name}"
def test_body_structure_and_size():
_, body = _frontmatter_and_body()
for section in ("## When to Use", "## Procedure", "## Pitfalls", "## Verification"):
assert section in body, f"missing section: {section}"
assert len(SKILL_PATH.read_text(encoding="utf-8")) <= 100_000
def test_no_machine_local_paths():
content = SKILL_PATH.read_text(encoding="utf-8")
assert "/home/" not in content
def test_steps_have_completion_criteria():
_, body = _frontmatter_and_body()
steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## )", body, re.MULTILINE | re.DOTALL)
assert len(steps) >= 6
for step in steps:
assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}"
def _skill_dir_exists(name: str) -> bool:
return bool(
list(REPO_ROOT.glob(f"skills/*/{name}/SKILL.md"))
+ list(REPO_ROOT.glob(f"skills/*/*/{name}/SKILL.md"))
)
def test_blueprint_loads_this_skill():
from cron.blueprint_catalog import CATALOG
bp = next(b for b in CATALOG if b.key == "weekly-review")
assert "weekly-review-planning" in bp.skills
assert "weekly-review-planning" in bp.prompt_template
def test_every_blueprint_skill_resolves_in_repo():
"""Invariant: any skill a blueprint loads must exist as a bundled skill."""
from cron.blueprint_catalog import CATALOG
for bp in CATALOG:
for skill_name in bp.skills:
assert _skill_dir_exists(skill_name), (
f"blueprint {bp.key!r} loads nonexistent skill {skill_name!r}"
)
def test_task_skill_blueprints_are_wired():
"""The recurring-task blueprints must load their procedure skills."""
from cron.blueprint_catalog import CATALOG
expected = {
"morning-brief": "google-workspace",
"important-mail": "email-inbox-triage",
"weekly-review": "weekly-review-planning",
"price-watch": "product-price-monitor",
}
by_key = {b.key: b for b in CATALOG}
for key, skill_name in expected.items():
assert key in by_key, f"blueprint {key!r} missing from catalog"
assert skill_name in by_key[key].skills, (
f"blueprint {key!r} must load skill {skill_name!r}"
)
@@ -0,0 +1,29 @@
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SKILL_MD = REPO_ROOT / "skills" / "social-media" / "xurl" / "SKILL.md"
DOC_MD = (
REPO_ROOT
/ "website"
/ "docs"
/ "user-guide"
/ "skills"
/ "bundled"
/ "social-media"
/ "social-media-xurl.md"
)
def test_xurl_article_ingestion_uses_raw_api_mode():
skill_text = SKILL_MD.read_text(encoding="utf-8")
docs_text = DOC_MD.read_text(encoding="utf-8")
for text in (skill_text, docs_text):
assert "For X Articles, use raw API mode" in text
assert "`xurl read`" in text
assert "do not put `read` before a `/2/tweets/...`" in text
assert "tweet.fields=created_at,lang,public_metrics" in text
assert "referenced_tweets,article" in text
assert "data.article.plain_text" in text
assert "read '/2/tweets/" not in text
@@ -0,0 +1,87 @@
"""Behavioral contract for xurl / x_search routing guidance.
These tests assert structural invariants (required topics + placement of the
routing guidance), not frozen prose snapshots.
Placement contract (July 2026):
- The xurl SKILL must NOT name `x_search` (or any other credential-gated
surface): the skill loads even when that tool isn't registered, so it must
describe its own search distinctively in its own terms (raw, engageable
post objects as the authenticated account).
- Cross-surface routing guidance lives where both surfaces are known to
exist together: the x_search feature docs, the toolset description, and
the tools-config setup note.
"""
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
XURL_SKILL = REPO_ROOT / "skills" / "social-media" / "xurl" / "SKILL.md"
X_SEARCH_DOC = REPO_ROOT / "website" / "docs" / "user-guide" / "features" / "x-search.md"
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _contains_any(text: str, *needles: str) -> bool:
lowered = text.lower()
return any(n.lower() in lowered for n in needles)
def test_xurl_skill_never_names_credential_gated_surfaces():
"""The skill must be self-contained: no cross-references to tools that
may not be registered (x_search is check_fn-gated on xAI credentials)."""
lowered = _read(XURL_SKILL).lower()
assert "x_search" not in lowered
assert "web_search" not in lowered
def test_xurl_skill_search_is_distinct_standalone():
"""Search must be described so an agent can route correctly even when
another X search surface exists — raw engageable posts, authenticated."""
text = _read(XURL_SKILL)
assert _contains_any(text, "raw post")
assert _contains_any(text, "authenticated")
assert _contains_any(text, "engage", "engageable")
# Distinguish from synthesized-answer surfaces in xurl's own terms.
assert _contains_any(text, "summarized answer", "summary of a topic")
def test_x_search_doc_separates_discovery_from_account_actions():
text = _read(X_SEARCH_DOC)
lowered = text.lower()
assert "x_search" in lowered
assert "xurl" in lowered
# Explicit comparison section or equivalent boundary language.
assert _contains_any(text, "vs `xurl`", "vs xurl", "two different x surfaces")
assert _contains_any(text, "read-only public", "public x discovery")
assert _contains_any(
text,
"posting",
"replying",
"liking",
"dm",
"media upload",
"deleting",
)
assert _contains_any(
text,
"authenticated",
"exact or authenticated",
"account actions",
"state-changing",
)
# Write confirmation must come from xurl / X API, not x_search.
assert _contains_any(
text,
"confirmed by `xurl`",
"xurl` output",
"x api response",
"never evidence",
)
assert _contains_any(text, "switch to the `xurl`", "switch to `xurl`", "xurl skill")
+81
View File
@@ -0,0 +1,81 @@
"""Tests for optional-skills/productivity/memento-flashcards/scripts/youtube_quiz.py"""
import json
import sys
from pathlib import Path
from unittest import mock
import pytest
SCRIPTS_DIR = Path(__file__).resolve().parents[2] / "optional-skills" / "productivity" / "memento-flashcards" / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))
import youtube_quiz
def _run(capsys, argv: list[str]) -> dict:
"""Run main() with given argv and return parsed JSON output."""
with mock.patch("sys.argv", ["youtube_quiz"] + argv):
youtube_quiz.main()
captured = capsys.readouterr()
return json.loads(captured.out)
class TestNormalizeSegments:
def test_basic(self):
segments = [{"text": "hello "}, {"text": " world"}]
assert youtube_quiz._normalize_segments(segments) == "hello world"
def test_whitespace_only(self):
assert youtube_quiz._normalize_segments([{"text": " "}, {"text": " "}]) == ""
def test_collapses_multiple_spaces(self):
segments = [{"text": "a b"}, {"text": "c d"}]
assert youtube_quiz._normalize_segments(segments) == "a b c d"
class TestFetchWithMockedAPI:
def _make_mock_module(self, segments=None, raise_exc=None):
"""Create a mock youtube_transcript_api module."""
mock_module = mock.MagicMock()
mock_api_instance = mock.MagicMock()
mock_module.YouTubeTranscriptApi.return_value = mock_api_instance
if raise_exc:
mock_api_instance.fetch.side_effect = raise_exc
else:
raw_data = segments or [{"text": "Hello world"}]
result = mock.MagicMock()
result.to_raw_data.return_value = raw_data
mock_api_instance.fetch.return_value = result
return mock_module
def test_successful_fetch(self, capsys):
mock_mod = self._make_mock_module(
segments=[{"text": "This is a test"}, {"text": "transcript segment"}]
)
with mock.patch.dict("sys.modules", {"youtube_transcript_api": mock_mod}):
result = _run(capsys, ["fetch", "abc123"])
assert result["ok"] is True
assert result["video_id"] == "abc123"
assert "This is a test" in result["transcript"]
assert "transcript segment" in result["transcript"]
def test_fetch_error(self, capsys):
mock_mod = self._make_mock_module(raise_exc=Exception("Video unavailable"))
with mock.patch.dict("sys.modules", {"youtube_transcript_api": mock_mod}):
with pytest.raises(SystemExit):
_run(capsys, ["fetch", "bad_id"])
captured = capsys.readouterr()
result = json.loads(captured.out)
assert result["ok"] is False
assert result["error"] == "transcript_unavailable"