core/tests/test_teaching_propose_cli.py
Shay 2c5eb2e36b
feat(w019): wire core teaching propose-miner/propose-curriculum CLI commands (#266)
Closes W-019 wiring debt. Per Phase 2 operator decision (path a):
wire CLI — smallest reachability fix, no architectural commitment.

teaching/from_miner.py and teaching/from_curriculum.py (ADR-0095/ADR-0104)
correctly build source-stamped PackMutationProposals but had no CLI or
runtime caller — test-live only. Now reachable:

  core teaching propose-miner  \
      --findings <jsonl> --miner-id <id> [--revision <rev>] [--out <jsonl>]

  core teaching propose-curriculum \
      --findings <jsonl> --curriculum-id <id> [--revision <rev>] [--out <jsonl>]

Changes to core/cli.py:
- _load_findings_jsonl(): deserializes ContemplationFinding records from
  operator-provided JSONL (as_dict() round-trip format).
- _read_jsonl_file(): shared JSONL line reader.
- cmd_teaching_propose_miner(): calls from_miner.from_findings(); writes
  proposals to --out JSONL or stdout; prints proposals/rejections summary
  to stderr. Returns 0 if any proposals built, 1 otherwise.
- cmd_teaching_propose_curriculum(): same shape for curriculum path.
- _current_git_revision(): --revision default, falls back to "unknown".
- _write_miner_curriculum_batch(): shared proposal serialisation + summary.
- Two new subcommands registered: propose-miner, propose-curriculum.

tests/test_teaching_propose_cli.py: 5 tests covering round-trip loading,
stdout output, file output, and empty-findings error path.
2026-05-25 06:09:57 -07:00

161 lines
5.1 KiB
Python

"""Tests for W-019: core teaching propose-miner / propose-curriculum CLI commands."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from core.contemplation.schema import (
ContemplationEvidenceRef,
ContemplationFinding,
FindingKind,
)
from teaching.epistemic import EpistemicStatus
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
def _good_finding(
*,
subject: str = "knowledge",
predicate: str = "requires",
object_: str | None = "evidence",
proposed_action: str = "extend cognition pack with knowledge→evidence chain",
substrate_hash: str = "abc123",
) -> ContemplationFinding:
return ContemplationFinding(
kind=FindingKind.PACK_MUTATION_CANDIDATE,
subject=subject,
predicate=predicate,
object=object_,
evidence_refs=(
ContemplationEvidenceRef(
source_type="articulation_observation",
source_id="run-1",
pointer="turn:1",
summary="weak surface",
),
),
proposed_action=proposed_action,
substrate_hash=substrate_hash,
)
def _write_findings_jsonl(tmp_path: Path, findings: list) -> Path:
path = tmp_path / "findings.jsonl"
lines = [json.dumps(f.as_dict(), sort_keys=True, ensure_ascii=False) for f in findings]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return path
# ---------------------------------------------------------------------------
# _load_findings_jsonl — unit test
# ---------------------------------------------------------------------------
def test_load_findings_jsonl_round_trips(tmp_path: Path) -> None:
from core.cli import _load_findings_jsonl
finding = _good_finding()
jsonl_path = _write_findings_jsonl(tmp_path, [finding])
loaded = _load_findings_jsonl(str(jsonl_path))
assert len(loaded) == 1
assert loaded[0].subject == "knowledge"
assert loaded[0].kind is FindingKind.PACK_MUTATION_CANDIDATE
assert loaded[0].epistemic_status is EpistemicStatus.SPECULATIVE
# ---------------------------------------------------------------------------
# cmd_teaching_propose_miner
# ---------------------------------------------------------------------------
def test_propose_miner_writes_proposals_to_stdout(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
import argparse
from core.cli import cmd_teaching_propose_miner
finding = _good_finding()
jsonl_path = _write_findings_jsonl(tmp_path, [finding])
args = argparse.Namespace(
findings=str(jsonl_path),
miner_id="test_miner_v1",
revision="test-revision",
out=None,
)
rc = cmd_teaching_propose_miner(args)
assert rc == 0
captured = capsys.readouterr()
proposal = json.loads(captured.out.strip().splitlines()[0])
assert "proposal_id" in proposal
assert proposal["subject"] == "knowledge"
def test_propose_miner_writes_to_file(tmp_path: Path) -> None:
import argparse
from core.cli import cmd_teaching_propose_miner
finding = _good_finding()
jsonl_path = _write_findings_jsonl(tmp_path, [finding])
out_path = tmp_path / "out.jsonl"
args = argparse.Namespace(
findings=str(jsonl_path),
miner_id="test_miner_v1",
revision="test-revision",
out=str(out_path),
)
rc = cmd_teaching_propose_miner(args)
assert rc == 0
lines = out_path.read_text(encoding="utf-8").strip().splitlines()
assert len(lines) == 1
proposal = json.loads(lines[0])
assert proposal["subject"] == "knowledge"
# ---------------------------------------------------------------------------
# cmd_teaching_propose_curriculum
# ---------------------------------------------------------------------------
def test_propose_curriculum_writes_proposals_to_stdout(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
import argparse
from core.cli import cmd_teaching_propose_curriculum
finding = _good_finding(subject="truth", predicate="grounds", proposed_action="ground truth chain")
jsonl_path = _write_findings_jsonl(tmp_path, [finding])
args = argparse.Namespace(
findings=str(jsonl_path),
curriculum_id="gsm8k_curriculum_v1",
revision="test-revision",
out=None,
)
rc = cmd_teaching_propose_curriculum(args)
assert rc == 0
captured = capsys.readouterr()
proposal = json.loads(captured.out.strip().splitlines()[0])
assert "proposal_id" in proposal
assert proposal["subject"] == "truth"
def test_propose_miner_returns_nonzero_on_empty_findings(tmp_path: Path) -> None:
import argparse
from core.cli import cmd_teaching_propose_miner
empty_path = tmp_path / "empty.jsonl"
empty_path.write_text("", encoding="utf-8")
args = argparse.Namespace(
findings=str(empty_path),
miner_id="test_miner_v1",
revision="test-revision",
out=None,
)
with pytest.raises(SystemExit) as exc_info:
cmd_teaching_propose_miner(args)
assert exc_info.value.code == 1