From 440cefcfca376c03190384d8546658e912b5fc8e Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:30:31 +0800 Subject: [PATCH] fix(cli): reuse canonical bootstrap for source invocations Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- docs/reference/source-cli-entrypoint.md | 157 ++++++++ loopx/cli.py | 13 +- loopx/entrypoint.py | 5 + .../test_source_cli_entrypoint.py | 337 ++++++++++++++++++ 4 files changed, 508 insertions(+), 4 deletions(-) create mode 100644 docs/reference/source-cli-entrypoint.md create mode 100644 tests/control_plane/test_source_cli_entrypoint.py diff --git a/docs/reference/source-cli-entrypoint.md b/docs/reference/source-cli-entrypoint.md new file mode 100644 index 0000000000..f2d6f704aa --- /dev/null +++ b/docs/reference/source-cli-entrypoint.md @@ -0,0 +1,157 @@ +# Source CLI entrypoint / 源 CLI 入口 + +`python -m loopx.cli` uses the same `loopx.entrypoint.main` bootstrap as the +console command. Importing `loopx.cli.main` or `build_parser` remains the full +in-process compatibility API. This is a Python host-bootstrap adapter repair, +not a new Python policy owner, command registry, validator or provider. + +`python -m loopx.cli` 复用 console command 的 `loopx.entrypoint.main`。以库方式 +导入 `loopx.cli.main` 或 `build_parser` 仍保留完整 parser 与进程内兼容调用。 +本次只修复 Python 宿主启动适配器,不增加 Python 策略 owner、命令目录、validator +或 provider。 + +## Owning boundary / 所属边界 + +- Version and root help avoid full command imports. Selected common commands + reuse the existing grammar, command registrars and handlers. Rejected selected + arguments return to the full parser for canonical diagnostics; other commands + retain the full-parser fallback. +- Receipt-bound scheduler follow-ups reuse the existing TS executable. Manual + or unbound follow-ups retain their compatibility route. Missing Node on the + bound route fails closed, not back into a weaker interpreter path. +- Outer-controlled calls stay in the selected dispatcher so its existing native + controller write guard runs before process replacement or provider effects. + The guard still owns Goal matching; the bootstrap does not implement another + permission decision. +- Quota, Todo completion, declaration/source/lease checks, provider commits and + settlement receipts retain their owners. No assertion, frozen completion + deadline or one-debit requirement changes. + +对应中文: + +- Version 与根 help 不加载全部命令。常用命令复用既有 grammar、registrar 和 + handler;selected parser 拒绝的参数交还完整 parser 输出原诊断,其他命令继续 + 走完整 parser。 +- 已绑定回执的 scheduler follow-up 复用现有 TS executable;手工或无绑定调用 + 保留兼容路线。绑定路线缺失 Node 时关闭失败,不退回较弱的解释器路径。 +- 外层控制器调用先经过 selected dispatcher 中现有 native controller write + guard,再决定写入。Goal 匹配仍由 guard 判断,bootstrap 不复制权限规则。 +- Quota、Todo 完成、declaration/source/lease fence、provider commit 与结算 + receipt 的 owner 不变。断言、冻结完成期限和一次扣额规则均不修改。 + +This belongs to S2's single-authority boundary and S10's evidenced recovery-cost +work, not full TS cutover or sustained provider qualification. See the +[TS migration RFC](../architecture/rfcs/typescript-control-plane-migration-v0.md), +[overall roadmap](../architecture/rfcs/loopx-overall-roadmap-v0.md) and +[optimization evidence guide](../development/testing-and-quality.md#roadmap-aligned-optimization). + +这属于 S2 单一权威边界和 S10 有证据的恢复成本优化,不代表 TS 全量切换或长程 +provider 验收完成。沿用上述 RFC、roadmap 与验收规则,不新建平行规划。 + +## User-visible compatibility / 用户可见兼容性 + +Source invocations now also share the existing console help and usage-statistics +policy. The first eligible source command may disclose on stderr; subsequent +commands follow the machine-local choice and existing TS policy. JSON stdout +stays pure; help/version do not start observation. `LOOPX_USAGE_PING=0`, +`DO_NOT_TRACK=1` and the existing disabled setting retain their opt-out behavior. +This intentionally removes entrypoint drift; it is not byte parity with the +old source entry's absence of disclosure. + +源调用也使用现有 console help 和 usage-statistics 策略。首条符合条件的源命令 +可能在 stderr 展示告知;后续沿用机器设置及既有 TS 策略。JSON stdout 保持纯净, +help/version 不启动观测;上述环境变量及既有 disabled 设置继续关闭观测。 +这是有意消除入口漂移,不宣称与旧源入口“不展示告知”的行为逐字等价。 + +The affected journey is source CLI invocation, including managed/canary callers +that execute that module. Frontend and Lark do not gain a new control, schema or +configuration authority. They consume the same existing command projections and +receipts; this repair does not newly qualify their delivery transports. The +existing usage-settings HTTP interaction verifies the shared machine choice. +No frontend asset or layout changes, so no repackaging is needed for this slice. + +受影响路径是源 CLI,以及执行该模块的 managed/canary caller。前端和 Lark 不增加 +控件、schema 或配置 owner,继续消费同一既有投影及回执;本次不新验收它们的投递 +transport。现有 usage-settings HTTP 交互验证共享机器设置。没有前端资源或布局 +改动,因此此切片无需重新打包前端。 + +## Qualification / 验收 + +Fresh-interpreter regressions compare source and console entries, preserve +canonical fallback diagnostics, prove unrelated owners remain unloaded, and +exercise outer-controller rejection before any Goal-mutating effect. Real File and SQLite +reads still work with the Markdown display removed, with exact provider-revision +readback and no mutation. Existing completion tests retain actual declaration +binding, validator execution, failure and replay. Existing scheduler tests keep +all callers, storage layouts, cross-agent identity and state-key assertions. +Source and console callers also preserve reads, permit existing and newly +registered independent Goals, reject actor changes as a scope escape, and +resume actual receipt-bound scheduler writes after the controller releases its scope. + +新解释器回归对比源入口与 console:保留 fallback 诊断,证明不加载无关 owner, +验证外层控制器拒绝发生在任何 Goal-mutating effect 之前。真实 File/SQLite 读回在移除 Markdown +展示后仍成功,保持精确 provider revision 且不修改权威。既有完成测试保留真实 +声明绑定、validator 执行、失败及重放;调度测试不删 caller、存储布局、跨 Agent +身份或 state-key 断言。 +源入口与 console 还验证了读取、现有及后注册的独立 Goal 不被误拦、改变 actor +不能逃逸 Goal 范围,以及控制器解除保护后真实的回执绑定 scheduler 写回。 + +```sh +uv run --extra test python -m pytest \ + tests/control_plane/test_source_cli_entrypoint.py \ + tests/test_cli_entrypoint.py \ + tests/test_usage_ping.py \ + tests/control_plane/test_completion_validation_initial_binding.py \ + tests/control_plane/test_completion_validation_lane_scope.py \ + tests/control_plane/test_scheduler_ack_current_host_binding.py \ + tests/control_plane/test_scheduler_compat_state_key.py \ + tests/control_plane/test_scheduler_host_followup_hint_transport.py \ + tests/test_kunluncode_goal_mode.py \ + tests/control_plane/test_public_boundary_parallel_reads.py -q --durations=10 +``` + +The 2026-09-28 development-host ABBA comparison retained the unchanged 28-case +scheduler matrix on base `f679911568eee2b3c3cfa1a6ade43dcb17cfb06e` and the +pre-rebase source-entry candidate on that base, using the same Python 3.13 +interpreter. Subsequent inclusion of the separately merged reader optimization +is not retroactively part of these measurements: + +| Arm / 分组 | First wall time / 首轮 | Second wall time / 次轮 | +| --- | --- | --- | +| Base / 主干 | 129.28 s | 123.24 s | +| Candidate / 候选 | 120.63 s | 191.03 s | + +All four runs passed the same 28 assertions. The candidate has an adverse slow +sample and does **not** establish a stable whole-batch improvement. The host and +OS/service caches were shared, not isolated cold-machine samples. The original +20-second budget was not met or increased. Independent import-loading and +outer-controller oracles reject both the old eager source entry and deliberately +reintroduced eager loading/unsafe process replacement; they qualify the repaired +boundary, not the batch deadline. + +四轮保留同一 28 项断言并全部通过,但候选存在变慢样本,不能宣称整批稳定提速。 +宿主及 OS/service cache 共享,不是隔离冷机器测量;原 20 秒预算未达成,也没有 +调高。独立加载与权限反例会拒绝旧 eager 入口,以及故意重新引入的 eager loading +或提前 process replacement;它们只证明修复边界,不证明整批期限已通过。 + +On that active shared host, an eight-arm alternating fresh-process startup +probe (four samples per revision and command, shared OS caches) measured +`--version` medians of 0.759 s → 0.040 s and `quota --help` medians of +0.805 s → 0.072 s. These measure only the tiny/help startup boundary, not a +business operation, cold machine, isolated load or p95 latency. + +同一活动共享宿主上的八组交替新进程 probe(每版本每命令四次,共享 OS cache) +测得上述 version/help 中位数。它们仅测量 tiny/help 启动边界,不代表业务操作、 +冷机器、隔离负载或 p95 延迟。 + +Startup savings are not whole-batch qualification. Measure the unchanged real-CLI +matrix with the same interpreter and alternating base/head arms; disclose shared +OS/service caches rather than calling a fresh Python process a cold-machine run. +The original multi-command synchronous-deadline acceptance remains open until +the complete workload passes its frozen budget. Source entry repair neither +approves a validator timeout increase nor makes progress writeback terminal. + +启动开销减少不等于整批验收通过。使用同一解释器交替测量未修改的真实 CLI 矩阵, +披露共享 OS/service cache,不把新 Python 进程叫作冷机器测量。只有完整负载通过 +冻结预算,才算完成原同步期限验收。本修复不批准扩大 validator timeout,也不把 +阶段写回转成 terminal 完成。 diff --git a/loopx/cli.py b/loopx/cli.py index 6dedd75eb7..3a7ebb00aa 100644 --- a/loopx/cli.py +++ b/loopx/cli.py @@ -1,5 +1,14 @@ from __future__ import annotations +# Source-tree invocations must use the same bounded entrypoint as the console +# script. Keep the full parser below importable for compatibility callers; the +# entrypoint can still import this module by name when a full-parser fallback is +# required, without executing this module-entry branch a second time. +if __name__ == "__main__": + from .entrypoint import main as _entrypoint_main + + raise SystemExit(_entrypoint_main()) + from .cli_commands.automation_cadence import ( register_automation_cadence_command, handle_automation_cadence_command, ) @@ -1029,7 +1038,3 @@ def main(argv: list[str] | None = None) -> int: return 1 return 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/loopx/entrypoint.py b/loopx/entrypoint.py index d92270513c..252be827d9 100644 --- a/loopx/entrypoint.py +++ b/loopx/entrypoint.py @@ -20,6 +20,11 @@ def _bound_option_present(argv: list[str], option: str) -> bool: def _native_scheduler_followup_argv(raw_argv: list[str]) -> list[str] | None: """Select only generated, receipt-bound scheduler follow-up commands.""" + # The selected Python dispatcher owns the existing outer-controller guard. + # Do not process-replace it before it can reject a model-visible write. + if os.environ.get("LOOPX_KUNLUNCODE_OUTER_CONTROLLER") == "1": + return None + value_options = {"--format", "--registry", "--runtime-root"} positionals: list[str] = [] skip_value = False diff --git a/tests/control_plane/test_source_cli_entrypoint.py b/tests/control_plane/test_source_cli_entrypoint.py new file mode 100644 index 0000000000..21dfe9ff70 --- /dev/null +++ b/tests/control_plane/test_source_cli_entrypoint.py @@ -0,0 +1,337 @@ +"""Fresh source-module callers reuse the existing console/TS ownership boundary.""" +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess +import sys + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[2] +UNRELATED_OWNERS = ( + "loopx.cli_commands.benchmark_dispatch", + "loopx.capabilities.content_ops.cli", +) + + +def run_entry( + entry: str, + argv: list[str], + *, + setup: str = "", + assertions: str = "", + env: dict[str, str] | None = None, +) -> subprocess.CompletedProcess[str]: + # runpy executes the actual module branch in a fresh interpreter. Give both + # public entries the same program name so argparse output is byte-comparable. + invocation = ( + 'runpy.run_module("loopx.cli", run_name="__main__", alter_sys=False)' + if entry == "module" + else "__import__('loopx.entrypoint', fromlist=['main']).main()" + ) + script = f""" +import runpy +import sys +sys.argv = ["loopx", *{argv!r}] +{setup} +try: + code = {invocation} +except SystemExit as exc: + code = exc.code +{assertions} +raise SystemExit(code) +""" + return subprocess.run( + [sys.executable, "-c", script], + cwd=REPO_ROOT, + env={**os.environ, "PYTHONPATH": str(REPO_ROOT), "LOOPX_USAGE_PING": "0", **(env or {})}, + text=True, + capture_output=True, + check=False, + timeout=45, + ) + + +@pytest.mark.parametrize( + "argv", + [["--version"], [], ["--help"], ["check", "--help"], ["status", "--help"], + ["diagnose", "--help"], ["review-packet", "--help"], ["quota", "--help"], + ["todo", "--help"], ["--registry=fixture", "--format=json", "quota", "--help"]], +) +def test_source_help_and_version_do_not_load_unrelated_owners(argv: list[str]) -> None: + assertions = f""" +assert "loopx.cli" not in sys.modules +for owner in {UNRELATED_OWNERS!r}: + assert owner not in sys.modules, owner +""" + if argv == ["--version"]: + assertions += '\nassert "loopx.cli_runtime" not in sys.modules\n' + module = run_entry("module", argv, assertions=assertions) + console = run_entry("console", argv, assertions=assertions) + assert module.returncode == console.returncode == 0, module.stderr + console.stderr + assert (module.stdout, module.stderr) == (console.stdout, console.stderr) + + +@pytest.mark.parametrize( + "argv", + [["version", "--format", "json"], ["status", "--unknown-option"], + ["todo", "list"], ["quota", "unknown-command"], ["--format", "unknown", "status"], + ["--reg", "fixture", "status"]], +) +def test_source_full_fallback_keeps_canonical_results_and_diagnostics(argv: list[str]) -> None: + module = run_entry("module", argv, assertions='assert "loopx.cli" in sys.modules') + console = run_entry("console", argv) + assert (module.returncode, module.stdout, module.stderr) == ( + console.returncode, console.stdout, console.stderr + ) + + +def test_imported_full_parser_remains_a_compatibility_api() -> None: + result = run_entry( + "module", ["--version"], + assertions=""" +from loopx.cli import build_parser, main +args = build_parser().parse_args(["todo", "list", "--goal-id", "fixture"]) +assert args.command == "todo" and args.todo_command == "list" +assert callable(main) +""", + ) + assert result.returncode == 0, result.stderr + + +def native_argv() -> list[str]: + return [ + "--format", "json", "--runtime-root", "fixture-runtime", "quota", + "scheduler-ack-current", "--goal-id", "fixture-goal", "--agent-id", "fixture-agent", + "--scheduler-host-facts-chunk", "fixture-facts", "--turn-instance-id", "fixture-turn", + "--execute", + ] + + +@pytest.mark.parametrize("entry", ["module", "console"]) +def test_receipt_bound_scheduler_dispatch_reuses_native_owner(entry: str) -> None: + setup = """ +import os +import shutil +shutil.which = lambda name: "/fixture/node" if name == "node" else None +seen = [] +def replace_process(executable, argv): + seen.append((executable, argv)) + raise SystemExit(73) +os.execv = replace_process +""" + result = run_entry(entry, native_argv(), setup=setup, assertions=""" +assert len(seen) == 1 +assert seen[0][0] == "/fixture/node" +assert seen[0][1][1:3] == ["--no-warnings", "--experimental-strip-types"] +from pathlib import Path +assert Path(seen[0][1][3]).parts[-2:] == ("scheduler", "heartbeat_followup_cli.ts") +assert seen[0][1][4:] == sys.argv[1:] +assert "loopx.cli_runtime" not in sys.modules +assert "loopx.cli" not in sys.modules +""") + assert result.returncode == 73, result.stdout + result.stderr + + +@pytest.mark.parametrize("entry", ["module", "console"]) +def test_native_scheduler_missing_node_fails_closed_without_python_fallback(entry: str) -> None: + result = run_entry( + entry, native_argv(), setup="import shutil; shutil.which = lambda name: None", + assertions='assert "loopx.cli_runtime" not in sys.modules', + ) + assert result.returncode == 2 + assert result.stdout == "" + assert "requires Node.js 22.22.3 or newer" in result.stderr + + +@pytest.mark.parametrize("entry", ["module", "console"]) +@pytest.mark.parametrize("argv", [native_argv(), [ + "todo", "update", "--goal-id", "fixture-goal", "--todo-id", "todo_fixture", + "--agent-id", "fixture-agent", "--status", "done", +]]) +def test_outer_controller_write_guard_runs_before_native_or_provider_effects( + tmp_path: Path, entry: str, argv: list[str], +) -> None: + runtime = tmp_path / "runtime" + args = [*argv] + if "--runtime-root" in args: + args[args.index("--runtime-root") + 1] = str(runtime) + else: + args = ["--runtime-root", str(runtime), *args] + result = run_entry( + entry, args, + setup=""" +import os +def replace_process(*args): + raise AssertionError("outer-controlled writes cannot process-replace their guard") +os.execv = replace_process +""", + assertions='assert "loopx.cli" not in sys.modules', + env={"LOOPX_KUNLUNCODE_OUTER_CONTROLLER": "1", + "LOOPX_KUNLUNCODE_OUTER_GOAL_ID": "fixture-goal", + "LOOPX_KUNLUNCODE_OUTER_AGENT_ID": "fixture-agent"}, + ) + assert result.returncode == 2, result.stdout + result.stderr + assert result.stdout == "" + payload = json.loads(result.stderr) + assert payload["error_code"] == "kunluncode_outer_controller_write_owned" + assert payload["goal_id"] == "fixture-goal" + assert not runtime.exists() + + +@pytest.mark.parametrize("entry", ["module", "console"]) +def test_outer_controller_scope_preserves_reads_other_goals_and_recovery( + tmp_path: Path, entry: str, +) -> None: + from examples.control_plane.quota_plan_fixtures import SCOPED_AGENT_ID, write_cli_fixture + from loopx.control_plane.scheduler.state import ( + APP_AUTOMATION_STATEFUL_BACKOFF_STATE_KEY as state_key, + load_scheduler_state, + ) + + registry, runtime, _project = write_cli_fixture(tmp_path / "fixture", scoped_agents=True) + prefix = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json"] + env = {"CODEX_HOME": str(tmp_path / "codex-home"), "CODEX_THREAD_ID": "", + "LOOPX_KUNLUNCODE_OUTER_CONTROLLER": "1", + "LOOPX_KUNLUNCODE_OUTER_GOAL_ID": "needs-operator", + "LOOPX_KUNLUNCODE_OUTER_AGENT_ID": SCOPED_AGENT_ID} + + def selected_ack(goal: str) -> list[str]: + decision = run_entry(entry, [*prefix, "quota", "should-run", "--goal-id", goal, + "--agent-id", SCOPED_AGENT_ID, "--codex-app", + "--turn-instance-id", "scope-turn"], env=env) + # An independent fixture without a Todo can return the ordinary health + # failure (1); it must still pass the outer write guard and commit its + # typed observation, rather than return that guard's rejection (2). + assert decision.returncode in (0, 1), decision.stdout + decision.stderr + payload = json.loads(decision.stdout) + assert payload["mode"] == "should-run" + assert payload["heartbeat_receipt"]["status"] == "committed" + return [*prefix, *payload["scheduler_hint"]["app_automation"] + ["ack_hint"]["cli_args"]] + + # A read for the controlled Goal remains allowed, but changing the actor + # does not allow its bound write to escape Goal-scoped ownership. + protected_ack = selected_ack("needs-operator") + for agent in (SCOPED_AGENT_ID, "different-agent"): + args = list(protected_ack) + args[args.index("--agent-id") + 1] = agent + blocked = run_entry(entry, args, env=env) + assert blocked.returncode == 2, blocked.stdout + blocked.stderr + assert json.loads(blocked.stderr)["error_code"] == "kunluncode_outer_controller_write_owned" + assert load_scheduler_state(runtime, goal_id="needs-operator", agent_id=SCOPED_AGENT_ID, + state_key=state_key) is None + + # Add an independent Goal after the protected scope was exercised. The + # original controller declaration must not silently become registry-wide. + registered = json.loads(registry.read_text()) + future = dict(next(row for row in registered["goals"] if row["id"] == "full-speed")) + future["id"] = "future-independent" + future["state_file"] = ".codex/goals/future-independent/ACTIVE_GOAL_STATE.md" + future_state = registry.parent.parent / future["state_file"] + future_state.parent.mkdir(parents=True) + future_state.write_text("---\nstatus: active\n---\n\n# Future independent Goal\n") + registered["goals"].append(future) + registry.write_text(json.dumps(registered)) + for goal in ("full-speed", "future-independent"): + accepted = run_entry(entry, selected_ack(goal), env=env) + assert accepted.returncode == 0, accepted.stdout + accepted.stderr + payload = json.loads(accepted.stdout) + assert payload["ok"] is True and payload["scheduler_state_mutated"] is True, payload + assert load_scheduler_state(runtime, goal_id=goal, agent_id=SCOPED_AGENT_ID, + state_key=state_key) is not None + + # Release by the existing controller owner restores real receipt-bound + # progress, not merely a different blocker or a mocked success payload. + recovered = run_entry(entry, protected_ack, + env={**env, "LOOPX_KUNLUNCODE_OUTER_CONTROLLER": "0"}) + assert recovered.returncode == 0, recovered.stdout + recovered.stderr + assert json.loads(recovered.stdout)["scheduler_state_mutated"] is True + assert load_scheduler_state(runtime, goal_id="needs-operator", agent_id=SCOPED_AGENT_ID, + state_key=state_key) is not None + + +def test_source_module_observes_usage_once_and_preserves_failure_result() -> None: + setup = """ +from loopx import usage_ping +calls = [] +usage_ping.begin = lambda command: calls.append(("begin", command)) or ("generation", 0) +def finish(ticket, command, code, error): + calls.append(("finish", command, code, type(error).__name__ if error else None)) +usage_ping.finish = finish +""" + result = run_entry("module", ["status", "--unknown-option"], setup=setup, assertions=""" +assert calls == [("begin", "status"), ("finish", "status", 2, "SystemExit")], calls +""") + assert result.returncode == 2, result.stderr + assert "unrecognized arguments" in result.stderr + + +def test_source_module_usage_opt_out_has_no_machine_state_or_sender(tmp_path: Path) -> None: + state = tmp_path / "machine" + result = run_entry("module", ["version", "--format", "json"], setup=f""" +from pathlib import Path +from loopx import usage_ping +usage_ping.DEFAULT_RUNTIME_ROOT = Path({str(state)!r}) +sent = [] +def unexpected_send(*args): + sent.append(args) +usage_ping._detach = unexpected_send +""", assertions="assert sent == [], sent") + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout)["ok"] is True + assert result.stderr == "" + assert not state.exists() + + +def test_source_first_usage_disclosure_keeps_json_pure_and_does_not_send(tmp_path: Path) -> None: + state = tmp_path / "machine" + result = run_entry("module", ["version", "--format", "json"], setup=f""" +import os +from pathlib import Path +from loopx import usage_ping +for key in ("CI", "DO_NOT_TRACK", "LOOPX_USAGE_PING", "LOOPX_USAGE_POLICY"): + os.environ.pop(key, None) +os.environ["LOOPX_USAGE_PING_ENDPOINT"] = "http://127.0.0.1:1/v1/ping" +usage_ping.DEFAULT_RUNTIME_ROOT = Path({str(state)!r}) +sent = [] +usage_ping._detach = lambda *args: sent.append(args) +""", assertions="assert sent == [], sent") + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout)["ok"] is True + assert "random installation ID" in result.stderr + stored = json.loads((state / "usage-ping.json").read_text()) + assert stored["notice"]["version"] == 3 + assert "last_attempt_day" not in stored and "counters" not in stored + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_source_read_uses_canonical_authority_not_missing_markdown( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str, +) -> None: + from canonical_authority_fixture import isolate_sqlite_runtime, promoted_create_fixture + from loopx.control_plane.coordination.local_authority import read_canonical_todos_if_promoted + from loopx.todos import add_goal_todo + + isolate_sqlite_runtime(tmp_path, monkeypatch) + registry, runtime, state = promoted_create_fixture(tmp_path, provider=provider) + created = add_goal_todo(registry_path=registry, goal_id="goal-a", role="agent", + text="Read canonical work", claimed_by="agent-a", agent_id="agent-a") + before = read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") + state.unlink() + argv = ["--registry", str(registry), "--runtime-root", str(runtime), "--format", "json", + "todo", "list", "--goal-id", "goal-a", "--agent-id", "agent-a"] + module = run_entry("module", argv, assertions='assert "loopx.cli" not in sys.modules') + console = run_entry("console", argv) + assert module.returncode == console.returncode == 0, module.stderr + console.stderr + assert json.loads(module.stdout) == json.loads(console.stdout) + result = json.loads(module.stdout) + assert result["authority_read"]["source_authority"] == f"{provider}_v0" + assert result["authority_read"]["legacy_fallback_used"] is False + assert result["authority_read"]["provider_revision"] == before["provider_revision"] + assert any(row["todo_id"] == created["todo_id"] for row in result["todos"]) + assert read_canonical_todos_if_promoted(runtime_root=runtime, goal_id="goal-a") == before + assert not state.exists()