skill: add portable read-only learning-data readers

This commit is contained in:
amosblomqvist
2026-10-16 21:24:27 +03:00
parent b3c9f9e18b
commit c0ad63a940
11 changed files with 2957 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
# LearnHarness data skill tests
Run independently of the application, with Python 3.10+ and no dependencies:
```sh
python3 -B -m unittest discover -s tests/skills -v
```
These tests exercise the self-contained readers in `skills/learnharness/` against
small synthetic records and the native golden data in `tests/fixtures/golden-data/`.
They check preservation of learning evidence, graph selection/merging, unsupported
formats, path boundaries, and unchanged file bytes/mtimes. A portability test copies
the skill and fixture data outside the repository and invokes both output formats
from an unrelated working directory, checking that no cache or data files appear.
No real installed-user profile, provider keys, app server, LLM, or network is used.
The test command is intentionally separate from the app's Node test suite; the skill
has no app-runtime, release, or website integration.
+169
View File
@@ -0,0 +1,169 @@
"""Filesystem boundary and portability tests for the standalone skill."""
from __future__ import annotations
import argparse
import contextlib
import io
import json
import os
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
sys.dont_write_bytecode = True
SCRIPTS = Path(__file__).resolve().parents[2] / "skills" / "learnharness" / "scripts"
sys.path.insert(0, str(SCRIPTS))
import _data
class DataHelpersTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.base = Path(self.temp.name)
self.root = self.base / "data"
self.root.mkdir()
def tearDown(self):
self.temp.cleanup()
def record(self, **changes):
value = {
"id": "chat_one", "title": "A lesson", "createdAt": "2026-10-01T00:00:00Z",
"updatedAt": "2026-10-02T00:00:00Z", "messages": [],
}
value.update(changes)
return value
def save_chat(self, **changes):
(self.root / "chats").mkdir(exist_ok=True)
(self.root / "chats" / "chat_one.json").write_text(json.dumps(self.record(**changes)), encoding="utf-8")
def test_explicit_existing_root(self):
self.assertEqual(_data.data_root(str(self.root)), self.root.resolve())
def test_missing_root_is_not_created(self):
target = self.base / "not-created"
with self.assertRaisesRegex(_data.DataError, "nothing was created"):
_data.data_root(str(target))
self.assertFalse(target.exists())
def test_mac_standard_root(self):
expected = self.base / "Library" / "Application Support" / "LearnHarness" / "data"
expected.mkdir(parents=True)
with patch.object(_data.sys, "platform", "darwin"), patch.object(Path, "home", return_value=self.base):
self.assertEqual(_data.data_root(), expected.resolve())
def test_windows_standard_root(self):
expected = self.base / "roaming" / "LearnHarness" / "data"
expected.mkdir(parents=True)
with patch.object(_data.sys, "platform", "win32"), patch.dict(os.environ, {"APPDATA": str(self.base / "roaming")}):
self.assertEqual(_data.data_root(), expected.resolve())
def test_windows_missing_appdata_requires_explicit_root(self):
with patch.object(_data.sys, "platform", "win32"), patch.dict(os.environ, {}, clear=True):
with self.assertRaisesRegex(_data.DataError, "--data-dir"):
_data.data_root()
def test_linux_xdg_and_fallback(self):
xdg = self.base / "config"
expected = xdg / "LearnHarness" / "data"
expected.mkdir(parents=True)
with patch.object(_data.sys, "platform", "linux"), patch.dict(os.environ, {"XDG_CONFIG_HOME": str(xdg)}):
self.assertEqual(_data.data_root(), expected.resolve())
fallback = self.base / ".config" / "LearnHarness" / "data"
fallback.mkdir(parents=True)
with patch.object(_data.sys, "platform", "linux"), patch.object(Path, "home", return_value=self.base), patch.dict(os.environ, {}, clear=True):
self.assertEqual(_data.data_root(), fallback.resolve())
def test_relative_xdg_is_explicit_error(self):
with patch.object(_data.sys, "platform", "linux"), patch.dict(os.environ, {"XDG_CONFIG_HOME": "relative"}):
with self.assertRaisesRegex(_data.DataError, "relative"):
_data.data_root()
def test_explicit_root_works_on_unknown_os(self):
with patch.object(_data.sys, "platform", "unknown"):
self.assertEqual(_data.data_root(str(self.root)), self.root.resolve())
with self.assertRaisesRegex(_data.DataError, "--data-dir"):
_data.data_root()
def test_safe_path_rejects_traversal_and_absolute_paths(self):
self.assertEqual(_data.safe_path(self.root, "chats/chat_one.json"), self.root.resolve() / "chats" / "chat_one.json")
for relative in ("../auth.json", self.base / "outside.json"):
with self.subTest(path=relative), self.assertRaises(_data.DataError):
_data.safe_path(self.root, relative)
def test_safe_path_rejects_escaping_symlink(self):
outside = self.base / "private.json"
outside.write_text('{"unrelated": true}', encoding="utf-8")
try:
(self.root / "link.json").symlink_to(outside)
except (OSError, NotImplementedError):
self.skipTest("Symlinks unavailable for this user")
with self.assertRaisesRegex(_data.DataError, "outside"):
_data.read_json(self.root, "link.json")
def test_json_read_missing_and_malformed_without_writes(self):
self.assertEqual(_data.read_json(self.root, "optional.json", missing=[]), [])
target = self.root / "bad.json"
target.write_text("{", encoding="utf-8")
with self.assertRaisesRegex(_data.DataError, "Cannot read"):
_data.read_json(self.root, "bad.json")
self.assertEqual(target.read_text(encoding="utf-8"), "{")
self.assertEqual([p.name for p in self.root.iterdir()], ["bad.json"])
def test_non_json_constants_are_not_accepted(self):
(self.root / "bad.json").write_text('{"value": NaN}', encoding="utf-8")
with self.assertRaises(_data.DataError):
_data.read_json(self.root, "bad.json")
def test_unstamped_native_chat_is_read_without_stamping(self):
self.save_chat()
target = self.root / "chats" / "chat_one.json"
before = target.read_bytes()
self.assertEqual(_data.read_chat(self.root, "chat_one"), self.record())
self.assertEqual(target.read_bytes(), before)
self.assertEqual(sorted(p.relative_to(self.root).as_posix() for p in self.root.rglob("*")), ["chats", "chats/chat_one.json"])
def test_future_chat_version_is_refused(self):
self.save_chat(version=2)
with self.assertRaisesRegex(_data.DataError, "Update the skill"):
_data.read_chat(self.root, "chat_one")
self.save_chat(version=True)
with self.assertRaisesRegex(_data.DataError, "invalid version"):
_data.read_chat(self.root, "chat_one")
def test_pre_native_parts_dialect_is_not_silently_empty(self):
self.save_chat(messages=[{"role": "user", "parts": [{"type": "text", "text": "old shape"}]}])
with self.assertRaisesRegex(_data.DataError, "older UIMessage"):
_data.read_chat(self.root, "chat_one")
def test_read_chat_rejects_missing_and_mismatched_records(self):
with self.assertRaisesRegex(_data.DataError, "not found"):
_data.read_chat(self.root, "chat_one")
self.save_chat(id="chat_other")
with self.assertRaisesRegex(_data.DataError, "mismatch"):
_data.read_chat(self.root, "chat_one")
def test_read_chat_rejects_path_in_place_of_id(self):
for value in ("../auth", "..", "", "chats/chat_one", "chats\\chat_one"):
with self.subTest(value=value), self.assertRaises(_data.DataError):
_data.read_chat(self.root, value)
def test_common_flags_and_unicode_json(self):
parser = argparse.ArgumentParser()
_data.add_common_arguments(parser)
args = parser.parse_args(["--data-dir", str(self.root), "--format", "json"])
self.assertEqual(args.data_dir, str(self.root))
self.assertEqual(args.format, "json")
out = io.StringIO()
with contextlib.redirect_stdout(out):
_data.print_json({"statement": "∇ and café"})
self.assertIn("∇", out.getvalue())
self.assertEqual(json.loads(out.getvalue()), {"statement": "∇ and café"})
if __name__ == "__main__":
unittest.main()
+351
View File
@@ -0,0 +1,351 @@
"""Portable, read-only graph CLI tests; no application-store imports."""
import contextlib
import io
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
sys.dont_write_bytecode = True
SCRIPTS = Path(__file__).resolve().parents[2] / "skills" / "learnharness" / "scripts"
sys.path.insert(0, str(SCRIPTS))
import graph
GOLDEN = Path(__file__).resolve().parents[1] / "fixtures" / "golden-data"
def snapshot(root):
"""Notice both changed bytes/mtimes and newly created files/directories."""
return {
str(path.relative_to(root)): (path.read_bytes(), path.stat().st_mtime_ns) if path.is_file() else None
for path in root.rglob("*")
}
def point(chat, message=""):
return {"chatId": chat, "messageId": message}
def node(identifier, session=None, **updates):
value = {
"id": identifier, "name": f"Name {identifier}",
"statement": f"Full statement {identifier}", "aliases": [],
"createdAt": "2026-01-01T00:00:00Z",
"provenance": [] if session is None else [point(session)],
}
value.update(updates)
return value
def chain(identifier, premises, conclusion, session, **updates):
value = {
"id": identifier, "premises": premises, "conclusion": conclusion,
"createdAt": "2026-01-02T00:00:00Z", "provenance": [point(session)],
}
value.update(updates)
return value
class GraphTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
def write_json(self, relative, value):
path = self.root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, ensure_ascii=False), encoding="utf-8")
def write_ledger(self, name, records):
path = self.root / "graph" / f"{name}.jsonl"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("".join(json.dumps(record, ensure_ascii=False) + "\n" for record in records), encoding="utf-8")
def invoke(self, *arguments, format="json", root=None):
out, err = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
status = graph.main(["--data-dir", str(root or self.root), "--format", format, *arguments])
return status, out.getvalue(), err.getvalue()
def result(self, *arguments, root=None):
status, output, diagnostics = self.invoke(*arguments, root=root)
self.assertEqual(status, 0, diagnostics)
self.assertEqual(diagnostics, "")
payload = json.loads(output)
self.assertEqual(payload["data_dir"], str((root or self.root).resolve()))
return payload["records"]
def fixture(self):
self.write_ledger("nodes", [
node("n_b", "chat_old", name="Identity stands", statement="First precise statement", aliases=["Original wording"], provenance=[point("chat_old", "m1")], extra="preserved"),
node("n_a", "src_notes", name="Café", statement="A separate proposition.", aliases=["Literal [brackets]?", "two\nlines"]),
node("n_b", "chat_new", name="Name must not replace", statement="Later restatement", aliases=["Original wording", "Another phrasing", ""], createdAt="2099-01-01", provenance=[point("chat_old", "m1"), point("chat_new", "m2"), point("chat_new", "m2")]),
node("n_empty"),
node("n_surviving", "chat_new"),
node("n_unknown", "missing_record"),
])
self.write_ledger("chains", [
chain("arg_first", ["n_b", "n_a"], "n_surviving", "chat_old", extra="preserved"),
chain("arg_other_id", ["n_a", "n_b"], "n_surviving", "chat_new", createdAt="2099-01-01"),
chain("arg_single", ["n_surviving"], "n_a", "src_notes"),
chain("arg_dangling", ["n_missing"], "n_a", "chat_old"),
])
for identifier, title, updated in [
("chat_old", "Old conversation", "2026-02-01T00:00:00Z"),
("chat_new", "Recent conversation", "2026-03-01T00:00:00Z"),
("chat_without_nodes", "Not a birth session", "2099-01-01"),
]:
self.write_json(f"chats/{identifier}.json", {
"id": identifier, "title": title, "createdAt": "2026-01-01T00:00:00Z",
"updatedAt": updated, "messages": [],
})
self.write_json("graph/sources.json", {
"src_notes": {"id": "src_notes", "filePath": "/never/open/these notes.md", "importedAt": "2026-02-15T00:00:00Z"},
})
def test_merge_nodes_first_identity_and_union(self):
self.fixture()
before = snapshot(self.root)
merged = self.result("propositions")
self.assertEqual([item["id"] for item in merged], ["n_a", "n_b", "n_empty", "n_surviving", "n_unknown"])
value = next(item for item in merged if item["id"] == "n_b")
self.assertEqual(value["name"], "Identity stands")
self.assertEqual(value["statement"], "First precise statement")
self.assertEqual(value["createdAt"], "2026-01-01T00:00:00Z")
self.assertEqual(value["aliases"], ["Original wording", "Later restatement", "Another phrasing"])
self.assertEqual(value["provenance"], [point("chat_old", "m1"), point("chat_new", "m2")])
self.assertEqual(value["extra"], "preserved")
self.assertEqual(snapshot(self.root), before)
def test_chain_key_identity_provenance_and_endpoint_filter(self):
self.fixture()
merged = self.result("chains")
self.assertEqual([item["id"] for item in merged], ["arg_first", "arg_single", "arg_dangling"])
self.assertEqual(merged[0]["premises"], ["n_b", "n_a"])
self.assertEqual(merged[0]["createdAt"], "2026-01-02T00:00:00Z")
self.assertEqual(merged[0]["extra"], "preserved")
self.assertEqual(merged[0]["provenance"], [point("chat_old"), point("chat_new")])
self.assertEqual(self.result("chains", "--node", "n_b"), merged[:1])
self.assertEqual(len(self.result("chains", "--node", "n_a")), 3)
self.assertEqual(self.result("chains", "--node", "no-such-node"), [])
def test_birth_index_metadata_and_filter(self):
self.fixture()
sessions = self.result("sessions")
self.assertEqual([item["id"] for item in sessions], ["chat_new", "src_notes", "chat_old", "missing_record"])
self.assertEqual([item["propositions"] for item in sessions], [1, 1, 1, 1])
self.assertEqual(sessions[0]["kind"], "chat")
self.assertEqual(sessions[0]["name"], "Recent conversation")
self.assertEqual(sessions[1]["name"], "/never/open/these notes.md")
self.assertEqual(sessions[-1], {"id": "missing_record", "name": "missing_record", "kind": "file", "createdAt": None, "updatedAt": None, "propositions": 1})
self.assertEqual([item["id"] for item in self.result("propositions", "--session", "chat_new")], ["n_surviving"])
self.assertEqual([item["id"] for item in self.result("propositions", "--session", "chat_old")], ["n_b"])
self.assertEqual(self.result("propositions", "--session", "nonexistent"), [])
def test_bad_chat_metadata_is_isolated_and_diagnosed(self):
self.fixture()
corrupted = self.root / "chats" / "chat_old.json"
corrupted.write_text("{torn", encoding="utf-8")
self.write_json("chats/chat_future.json", {
"id": "chat_future", "title": "Future metadata", "createdAt": "2026-01-01",
"updatedAt": "2026-01-01", "version": 2, "messages": [],
})
self.write_json("chats/src_notes.json", {
"id": "src_notes", "title": "Older parts metadata", "createdAt": "2026-01-01",
"updatedAt": "2026-01-01", "messages": [{"role": "assistant", "parts": []}],
})
self.write_json("chats/chat_mismatch.json", {
"id": "another_id", "title": "Wrong filename", "createdAt": "2026-01-01",
"updatedAt": "2026-01-01", "messages": [],
})
before = snapshot(self.root)
for arguments, format in [(("sessions",), "json"), (("propositions",), "markdown")]:
with self.subTest(arguments=arguments):
status, output, diagnostics = self.invoke(*arguments, format=format)
self.assertEqual(status, 0, diagnostics)
for name in ("chat_old.json", "chat_future.json", "src_notes.json", "chat_mismatch.json"):
self.assertIn(name, diagnostics)
for reason in ("format 2", "older UIMessage/parts", "filename/ID mismatch", "skipped"):
self.assertIn(reason, diagnostics)
if format == "json":
sessions = {item["id"]: item for item in json.loads(output)["records"]}
self.assertEqual(sessions["chat_old"]["name"], "chat_old")
self.assertIsNone(sessions["chat_old"]["createdAt"])
self.assertEqual(sessions["chat_new"]["name"], "Recent conversation")
self.assertEqual(sessions["src_notes"]["name"], "/never/open/these notes.md")
else:
self.assertIn("First precise statement", output)
self.assertIn("Recent conversation", output)
self.assertIn("/never/open/these notes.md", output)
self.assertEqual(snapshot(self.root), before)
def test_explicit_empty_query_and_ids_rejected_but_whitespace_accepted(self):
for arguments in (("find", ""), ("propositions", "--session", ""), ("chains", "--node", "")):
with self.subTest(arguments=arguments):
out, err = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
with self.assertRaises(SystemExit) as raised:
graph.main(["--data-dir", str(self.root), *arguments])
self.assertEqual(raised.exception.code, 2)
self.assertEqual(out.getvalue(), "")
self.assertIn("must not be empty", err.getvalue())
for arguments in (("find", " "), ("propositions", "--session", " "), ("chains", "--node", " ")):
self.assertEqual(self.result(*arguments), [])
self.assertEqual(list(self.root.iterdir()), [])
def test_empty_first_provenance_acquires_birth_from_surviving_link(self):
self.write_ledger("nodes", [node("n_x"), node("n_x", "survivor")])
self.assertEqual([item["id"] for item in self.result("propositions", "--session", "survivor")], ["n_x"])
self.assertEqual(self.result("sessions")[0]["id"], "survivor")
def test_session_order_timestamp_ties_then_stable_id(self):
self.write_ledger("nodes", [node("n_b", "b"), node("n_a", "a"), node("n_c", "c")])
self.assertEqual([item["id"] for item in self.result("sessions")], ["a", "b", "c"])
def test_find_matches_names_statements_aliases_and_literal_punctuation(self):
self.fixture()
for query in ("IDENTITY", "precise", "later RESTATEMENT", "another phrasing"):
self.assertEqual([item["id"] for item in self.result("find", query)], ["n_b"])
self.assertEqual([item["id"] for item in self.result("find", "[brackets]?")], ["n_a"])
self.assertEqual([item["id"] for item in self.result("find", "Café")], ["n_a"])
self.assertEqual(self.result("find", "n_b"), []) # IDs are not searchable fields.
self.assertEqual(self.result("find", "Name must not replace"), [])
self.assertEqual(self.result("find", " no such text "), [])
def test_find_case_matching_agrees_with_app_js_regex(self):
self.write_ledger("nodes", [node("n_case", name="Exact label", statement="Exact statement", aliases=["K", "ſ", "ı", "İ", "ς", "𐐨", "Café"])])
for query in ("k", "s", "i", "𐐀"):
# Explicit stars avoid unrelated substring matches in the name.
self.assertEqual(self.result("find", query + "*"), [], query)
for query in ("K*", "ſ*", "ı*", "İ*", "σ*", "𐐨*", "CAFÉ*"):
self.assertEqual([item["id"] for item in self.result("find", query)], ["n_case"], query)
def test_find_star_glob_newlines_and_ledger_order(self):
self.fixture()
self.assertEqual([item["id"] for item in self.result("find", "*statement*")], ["n_b", "n_empty", "n_surviving", "n_unknown"])
self.assertEqual([item["id"] for item in self.result("find", "two*lines")], ["n_a"])
self.assertEqual(self.result("find", "precise*"), [])
self.assertEqual(len(self.result("find", "*")), 5)
def test_missing_ledgers_empty_graph_and_no_directory_creation(self):
for command in ("sessions", "propositions", "chains"):
self.assertEqual(self.result(command), [])
self.assertEqual(self.result("find", "anything"), [])
self.assertEqual(list(self.root.iterdir()), [])
status, output, diagnostics = self.invoke("propositions", format="markdown")
self.assertEqual(status, 0, diagnostics)
self.assertIn("No matching records", output)
self.assertEqual(list(self.root.iterdir()), [])
def test_malformed_and_wrong_shape_lines_warn_instead_of_silent_loss(self):
self.write_ledger("nodes", [node("n_ok", "source")])
path = self.root / "graph" / "nodes.jsonl"
with path.open("a", encoding="utf-8") as handle:
handle.write('\n{"torn":\n{"id":"bad"}\nnull\n')
self.write_ledger("chains", [chain("arg_bad", [], "n_ok", "source")])
before = snapshot(self.root)
status, output, diagnostics = self.invoke("propositions")
self.assertEqual(status, 0)
self.assertEqual([item["id"] for item in json.loads(output)["records"]], ["n_ok"])
self.assertIn("nodes.jsonl:3: malformed JSON", diagnostics)
self.assertIn("nodes.jsonl:4: invalid record shape", diagnostics)
self.assertIn("nodes.jsonl:5: invalid record shape", diagnostics)
self.assertIn("chains.jsonl:1: invalid record shape", diagnostics)
self.assertIn("incomplete", diagnostics)
self.assertEqual(snapshot(self.root), before)
def test_future_format_refuses_without_partial_output_or_writes(self):
self.fixture()
self.write_json("graph/format.json", {"version": 2})
before = snapshot(self.root)
for command in ("sessions", "propositions", "chains"):
status, output, diagnostics = self.invoke(command)
self.assertNotEqual(status, 0)
self.assertEqual(output, "")
self.assertIn("newer", diagnostics)
self.assertEqual(snapshot(self.root), before)
def test_current_format_works_and_malformed_json_refuses(self):
self.write_json("graph/format.json", {"version": 1})
self.assertEqual(self.result("propositions"), [])
(self.root / "graph" / "format.json").write_text("{torn", encoding="utf-8")
status, output, diagnostics = self.invoke("propositions")
self.assertNotEqual(status, 0)
self.assertEqual(output, "")
self.assertIn("format.json", diagnostics)
def test_source_paths_are_only_references_and_markdown_is_full_text(self):
self.fixture()
before = snapshot(self.root)
status, output, diagnostics = self.invoke("propositions", format="markdown")
self.assertEqual(status, 0, diagnostics)
for text in ("First precise statement", "Original wording", "Later restatement", "Another phrasing", "chat_old", "m1", "Recent conversation", "Café", "/never/open/these notes.md"):
self.assertIn(text, output)
self.assertIn(f"Data directory: {self.root.resolve()}", output)
self.assertIn("not mastery or guaranteed truth", output)
status, output, diagnostics = self.invoke("chains", "--node", "n_a", format="markdown")
self.assertEqual(status, 0, diagnostics)
self.assertIn("First precise statement", output)
self.assertIn("missing proposition", output)
self.assertEqual(snapshot(self.root), before)
@unittest.skipUnless(hasattr(os, "symlink"), "symlinks unavailable")
def test_ledger_symlink_escape_is_rejected(self):
with tempfile.TemporaryDirectory() as external:
path = Path(external) / "nodes.jsonl"
path.write_text(json.dumps(node("n_external")), encoding="utf-8")
(self.root / "graph").mkdir()
try:
(self.root / "graph" / "nodes.jsonl").symlink_to(path)
except OSError as exc:
self.skipTest(f"symlink permission unavailable: {exc}")
status, output, diagnostics = self.invoke("propositions")
self.assertNotEqual(status, 0)
self.assertEqual(output, "")
self.assertTrue(diagnostics)
self.assertEqual(path.read_text(encoding="utf-8"), json.dumps(node("n_external")))
def test_cli_root_options_order_missing_root_and_no_bytecode(self):
env = {**os.environ, "LH_DATA_DIR": str(GOLDEN)}
env.pop("PYTHONDONTWRITEBYTECODE", None)
before = snapshot(self.root)
caches_before = set(SCRIPTS.rglob("*.pyc"))
command = [sys.executable, str(SCRIPTS / "graph.py")]
completed = subprocess.run([*command, "--data-dir", str(self.root), "--format", "json", "sessions"], env=env, capture_output=True, text=True, check=False)
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertEqual(json.loads(completed.stdout), {"data_dir": str(self.root.resolve()), "records": []}) # Never falls back to repo/LH_DATA_DIR.
bad = subprocess.run([*command, "sessions", "--data-dir", str(self.root)], env=env, capture_output=True, text=True, check=False)
self.assertNotEqual(bad.returncode, 0)
absent = subprocess.run([*command, "--data-dir", str(self.root / "missing"), "sessions"], env=env, capture_output=True, text=True, check=False)
self.assertNotEqual(absent.returncode, 0)
self.assertEqual(absent.stdout, "")
self.assertEqual(snapshot(self.root), before)
self.assertEqual(set(SCRIPTS.rglob("*.pyc")), caches_before)
def test_golden_data_read_only_broad_graph(self):
before = snapshot(GOLDEN)
nodes = self.result("propositions", root=GOLDEN)
chains = self.result("chains", root=GOLDEN)
sessions = self.result("sessions", root=GOLDEN)
self.assertGreaterEqual(len(nodes), 250)
self.assertGreaterEqual(len(chains), 150)
self.assertEqual(len({item["id"] for item in nodes}), len(nodes))
self.assertEqual(sum(item["propositions"] for item in sessions), sum(bool(item["provenance"]) for item in nodes))
for session in sessions:
born = self.result("propositions", "--session", session["id"], root=GOLDEN)
self.assertEqual(len(born), session["propositions"])
self.assertTrue(all(item["provenance"][0]["chatId"] == session["id"] for item in born))
self.assertTrue(any(item["kind"] == "file" for item in sessions))
# Golden chat files are not the original graph birth sessions; their
# missing historical metadata must remain honestly undated.
self.assertTrue(any(item["createdAt"] is None for item in sessions))
self.assertEqual(snapshot(GOLDEN), before)
if __name__ == "__main__":
unittest.main()
+393
View File
@@ -0,0 +1,393 @@
"""Read-only evidence-preservation tests for the native session readers."""
from __future__ import annotations
import copy
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[2]
SCRIPTS = ROOT / "skills" / "learnharness" / "scripts"
GOLDEN = ROOT / "tests" / "fixtures" / "golden-data"
sys.path.insert(0, str(SCRIPTS))
spec = importlib.util.spec_from_file_location("lh_sessions_reader", SCRIPTS / "sessions.py")
sessions = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sessions)
def record(messages=None, session_id="sample", **extra):
return {
"id": session_id, "title": "Evidence session", "createdAt": "2026-10-10T10:00:00Z",
"updatedAt": "2026-10-10T11:00:00Z", "messages": messages or [], **extra,
}
def assistant(*blocks, message_id="teacher", **extra):
return {"id": message_id, "role": "assistant", "content": list(blocks), **extra}
def call(name, arguments, call_id="call-one"):
return {"type": "toolCall", "id": call_id, "name": name, "arguments": arguments}
def result(name, details=None, call_id="call-one", content=None, **extra):
value = {"id": "answer-result", "role": "toolResult", "toolName": name,
"toolCallId": call_id, "isError": False,
"content": content if content is not None else [{"type": "text", "text": '{"kind":"quiz"}'}], **extra}
if details is not None:
value["details"] = details
return value
def marker(kind, content, details, marker_id="marker"):
return {"id": marker_id, "role": "custom", "customType": kind,
"content": content, "details": details, "display": True}
def snapshot(folder):
return {str(p.relative_to(folder)): hashlib.sha256(p.read_bytes()).hexdigest()
for p in folder.rglob("*") if p.is_file()}
class SessionsTest(unittest.TestCase):
def cli(self, root, *args):
return subprocess.run([sys.executable, "-B", str(SCRIPTS / "sessions.py"),
"--data-dir", str(root), *args], capture_output=True,
text=True, env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, check=False)
def save(self, root, value, filename=None):
folder = root / "chats"
folder.mkdir(exist_ok=True)
path = folder / (filename or f"{value['id']}.json")
path.write_text(json.dumps(value), encoding="utf-8")
return path
def render(self, value, **flags):
return sessions.render_markdown(sessions.selected_record(value, **flags))
def test_golden_default_preserves_learning_and_omits_noise(self):
before = snapshot(GOLDEN)
listing = self.cli(GOLDEN, "--format", "json", "list")
self.assertEqual(listing.returncode, 0, listing.stderr)
index = json.loads(listing.stdout)
self.assertEqual(len(index["sessions"]), 2)
self.assertEqual(index["skipped_count"], 0)
self.assertEqual(index["data_dir"], str(GOLDEN.resolve()))
self.assertEqual({s["kind"] for s in index["sessions"]}, {"teach", "practice"})
for summary in index["sessions"]:
response = self.cli(GOLDEN, "read", summary["id"])
self.assertEqual(response.returncode, 0, response.stderr)
raw = json.loads((GOLDEN / "chats" / f"{summary['id']}.json").read_text())
text = response.stdout
for message in raw["messages"]:
self.assertIn(message["id"], text)
if message["role"] == "user":
self.assertIn(sessions._text(message["content"]), text)
if message["role"] == "custom":
self.assertIn(message["customType"], text)
if message["role"] == "toolResult" and message.get("toolName") in sessions.QUESTION_TOOLS:
self.assertIn(message["toolCallId"], text)
details = message.get("details", {})
for key in ("thinking", "note", "readingThinking", "committed", "text", "explanation", "other"):
if key in details:
self.assertIn(sessions._text(details[key]), text)
if message["role"] == "assistant":
for block in message["content"]:
if block["type"] == "text":
self.assertIn(sessions._text(block["text"]), text)
self.assertIn("Explicit omissions", text)
self.assertIn(f"Data directory: {GOLDEN.resolve()}", text)
self.assertNotIn('"thinkingSignature"', text)
self.assertEqual(before, snapshot(GOLDEN))
def test_answer_details_are_full_truth_and_indexes_are_not_joined(self):
answer = {
"kind": "quiz", "questionText": "Q snapshot", "options": [
{"index": 1, "label": "Displayed B"}, {"index": 2, "label": "Displayed A"}],
"answers": [{"index": 1, "label": "Displayed B", "value": "b"}],
"correctIndices": [2], "correct": False, "confidence": 0, "dontKnow": False,
"thinking": "Thinking exactly", "note": "Legacy note separately",
"readingThinking": "Reading exactly", "explanation": "Teacher resolution",
}
source = record([assistant(call("quiz", {"question": "Raw Q", "options": [
{"label": "Authored A", "value": "a"}, {"label": "Authored B", "value": "b"}],
"correctAnswer": "a", "explanation": "Call explanation"})),
result("quiz", answer, content=[{"type": "text", "text": json.dumps({
"kind": "quiz", "confidence": 0, "thinking": "Thinking exactly",
"thinkingWhileReading": "Reading exactly",
"answers": [{"index": 1, "label": "Displayed B"}],
})}])])
before = copy.deepcopy(source)
selected = sessions.selected_record(source)
self.assertEqual(selected["record"]["messages"][1]["details"], answer)
self.assertNotIn("content", selected["record"]["messages"][1])
text = sessions.render_markdown(selected)
for phrase in ("Displayed B", "Authored A", "Thinking exactly", "Legacy note separately", "Reading exactly", "Teacher resolution", "confidence (raw 0–100)**: 0", "Teacher-authored", "Reveal status", "snapshot display indexes"):
self.assertIn(phrase, text)
self.assertNotIn("99", text)
self.assertEqual(source, before)
def test_extra_or_conflicting_answer_content_is_not_silently_discarded(self):
cases = [
({}, {"kind": "explain", "text": "only surviving learner answer"}),
({"kind": "quiz", "confidence": 0}, {"kind": "quiz", "confidence": 99}),
({"kind": "quiz"}, {"kind": "quiz", "thinking": "extra learner reasoning"}),
]
for details, compact in cases:
with self.subTest(details=details, compact=compact):
message = result("quiz", details, content=[{"type": "text", "text": json.dumps(compact)}])
selected = sessions.selected_record(record([message]))
self.assertEqual(selected["record"]["messages"][0]["content"], message["content"])
text = sessions.render_markdown(selected)
self.assertIn("may differ from the primary details snapshot", text)
self.assertIn(sessions._text(json.dumps(compact)), text)
message = result("quiz", {"kind": "quiz", "note": "Legacy thought", "readingThinking": "Reading thought"},
content=[{"type": "text", "text": '{"kind":"quiz","thinking":"Legacy thought","thinkingWhileReading":"Reading thought"}'}])
selected = sessions.selected_record(record([message]))
self.assertNotIn("content", selected["record"]["messages"][0])
self.assertIn("verified redundant", selected["omissions"][0]["reason"])
diagnostic = sessions.selected_record(record([message]), include_operations=True)
self.assertEqual(diagnostic["record"]["messages"][0]["content"], message["content"])
non_json = result("explain", content=[{"type": "text", "text": '{"text": NaN}'}])
self.assertIn("unparsed; full snapshot unavailable", self.render(record([non_json])))
def test_only_exact_learning_acknowledgments_are_noise(self):
for tool, ack in sessions.ACK_TEXT.items():
with self.subTest(tool=tool):
message = result(tool, content=[{"type": "text", "text": ack}])
default = sessions.selected_record(record([message]))
self.assertNotIn("content", default["record"]["messages"][0])
full = sessions.selected_record(record([message]), include_operations=True)
self.assertEqual(full["record"]["messages"][0]["content"], message["content"])
message["content"] = [{"type": "text", "text": "Historical teaching/assessment, not an acknowledgment"}]
self.assertIn("Historical teaching/assessment", self.render(record([message])))
dispatched = result("research", "Dispatched (job example).", content=[{"type": "text", "text": "Full historical research brief"}])
self.assertIn("Full historical research brief", self.render(record([dispatched])))
def test_all_answer_modes_and_zero_false_empty_values(self):
payloads = [
("probe", {"kind": "quiz", "dontKnow": True, "answers": [], "thinking": "don't know reasoning"}),
("ask_user_question", {"kind": "ask", "other": "Other exact text", "answers": [], "text": "typed answer"}),
("explain", {"kind": "explain", "text": "free recall answer", "committed": "full committed solution", "readingThinking": "reading channel"}),
("recall", {"kind": "explain", "text": "recalled verbatim", "committed": "recall key"}),
("quiz", {"kind": "quiz", "dismissed": True, "answers": [], "dontKnow": False}),
]
text = self.render(record([result(name, payload) for name, payload in payloads]))
for _, payload in payloads:
for value in payload.values():
if isinstance(value, str):
self.assertIn(sessions._text(value), text)
self.assertIn("Learner don't know**: true", text)
self.assertIn("Skipped/dismissed**: true", text)
self.assertIn("Learner answers (stored display labels/indexes)**: []", text)
def test_json_content_fallback_when_details_absent_or_null(self):
payload = {"kind": "explain", "text": "fallback answer", "note": "old note", "thinkingWhileReading": "fallback reading", "confidence": 0}
for details in ({}, {"details": None}):
with self.subTest(details=details):
message = result("recall", content=[{"type": "text", "text": json.dumps(payload)}])
message.update(details)
text = self.render(record([message]))
self.assertIn("JSON fallback", text)
self.assertIn("fallback answer", text)
self.assertIn("old note", text)
self.assertIn("fallback reading", text)
self.assertIn("confidence (raw 0–100)**: 0", text)
text = self.render(record([result("probe", content=[{"type": "text", "text": "not JSON"}])]))
self.assertIn("unparsed; full snapshot unavailable", text)
self.assertIn("not JSON", text)
# Parsing one text block must not hide sibling images/extra fields or
# misattribute a later block to content[0].text.
richer = result("recall", content=[
{"type": "image", "mimeType": "image/png", "data": "BINARY-NOT-PRINTED"},
{"type": "text", "text": json.dumps(payload)},
])
text = self.render(record([richer]))
self.assertIn("image/png", text)
self.assertIn("fallback answer", text)
self.assertNotIn("BINARY-NOT-PRINTED", text)
self.assertNotIn("content[0].text (JSON fallback", text)
richer["content"] = [{"type": "text", "text": json.dumps(payload), "futureField": "retain block metadata"}]
self.assertIn("retain block metadata", self.render(record([richer])))
def test_pending_failed_and_legacy_duplicate_call_ids(self):
messages = [
assistant(call("quiz", {"question": "First", "correctAnswer": "first key"}), message_id="first"),
result("quiz", {"kind": "quiz", "text": "first answer"}),
assistant(call("quiz", {"question": "Failed", "correctAnswer": "failed key"}), message_id="second"),
result("quiz", {"problems": ["validation failure"]}, isError=True,
content=[{"type": "text", "text": "failure text"}]),
assistant(call("problem", {"statement": "Pending", "modelSolution": "pending full model solution"}), message_id="third"),
]
statuses = sessions._call_statuses(messages)
self.assertIn("result recorded", statuses[(0, 0)])
self.assertIn("FAILED", statuses[(2, 0)])
self.assertIn("PENDING", statuses[(4, 0)])
text = self.render(record(messages))
for word in ("first key", "failed key", "pending full model solution", "validation failure", "failure text", "not successful", "PENDING", "even pending"):
self.assertIn(word, text)
orphan = result("quiz", {"kind": "quiz", "text": "orphan answer"}, call_id="later")
statuses = sessions._call_statuses([orphan, assistant(call("quiz", {}, call_id="later"))])
self.assertIn("PENDING", statuses[(1, 0)])
def test_flags_are_separate_and_do_not_hide_learner_thinking(self):
source = record([
assistant({"type": "thinking", "thinking": "PRIVATE AGENT THOUGHT", "thinkingSignature": "SIGNATURE"},
{"type": "text", "text": "Teacher text"}, call("read", {"path": "NO FILE OPEN"}),
usage={"tokens": 123}, provider="vendor", responseId="provider-response"),
result("read", "routine read result", content=[{"type": "text", "text": "routine read result"}]),
result("quiz", {"thinking": "LEARNER THOUGHT", "readingThinking": "LEARNER READING"}),
marker("practice-thinking", "practice content", {"text": "PRACTICE THOUGHT"}),
], turns=[{"usage": 42}], extractions=[{"cost": 23}])
default = self.render(source)
for flag, value in (("agent thinking", "PRIVATE AGENT THOUGHT"), ("metadata", "provider-response"), ("routine", "routine read result")):
self.assertNotIn(value, default, flag)
for flags in ({}, {"include_agent_thinking": True}, {"include_operations": True},
{"include_operations": True, "include_agent_thinking": True}):
text = self.render(source, **flags)
for learner in ("LEARNER THOUGHT", "LEARNER READING", "PRACTICE THOUGHT"):
self.assertIn(learner, text)
self.assertEqual("PRIVATE AGENT THOUGHT" in text, flags.get("include_agent_thinking", False))
self.assertEqual("routine read result" in text, flags.get("include_operations", False))
self.assertEqual("SIGNATURE" in text, flags.get("include_agent_thinking", False) and flags.get("include_operations", False))
def test_full_artifacts_refs_and_no_payload_embedding(self):
brief = "FULL BRIEF START\n" + "long evidence paragraph.\n" * 800 + "FULL BRIEF END"
messages = [
marker("subagent-steer", "Delivered research", {"jobId": "job-research", "kind": "research", "toolCallId": "research-call", "result": {"brief": brief, "sources": [{"title": "Source", "url": "https://example.invalid/source"}]}}),
marker("subagent-steer", "PDF delivery", {"jobId": "job-pdf", "kind": "pdf", "toolCallId": "pdf-call", "result": {"brief": brief, "path": "/never/open.pdf", "pagesRead": [1, 4], "pagesConsulted": [2]}}),
marker("subagent-steer", "Demo reference", {"kind": "demo", "result": {"title": "Demo title", "html": "<script>PAYLOAD-HTML</script>", "tested": True, "notes": "Demo notes"}}),
marker("subagent-steer", "Visual reference", {"kind": "visual", "result": {"svg": "<svg>PAYLOAD-SVG</svg>", "notes": "Visual notes"}}),
{"id": "image-user", "role": "user", "content": [{"type": "image", "mimeType": "image/png", "data": "PAYLOAD-BASE64"}, {"type": "text", "text": "Describe this image"}]},
]
for flags in ({}, {"include_operations": True, "include_agent_thinking": True}):
selected = sessions.selected_record(record(messages), **flags)
text = sessions.render_markdown(selected)
output = json.dumps(selected)
self.assertIn(brief, text)
for phrase in ("job-research", "research-call", "job-pdf", "pdf-call", "/never/open.pdf", "pagesRead", "pagesConsulted", "Demo title", "Demo notes", "Visual notes", "image/png"):
self.assertIn(phrase, text)
for payload in ("PAYLOAD-HTML", "PAYLOAD-SVG", "PAYLOAD-BASE64"):
self.assertNotIn(payload, text)
self.assertNotIn(payload, output)
self.assertEqual(selected["omitted_field_count"], 3)
# A persisted full artifact result is not a routine dispatch acknowledgment.
direct = self.render(record([result("research", {"brief": brief}), result("pdf", {"brief": brief, "pagesRead": [7]})]))
self.assertEqual(direct.count(brief), 2)
self.assertIn("pagesRead", direct)
def test_graph_context_compact_default_and_full_operations(self):
context = marker("graph-context", "VERBOSE INJECTED GRAPH PREAMBLE", {
"propositions": [{"id": "node-a", "name": "Node A", "statement": "VERBOSE GRAPH STATEMENT"},
{"id": "node-b", "name": "Node B", "statement": "SECOND GRAPH STATEMENT"}],
"chains": ["chain-a", "chain-b"], "futureContext": "unknown preserved context",
}, marker_id="context-id")
source = record([context,
{"id": "learner", "role": "user", "content": "VERBOSE GRAPH STATEMENT"},
assistant({"type": "text", "text": "Teacher speech preserved"}),
marker("practice-thinking", "Learner practice speech", {"text": "Learner trace"}),
marker("subagent-steer", "Artifact delivery", {"kind": "research", "result": {"brief": "Full artifact speech"}})])
original = copy.deepcopy(source)
compact = sessions.selected_record(source)
selected_context = compact["record"]["messages"][0]
self.assertNotIn("content", selected_context)
self.assertNotIn("statement", selected_context["details"]["propositions"][0])
self.assertEqual(selected_context["details"]["chains"], ["chain-a", "chain-b"])
self.assertEqual({o["source"] for o in compact["omissions"]}, {
"messages[0].content", "messages[0].details.propositions[0].statement",
"messages[0].details.propositions[1].statement"})
text = sessions.render_markdown(compact)
for phrase in ("context-id", "node-a", "Node A", "node-b", "Node B", "chain-a", "chain-b", "unknown preserved context",
"VERBOSE GRAPH STATEMENT", "Teacher speech preserved", "Learner practice speech", "Learner trace", "Full artifact speech"):
self.assertIn(phrase, text)
self.assertNotIn("VERBOSE INJECTED GRAPH PREAMBLE", text)
self.assertNotIn("SECOND GRAPH STATEMENT", text)
self.assertNotIn("Unsupported/unrendered native content shape", text)
full = sessions.selected_record(source, include_operations=True)
self.assertEqual(full["record"]["messages"][0], context)
self.assertIn("VERBOSE INJECTED GRAPH PREAMBLE", sessions.render_markdown(full))
self.assertIn("SECOND GRAPH STATEMENT", sessions.render_markdown(full))
self.assertEqual(source, original)
# Unknown context shapes cannot safely be compacted into references.
unknown = marker("graph-context", "only stored context", {"future": "shape"})
self.assertIn("only stored context", self.render(record([unknown])))
def test_unknown_shapes_markers_and_content_are_explicit(self):
source = record([
marker("new-marker", "unknown content", {"futureField": "opaque payload"}),
assistant({"type": "future-block", "opaque": "block evidence"}, call("new-tool", {"note": "future tool"})),
{"id": "new-role", "role": "alien", "content": {"unknown": "role evidence"}},
result("new-tool", {"future": "result evidence"}),
])
text = self.render(source)
self.assertGreaterEqual(text.lower().count("unsupported/unrendered"), 5)
self.assertIn("Stored role: alien (unsupported semantics; fields preserved).", text)
for value in ("unknown content", "opaque payload", "block evidence", "future tool", "role evidence", "result evidence"):
self.assertIn(value, text)
# HTML in ordinary learner text stays verbatim in selected JSON, but is inert markdown.
source = record([{"role": "user", "id": "u", "content": '<script>alert("untrusted")</script>'}])
self.assertEqual(sessions.selected_record(source)["record"]["messages"][0]["content"], source["messages"][0]["content"])
self.assertNotIn("<script>", self.render(source))
self.assertIn("&lt;script&gt;", self.render(source))
def test_cli_errors_isolated_list_and_missing_defaults(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
self.save(root, record())
self.save(root, record(session_id="future", version=2))
self.save(root, record(session_id="old", messages=[{"role": "assistant", "parts": []}]))
(root / "chats" / "broken.json").write_text("{bad", encoding="utf-8")
before = snapshot(root)
output = self.cli(root, "--format", "json", "list")
self.assertEqual(output.returncode, 0, output.stderr)
value = json.loads(output.stdout)
self.assertEqual(value["sessions"][0]["kind"], "teach")
self.assertEqual(value["skipped_count"], 3)
self.assertEqual({s["id"] for s in value["skipped"]}, {"future", "old", "broken"})
self.assertEqual(output.stderr.count("Warning: skipped session"), 3)
plain = self.cli(root, "list")
self.assertIn("Skipped/unreadable sessions: 3", plain.stdout)
for session_id, phrase in (("future", "Update the skill"), ("old", "UIMessage"), ("broken", "Cannot read"), ("missing", "Session not found"), ("../escape", "session ID")):
result_ = self.cli(root, "read", session_id)
self.assertNotEqual(result_.returncode, 0)
self.assertIn(phrase, result_.stderr)
self.assertNotIn("Traceback", result_.stderr)
self.assertEqual(result_.stdout, "")
good = self.cli(root, "--format", "json", "read", "sample", "--include-agent-thinking", "--include-operations")
self.assertEqual(good.returncode, 0, good.stderr)
selected = json.loads(good.stdout)
self.assertIn("record", selected)
self.assertEqual(selected["data_dir"], str(root.resolve()))
self.assertTrue(selected["reading_notes"])
self.assertNotEqual(self.cli(root, "read", "sample", "--format", "json").returncode, 0)
self.assertEqual(before, snapshot(root))
absent = root / "not-created"
missing = self.cli(absent, "list")
self.assertNotEqual(missing.returncode, 0)
self.assertFalse(absent.exists())
def test_symlink_escape_refused_without_opening_external_record(self):
with tempfile.TemporaryDirectory() as directory, tempfile.TemporaryDirectory() as outside:
root = Path(directory)
target = self.save(Path(outside), record(session_id="escape"))
(root / "chats").mkdir()
try:
(root / "chats" / "escape.json").symlink_to(target)
except OSError as exc:
self.skipTest(str(exc))
result_ = self.cli(root, "read", "escape")
self.assertNotEqual(result_.returncode, 0)
self.assertIn("outside", result_.stderr)
listing = self.cli(root, "--format", "json", "list")
self.assertEqual(json.loads(listing.stdout)["skipped_count"], 1)
if __name__ == "__main__":
unittest.main()
+100
View File
@@ -0,0 +1,100 @@
"""The skill remains a portable folder, not an integration with the app."""
from __future__ import annotations
import ast
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
sys.dont_write_bytecode = True
REPO = Path(__file__).resolve().parents[2]
SKILL = REPO / "skills" / "learnharness"
GOLDEN = REPO / "tests" / "fixtures" / "golden-data"
def snapshot(root):
return {
path.relative_to(root).as_posix(): (path.read_bytes(), path.stat().st_mtime_ns) if path.is_file() else None
for path in root.rglob("*")
}
class SkillFolderTest(unittest.TestCase):
def test_portable_frontmatter_and_local_reference(self):
text = (SKILL / "SKILL.md").read_text(encoding="utf-8")
frontmatter = text.split("---", 2)[1]
fields = dict(line.split(": ", 1) for line in frontmatter.strip().splitlines())
self.assertEqual(fields["name"], SKILL.name)
self.assertTrue(0 < len(fields["description"]) <= 1024)
self.assertTrue(0 < len(fields["compatibility"]) <= 500)
self.assertEqual(fields["license"], "AGPL-3.0-only")
self.assertLess(len(text.splitlines()), 500)
self.assertIn("references/data-format.md", text)
self.assertTrue((SKILL / "references" / "data-format.md").is_file())
self.assertIn("Never modify LearnHarness's data", text)
self.assertIn("archival view", text)
self.assertNotIn("/Users/amos/", text)
def test_license_is_bundled_for_a_standalone_copy(self):
self.assertEqual((SKILL / "LICENSE").read_bytes(), (REPO / "LICENSE").read_bytes())
self.assertIn("GNU AFFERO GENERAL PUBLIC LICENSE", (SKILL / "LICENSE").read_text(encoding="utf-8"))
def test_python_syntax_supports_declared_minimum(self):
for script in (SKILL / "scripts").glob("*.py"):
with self.subTest(script=script.name):
ast.parse(script.read_text(encoding="utf-8"), filename=str(script), feature_version=(3, 10))
def test_copied_skill_runs_outside_repository_and_creates_no_cache_or_data(self):
with tempfile.TemporaryDirectory() as temporary:
base = Path(temporary)
skill = base / "standalone" / "learnharness"
data = base / "learning-data"
elsewhere = base / "unrelated-working-directory"
elsewhere.mkdir()
shutil.copytree(SKILL, skill)
shutil.copytree(GOLDEN, data)
before_skill = snapshot(skill)
before_data = snapshot(data)
env = dict(os.environ)
env.pop("PYTHONDONTWRITEBYTECODE", None)
env.pop("PYTHONPATH", None)
commands = [
("sessions.py", ["list"]),
("sessions.py", ["read", "chat_mv27rrra_feff23b6"]),
("sessions.py", ["read", "chat_mv293wv5_10656952"]),
("sessions.py", ["read", "chat_mv293wv5_10656952", "--include-agent-thinking", "--include-operations"]),
("graph.py", ["sessions"]),
("graph.py", ["propositions"]),
("graph.py", ["find", "*"]),
("graph.py", ["chains"]),
("graph.py", ["chains", "--node", "n_nonexistent"]),
]
for filename, arguments in commands:
for output_format in ("markdown", "json"):
with self.subTest(script=filename, arguments=arguments, format=output_format):
# Deliberately omit -B here: the entrypoints must disable
# bytecode before importing their helper on their own.
completed = subprocess.run(
[sys.executable, str(skill / "scripts" / filename), "--data-dir", str(data), "--format", output_format, *arguments],
cwd=elsewhere, env=env, capture_output=True, text=True, encoding="utf-8", timeout=30,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertTrue(completed.stdout.strip())
if output_format == "json":
result = json.loads(completed.stdout)
self.assertEqual(result["data_dir"], str(data.resolve()))
else:
self.assertIn(str(data.resolve()), completed.stdout)
self.assertEqual(snapshot(skill), before_skill)
self.assertEqual(snapshot(data), before_data)
self.assertEqual(list(elsewhere.iterdir()), [])
if __name__ == "__main__":
unittest.main()