From ec3600e7454367029dba6e866fcd5a8c23fda622 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Fri, 6 Mar 2026 02:37:09 -0500 Subject: [PATCH] Add minimal REPL host and tests --- src/context_compiler/repl.py | 30 +++++++++++++++++++ tests/test_repl.py | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/context_compiler/repl.py create mode 100644 tests/test_repl.py diff --git a/src/context_compiler/repl.py b/src/context_compiler/repl.py new file mode 100644 index 0000000..8239d04 --- /dev/null +++ b/src/context_compiler/repl.py @@ -0,0 +1,30 @@ +import json +import sys +from typing import TextIO + +from . import create_engine +from .engine import Decision + + +def format_decision(decision: Decision) -> str: + return json.dumps(decision, sort_keys=True, separators=(",", ":")) + + +def run_repl(in_stream: TextIO, out_stream: TextIO) -> None: + engine = create_engine() + + for line in in_stream: + user_input = line.rstrip("\n") + if user_input.strip().lower() in {"exit", "quit"}: + return + decision = engine.step(user_input) + print(format_decision(decision), file=out_stream) + + +def main() -> int: + run_repl(sys.stdin, sys.stdout) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_repl.py b/tests/test_repl.py new file mode 100644 index 0000000..beef478 --- /dev/null +++ b/tests/test_repl.py @@ -0,0 +1,57 @@ +import json +from io import StringIO + +from context_compiler.repl import run_repl + + +def _run_session(text: str) -> list[dict[str, object]]: + out = StringIO() + run_repl(StringIO(text), out) + lines = [line for line in out.getvalue().splitlines() if line.strip()] + return [json.loads(line) for line in lines] + + +def test_repl_update_flow() -> None: + decisions = _run_session("use Nord Stage 4\nquit\n") + + assert len(decisions) == 1 + assert decisions[0]["kind"] == "update" + assert decisions[0]["state"] == { + "facts": {"focus.device": "Nord Stage 4"}, + "policies": {"prohibit": []}, + "version": 1, + } + + +def test_repl_clarify_flow() -> None: + decisions = _run_session("no use docker\nquit\n") + + assert len(decisions) == 1 + assert decisions[0]["kind"] == "clarify" + assert decisions[0]["state"] is None + + +def test_repl_state_persists_across_turns() -> None: + decisions = _run_session("avoid docker\nallow docker\nquit\n") + + assert len(decisions) == 2 + assert decisions[0]["kind"] == "update" + assert decisions[0]["state"] == { + "facts": {"focus.device": None}, + "policies": {"prohibit": ["docker"]}, + "version": 1, + } + assert decisions[1]["kind"] == "update" + assert decisions[1]["state"] == { + "facts": {"focus.device": None}, + "policies": {"prohibit": []}, + "version": 1, + } + + +def test_repl_exit_and_quit_terminate_session() -> None: + decisions_exit = _run_session("exit\nuse Nord Stage 4\n") + decisions_quit = _run_session("quit\nuse Nord Stage 4\n") + + assert decisions_exit == [] + assert decisions_quit == []