"""Platforms without a database (milestone 463 step 2). Detection is pure — a catalog and a list of repo paths in, platform ids out — so it is pinned here against fixture trees. What it writes, and what it must never overwrite, is in tests/test_integration_platforms.py. """ from __future__ import annotations from types import SimpleNamespace from scribe.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS from scribe.mcp.tools.projects import _inception_choices from scribe.services.coverage import scan_archive from tests.helpers import forge_tarball from scribe.services.platforms import ( MEMBER_STATES, SETTABLE_STATES, detect, marker_matches, members, validate_markers, validate_updates, ) def _platform(pid: int, *markers: str) -> SimpleNamespace: return SimpleNamespace(id=pid, markers=list(markers)) # --- a single marker ---------------------------------------------------------- def test_a_bare_marker_matches_the_basename_anywhere_in_the_tree(): assert marker_matches("go.mod", "go.mod") assert marker_matches("go.mod", "services/api/go.mod") assert marker_matches("AndroidManifest.xml", "app/src/main/AndroidManifest.xml") assert marker_matches("vite.config.*", "frontend/vite.config.ts") assert not marker_matches("go.mod", "go.mod.bak") assert not marker_matches("go.mod", "docs/go.mod.md") def test_a_marker_with_a_slash_matches_the_whole_path_only(): """A directory-shaped marker cannot be satisfied by a same-named file somewhere else — `.github/workflows/*` is about the repo root's CI, not a vendored copy of some other project's.""" assert marker_matches(".github/workflows/*", ".github/workflows/ci.yml") assert not marker_matches(".github/workflows/*", "vendor/x/.github/workflows/ci.yml") assert not marker_matches(".github/workflows/*", "ci.yml") def test_a_blank_marker_matches_nothing(): assert not marker_matches("", "anything") assert not marker_matches(" ", "anything") # --- detection over a tree ------------------------------------------------------- ANDROID_AND_GO = [ "app/build.gradle.kts", "app/src/main/AndroidManifest.xml", "server/go.mod", "server/main.go", "README.md", ] def test_detect_finds_every_platform_a_tree_carries_and_nothing_else(): catalog = [ _platform(1, "AndroidManifest.xml"), _platform(2, "go.mod"), _platform(3, "Cargo.toml"), _platform(4, ".github/workflows/*"), ] assert detect(catalog, ANDROID_AND_GO) == [1, 2] def test_a_platform_with_no_markers_is_declare_only(): """An empty marker list is how a platform says "a person must tell you" — it must not match everything, and it must not match nothing by error.""" catalog = [_platform(1), _platform(2, "go.mod")] assert detect(catalog, ANDROID_AND_GO) == [2] assert detect([SimpleNamespace(id=9, markers=None)], ANDROID_AND_GO) == [] def test_detect_on_an_empty_tree_finds_nothing(): assert detect([_platform(1, "go.mod")], []) == [] def test_the_archive_scan_carries_every_path_not_only_the_scannable_ones(): """The markers that say what a project IS are mostly files the shape scan skips — a manifest, a go.mod, a gradle script. Detection reads the scan's paths, so a scan that listed only code files would detect nothing.""" scan = scan_archive(forge_tarball({ "app/src/main/AndroidManifest.xml": b"", "server/go.mod": b"module x\n", "server/main.py": b"def main():\n pass\n", })) assert set(scan.paths) == { "app/src/main/AndroidManifest.xml", "server/go.mod", "server/main.py", } # The forge's wrapping directory is stripped, as it is for shapes. assert not any(p.startswith("widget/") for p in scan.paths) # --- the answers a person may give ------------------------------------------------- def test_detected_is_the_refreshs_to_write_never_a_persons(): assert "detected" in MEMBER_STATES assert "detected" not in SETTABLE_STATES assert "rejected" not in MEMBER_STATES def test_validate_updates(): assert validate_updates({"go": "declared", "rust": "rejected", "python": None}) is None assert validate_updates({}) is None assert "slug: state" in validate_updates(["go"]) assert "not a state a person sets" in validate_updates({"go": "detected"}) assert "not a state a person sets" in validate_updates({"go": "maybe"}) assert "named by its slug" in validate_updates({"": "declared"}) def test_validate_markers(): assert validate_markers(None) is None assert validate_markers(["go.mod", ".github/workflows/*"]) is None assert "list of glob patterns" in validate_markers("go.mod") assert "list of glob patterns" in validate_markers([1]) assert "repo-relative" in validate_markers(["/etc/passwd"]) def test_members_drops_the_rejected_answers(): rows = [ {"id": 1, "slug": "go", "name": "Go", "state": "declared"}, {"id": 2, "slug": "rust", "name": "Rust", "state": "rejected"}, {"id": 3, "slug": "python", "name": "Python", "state": "detected"}, ] assert [m["slug"] for m in members(rows)] == ["go", "python"] # --- the doors ------------------------------------------------------------------------ def test_the_platform_tools_are_classified(): assert "list_platforms" in _READ_ONLY_TOOLS assert "set_project_platforms" in _WRITE_TOOLS def test_inception_choices_carry_platforms_only_when_given(): """Left out, platforms stays UNSTATED (None after normalising) — which is not the same answer as an empty list, "none of them".""" assert "platforms" not in _inception_choices(0, True) assert _inception_choices(0, True, ["go"])["platforms"] == ["go"] assert _inception_choices(0, True, [])["platforms"] == []