#!/usr/bin/env python3 """Differential test of the C++ agent core against tai.py. Builds cases (curated plus seeded fuzz), computes the expected result with tai.py, feeds the same cases to build/agent_diff (tests/agent_diff.cpp) and compares the JSON results. Exit 0 only when every case matches. python3 tests/agent_diff.py [--seed N] [--fuzz N] [--bin build/agent_diff] [-v] """ import argparse import hashlib import html import json import os import random import subprocess import sys import tempfile import urllib.parse from datetime import datetime, timezone HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) # a hermetic environment: no tmux, no plexer, no tui socket, fixed zone HOME = tempfile.mkdtemp(prefix="agent-diff-home-") os.environ.pop("TMUX", None) os.environ.pop("TMUX_PANE", None) for key in list(os.environ): if key.startswith("TAI_"): del os.environ[key] os.environ["TAI_HOME"] = HOME os.environ["TZ"] = "Europe/Amsterdam" os.environ["PATH"] = "/usr/bin:/bin" for _name in ("MUSE_BIN", "OPENCODE_BIN", "GROK_BIN", "CLAUDE_BIN", "CODEX_BIN", "GEMINI_BIN"): os.environ.pop(_name, None) sys.path.insert(0, ROOT) sys.dont_write_bytecode = True import tai # noqa: E402 import types # noqa: E402 import urllib.error # noqa: E402 import urllib.request # noqa: E402 def _fail_urlopen(*args, **kwargs): raise urllib.error.URLError("fake") def py_chat_candidates(cfg, records, pins): original = urllib.request.urlopen urllib.request.urlopen = _fail_urlopen try: home = tempfile.mkdtemp(prefix="agent-diff-chat-") cfg = dict(cfg, home=home) config = types.SimpleNamespace(**cfg) chat = tai.ChatClient(config) for provider, model, ok, lat, tps in records: chat.health.record(provider, model, ok, lat, tps) chat.provider_pin = pins.get("provider") chat.model_pin = pins.get("model") def view(c): return [c["provider"], c["chat_url"], c["models_url"], c["key"], c["model"], c["profile"], c.get("runner", "")] return {"ordered": [view(c) for c in chat.ordered_candidates()], "all": [view(c) for c in chat.all_candidates()], "backends": [list(b) for b in chat.backends()], "pin_note": chat.pin_note()} finally: urllib.request.urlopen = original def dumps_sorted(value): return json.dumps(value, sort_keys=True, ensure_ascii=True) def guard(func): def run(*args): try: return func(*args) except tai.BackendError as exc: return {"backend_error": str(exc), "status": exc.status} except Exception as exc: # noqa: BLE001 return {"exception": str(exc)} return run def verdict_value(text, goal): v = tai.GoalVerdict.from_text(text) return {"ok": v.ok, "failures": [f.to_dict() for f in v.failures], "commentary": v.commentary, "raw": v.raw, "report": v.report(goal), "followup": v.followup()} def names(picked): return [s["function"]["name"] for s in picked] def int_or_none(value): try: return int(value) except (TypeError, ValueError): return None def float_or_none(value): try: return float(value) except (TypeError, ValueError): return None def schedule_now(epoch): return datetime.fromtimestamp(epoch, timezone.utc) EXPECT = { "chat_candidates": lambda cfg, records, pins: py_chat_candidates(cfg, records, pins), "json_roundtrip": guard(lambda text: json.dumps(json.loads(text))), "json_compact_sorted": guard(lambda text: json.dumps(json.loads(text), sort_keys=True, separators=(",", ":"))), "schemas": lambda: json.dumps(tai.TOOL_SCHEMAS), "select_tools": lambda text: names(tai.select_tools(text)), "select_tools_extra": lambda text, extra: names(tai.select_tools(text, extra)), "tool_catalog": lambda: tai.tool_catalog(), "truncate": lambda text, head, tail: tai.truncate(text, head, tail), "estimate_tokens": lambda value: tai.estimate_tokens(value), "normalize_tag": lambda raw: tai.normalize_tag(raw), "normalize_tags": lambda items: tai.normalize_tags(items), "singular_noun": lambda word: tai.singular_noun(word), "tag_variants": lambda tag: tai.tag_variants(tag), "parse_node_id": lambda raw: (lambda r: list(r) if r else None)(tai.parse_node_id(raw)), "secret_env_name": lambda name: tai.secret_env_name(name), "format_delay": lambda s: tai.format_delay(s), "parse_duration": lambda raw: tai.parse_duration(raw), "parse_schedule_at": guard(lambda raw: tai.parse_schedule_at(raw)), "local_display": lambda raw: tai.local_display(raw), "html_unescape": lambda text: html.unescape(text), "url_quote_plus": lambda text: urllib.parse.urlencode({"q": text})[2:], "chunk_lines": lambda text: tai.chunk_lines(text), "collapse_repeated_lines": lambda text: tai.collapse_repeated_lines(text), "tool_name_clean": lambda raw: tai.tool_name_clean(raw), "tool_args_valid": lambda raw: tai.tool_args_valid(raw), "tool_args_text": lambda raw: tai.tool_args_text(raw), "action_signature": lambda name, raw: tai.action_signature(name, raw), "result_digest": lambda text: tai.result_digest(text), "clean_history": lambda messages: tai.clean_history(messages), "conversation_tokens": lambda messages, tools=None: tai.conversation_tokens(messages, tools), "conversation_text": lambda messages: tai.conversation_text(messages), "approval_parse": lambda answer: tai.Approval.parse_allow(answer), "approval_affirm": lambda text: tai.Approval.is_affirmative(text), "goal_verdict": lambda text, goal: verdict_value(text, goal), "extract_json": lambda text: list(tai.GoalVerifier.extract_json(text)), "goal_transcript": lambda messages: tai.GoalVerifier.transcript(messages), "goal_tried": lambda messages: tai.GoalVerifier.tried_actions(messages), "goal_task": lambda goal, final, transcript, tried: tai.GoalVerifier.build_task(goal, final, transcript, tried), "muse_prompt": lambda messages, tools: tai.muse_prompt(messages, tools), "opencode_prompt": lambda messages, tools: tai.opencode_prompt(messages, tools), "opencode_parse": lambda text: tai.opencode_parse(text), "opencode_find_answer": lambda text: tai.opencode_find_answer(text), "opencode_parse_events": guard(lambda out, code, err: tai.opencode_parse_events(out, code, err)), "grok_parse_output": guard(lambda text: tai.grok_parse_output(text)), "claude_parse_output": guard(lambda text: tai.claude_parse_output(text)), "gemini_parse_output": guard(lambda text: tai.gemini_parse_output(text)), "codex_parse_output": guard(lambda out, outfile, used: tai.codex_parse_output(out, outfile, used)), "muse_valid_answer": lambda text: tai.muse_valid_answer(text), "muse_message_hash": lambda message: tai.muse_message_hash(message), "model_weight": lambda entry: tai.model_weight(entry), "model_outcome_reward": lambda lat, tps: tai.model_outcome_reward(lat, tps), "summarize_errors": lambda errors, shown, width: tai.ChatClient.summarize_errors(errors, shown, width), "split_models": lambda spec: tai.split_models(spec), "parse_backends": lambda spec: tai.parse_backends(spec), "is_free_model": lambda model: tai.is_free_model(model), "embedded_failure_message": lambda data: tai.embedded_failure_message(data), "stream_status": lambda code, output, elapsed, timed_out: tai.stream_status(code, output, elapsed, timed_out), "curl_hint": lambda command: tai.curl_hint(command), "format_schedule_lines": lambda items, now: tai.format_schedule_lines(items, schedule_now(now)), "py_repr": lambda value: repr(value), "py_str": lambda value: str(value), "py_strip": lambda text: text.strip(), "py_lower": lambda text: text.lower(), "py_splitlines": lambda text: text.splitlines(), "py_split_ws": lambda text: text.split(), "py_int": lambda value: int_or_none(value), "py_float": lambda value: float_or_none(value), "py_slice": lambda text, start, end: text[start:end], "py_len": lambda text: len(text), "float_repr": lambda value: repr(float(value)), "sha1": lambda text: hashlib.sha1(text.encode("utf-8", "surrogatepass")).hexdigest(), "sha256": lambda text: hashlib.sha256(text.encode("utf-8", "surrogatepass")).hexdigest(), "utf8_replace": lambda text: text.encode("latin-1").decode("utf-8", "replace"), "orientation_block": lambda directory, sandbox: tai.orientation_block(directory or None, sandbox), "capability_block": lambda yolo, auto: tai.capability_block(yolo, auto), "sessions_block": lambda home, profile: tai.sessions_block(home, profile), "split_leading_tags": lambda text: list(tai.split_leading_tags(text)), "expand_file_mentions": lambda text, directory: tai.expand_file_mentions(text, directory), "profile_ok": lambda s: bool(tai.PROFILE_RE.match(s)), "bot_ok": lambda s: bool(tai.BOT_RE.match(s)), "secret_ok": lambda s: bool(tai.SECRET_RE.match(s)), "skill_name_ok": lambda s: bool(tai.SKILL_NAME_RE.match(s)), "mcp_server_ok": lambda s: bool(tai.MCP_SERVER_RE.match(s)), "create_skill_prompt": lambda name, brief, skill_dir: tai.create_skill_prompt(name, brief, skill_dir), "data_texts": lambda: {"default_system": tai.DEFAULT_SYSTEM, "merge_system": tai.MERGE_SYSTEM, "compact_system": tai.COMPACT_SYSTEM, "autonomous_note": tai.AUTONOMOUS_NOTE, "legacy_password_note": tai.LEGACY_PASSWORD_NOTE, "vault_memory_note": tai.VAULT_MEMORY_NOTE, "mcp_resolver_brief": tai.MCP_RESOLVER_BRIEF, "goal_task": tai.GoalVerifier.TASK, "curl_hint": tai.CURL_HINT}, } POOL = ["a", "b", "c", "x", "Y", "Z", "0", "1", "9", " ", " ", "\t", "\n", "\r\n", "-", "_", ".", ",", ";", ":", "!", "?", "(", ")", "[", "]", "{", "}", "\"", "'", "\\", "/", "@", "#", "&", "<", ">", "=", "+", "*", "%", "é", "é", "ß", "İ", "K", " ", "
", " ", "中", "文", "\U0001f600", "€", "\x7f", "\x1c", "TODO", "token", "secret", "readme", "README", "http://x", "curl", "wget", "&", "<", "A", "A", " ", "©", "&unknown;", "ies", "ses", "news", "ss", "tags", "record"] def fuzz_text(rng, max_parts=14): return "".join(rng.choice(POOL) for _ in range(rng.randint(0, max_parts))) def fuzz_json(rng, depth=0): kind = rng.randint(0, 9 if depth < 3 else 5) if kind == 0: return None if kind == 1: return rng.choice([True, False]) if kind == 2: return rng.randint(-1000, 10 ** 12) if kind == 3: return rng.choice([0.0, 1.5, -2.25, 1e20, 1e-7, 123456.789, 0.1, 1 / 3]) if kind in (4, 5): return fuzz_text(rng, 6) if kind in (6, 7): return [fuzz_json(rng, depth + 1) for _ in range(rng.randint(0, 4))] return {fuzz_text(rng, 3): fuzz_json(rng, depth + 1) for _ in range(rng.randint(0, 4))} def fuzz_messages(rng): messages = [] for _ in range(rng.randint(0, 7)): role = rng.choice(["system", "user", "assistant", "tool"]) item = {"role": role, "content": rng.choice([fuzz_text(rng, 5), None, "", fuzz_text(rng, 30)])} if role == "assistant" and rng.random() < 0.6: calls = [] for k in range(rng.randint(1, 3)): name = rng.choice(["shell", "read_file", "bad name!", "x" * 70, "web_fetch", "", "a.b-c_d"]) args = rng.choice(['{"a": 1}', "", "not json", "{}", None, {"a": 1}, "[1, 2]"]) calls.append({"id": "c%d" % rng.randint(0, 3), "type": "function", "function": {"name": name, "arguments": args}}) item["tool_calls"] = calls if role == "tool": item["tool_call_id"] = "c%d" % rng.randint(0, 3) messages.append(item) return messages PROVIDERS = ["lab", "devplace", "openrouter", "pollinations", "zen", "ollama", "muse", "opencode", "other"] MODELS = ["m1", "m2", "molodetz", "free", "muse-spark-x", "a:free", "b-free", "openai-fast", "deepseek/deepseek-v4.1-flash", "z"] def fuzz_config(rng): pick = lambda n: rng.sample(MODELS, rng.randint(0, n)) # noqa: E731 backends = [] for i in range(rng.randint(0, 3)): backends.append({"label": rng.choice(["lab", "devplace", "Acme", "other"]), "base": "http://h%d/v1" % i, "key": rng.choice(["", "k"]), "model": ",".join(pick(2)), "profile": rng.choice(["", "opencode"])}) return {"model": rng.choice(["muse", "m1"]), "explicit_model": rng.choice(["", "m2", "xx"]), "backends": backends, "openrouter_key": rng.choice(["", "key"]), "openrouter_models": pick(3), "retry_seconds": 0, "opencode_enabled": rng.random() < 0.5, "opencode_models": pick(2), "devplace_key": rng.choice(["", "dk"]), "devplace_base": "https://devplace.net/openai/v1", "devplace_models": pick(2), "openrouter_enabled": rng.random() < 0.7, "pollinations_enabled": rng.random() < 0.7, "pollinations_models": pick(2), "zen_enabled": rng.random() < 0.4, "zen_models": pick(3), "ollama_enabled": rng.random() < 0.6, "ollama_models": pick(2)} def fuzz_records(rng): out = [] for _ in range(rng.randint(0, 12)): out.append([rng.choice(PROVIDERS), rng.choice(MODELS), rng.random() < 0.6, rng.choice([None, 100.0, 5000.5]), rng.choice([None, 10.0, 80.0])]) return out def build_cases(rng, fuzz): cases = [] def add(fn, *args): cases.append({"fn": fn, "args": list(args)}) for _ in range(max(40, fuzz // 3)): pins = rng.choice([{}, {}, {"provider": rng.choice(PROVIDERS)}, {"model": rng.choice(MODELS)}, {"provider": rng.choice(PROVIDERS), "model": rng.choice(MODELS)}]) add("chat_candidates", fuzz_config(rng), fuzz_records(rng), pins) add("schemas") add("tool_catalog") add("data_texts") prompts = ["", "hello", "please run the tests and fix the failing build", "search the web for rust news", "read the file readme", "schedule a reminder every hour", "remember my favorite color", "store a password in the vault", "delete the old file", "describe this image screenshot", "fork a subagent to research in parallel", "install the skill from github", "mcp server list", "audit changes since yesterday", "release a new version", "create a bot persona", "graph link nodes", "tail terminals tmux panes", "what is the system info of this machine?", "translate this poem", "SEARCH the WEB", "Kelvin K sign and İstanbul", "tag tags tagging", "ask the user a question and wait"] for p in prompts: add("select_tools", p) for _ in range(fuzz): add("select_tools", fuzz_text(rng, 25)) extra = [{"type": "function", "function": {"name": "mcp_demo_weather", "description": "Fetch the weather forecast for a city", "parameters": {}}}, {"type": "function", "function": {"name": "mcp_demo_list", "description": "short", "parameters": {}}}] for p in ["weather forecast please", "list things", "forecast", ""]: add("select_tools_extra", p, extra) for _ in range(fuzz): t = fuzz_text(rng, 40) add("truncate", t * rng.randint(1, 120), rng.choice([10, 100, 1500]), rng.choice([5, 50, 500])) add("estimate_tokens", rng.choice([t, None, "", [1, 2, 3, 4, 5]])) add("normalize_tag", t) add("normalize_tags", [fuzz_text(rng, 4) for _ in range(rng.randint(0, 25))]) add("singular_noun", fuzz_text(rng, 3)) add("tag_variants", fuzz_text(rng, 3)) add("secret_env_name", t) add("html_unescape", t) add("url_quote_plus", t) add("chunk_lines", "\n".join(fuzz_text(rng, 30) for _ in range(rng.randint(0, 200)))) add("collapse_repeated_lines", "\n".join(rng.choice(["a", "a", "a", "b", fuzz_text(rng, 2)]) for _ in range(rng.randint(0, 30)))) add("tool_name_clean", rng.choice([t, None, "shell<|channel|>commentary", " x "])) add("tool_args_valid", rng.choice([t, None, "{}", '{"a":1}', {"a": 1}, "", "[1]"])) add("tool_args_text", rng.choice([t, None, "", {"a": [1, "é"]}, ["x"], 0, 5])) add("action_signature", rng.choice(["shell", "read_file"]), rng.choice(['{"b":1,"a":2}', "nope", None, "", {"z": 1, "a": 2}])) add("result_digest", t) msgs = fuzz_messages(rng) add("clean_history", msgs) add("conversation_tokens", msgs) add("conversation_text", msgs) add("goal_transcript", msgs) add("goal_tried", msgs) add("approval_parse", t) add("approval_affirm", rng.choice([t, "Yes!", " go ahead. ", "LOOKS good", "yes, but no", "ok.?"])) add("py_repr", fuzz_json(rng)) add("py_str", fuzz_json(rng)) add("py_strip", t) add("py_lower", t) add("py_splitlines", t) add("py_split_ws", t) add("py_int", rng.choice([fuzz_json(rng), "12", " 7 ", "1_0", "0x10", "1e3", "-5", "+3", "", "٣"])) add("py_float", rng.choice([fuzz_json(rng), "1.5", " 2 ", "1e3", "nan", "inf", "-Infinity", "1_0.5", "0x1p3", "."])) add("py_slice", t, rng.choice([None, -3, 0, 2, 50]), rng.choice([None, -1, 0, 4, 99])) add("py_len", t) add("float_repr", rng.choice([0.1, 1e16, 1e-5, 123456789.123456789, 5e-324, 1.7976931348623157e308, 100.0, 1 / 3, 2.5e22])) add("sha1", t) add("sha256", t) add("split_leading_tags", rng.choice(["@a b", "@a @b rest", "@@x", "plain", "@ x", "@x y", "@a\n@b\tc " + t])) add("profile_ok", rng.choice([t, "abc", "abc\n", "a" * 32, "a" * 33, "-x", "a_b-c"])) add("bot_ok", rng.choice([t, "abc", "ABC", "a-b_c"])) add("secret_ok", rng.choice([t, "a.b-c", "a" * 64, "a" * 65])) add("skill_name_ok", rng.choice([t, "ab", "a-b", "a-", "-a", "a" * 64, "a" * 65, "x\n"])) add("mcp_server_ok", rng.choice([t, "ok.name", "a" * 64])) add("json_roundtrip", json.dumps(fuzz_json(rng), ensure_ascii=rng.random() < 0.5)) add("json_compact_sorted", json.dumps(fuzz_json(rng))) add("model_outcome_reward", rng.choice([None, 0, 120.5, 8000]), rng.choice([None, 0, 12.5, 100])) add("model_weight", rng.choice([{}, {"s": 3, "f": 1, "r": 2.5}, {"s": 0, "f": 0}, {"r": 1.0, "s": None, "f": 2}])) for raw in ["", "{", "[1,]", '{"a":1,}', '{"a":1 "b":2}', '{"a" 1}', '"abc', '"a\\q"', '"\\u12"', "tru", "nan", "NaN", "-", "1e", "[1 2]", "[1,2] x", '{"a":1}\n', "\t[ ]", '"a\tb"', '{"é": "ü\\ud83d\\ude00"}', '"\\ud800"', "1.5e300", "1" * 30, "-0", "-0.0", "[" * 5 + "]" * 5, "{}", '{"a":{"b":[1,2,{"c":null}]}}', '{"a":1,"a":2}', "1.", ".5", "01", "+1"]: add("json_roundtrip", raw) add("json_compact_sorted", raw) for raw in ["", "2026-10-08T09:00", "2026-10-08 09:00:15.5", "2026-10-08T09:00Z", "2026-10-08T09:00+02:00", "2026-10-08T09:00-0530", "2026-10-08", "20261008", "2026-13-01", "2026-02-30", "garbage", "2026-10-08T25:00", "2026-03-29T02:30", "2026-10-25T02:30", "2026-10-08T09:00:00.123456789", " 2026-10-08T09:00 ", "2026-10-08t09:00", "2026-10-08T09", "2026-10-08T09:00+02"]: add("parse_schedule_at", raw) add("local_display", raw) for s in [-5, 0, 59, 60, 3599, 3600, 86399, 86400, 10 ** 7]: add("format_delay", s) for raw in ["", "5", "5s", "10 m", "3h", "2d", "1w", "x", "5x", "1.5h", " 7D ", "99999999999999999999"]: add("parse_duration", raw) items = [{"id": 1, "status": "pending", "every": 0, "next_run": "2026-10-25T12:00:00+00:00", "tags": ["a", "b"], "name": "x", "prompt": "p" * 100, "last": "done", "result": "r" * 200}, {"id": 2, "status": "pending", "every": 3600, "next_run": "bad", "tags": [], "name": "", "prompt": "q", "last": "", "result": ""}] add("format_schedule_lines", items, 1792924800.0) add("format_schedule_lines", [], 1792924800.0) for raw in ["&<>"'", "¬it;", "∉", "�", "€", "€", "�", "�", "�", "&", "&x", "a&b", "&;", "&#;", "&#x;", "&#xzz;", "&", "≂̸", "♥&hearts", "€", "A", "A", " x&NBSP;", "&" * 40]: add("html_unescape", raw) for raw in ["hello world", "a+b=c&d", "é中\U0001f600", "~_.-", "100%"]: add("url_quote_plus", raw) for raw in ["", "{}", '```json\n{"success": true, "failures": []}\n```', '{"success": true, "failures": [{"item": "a", "reason": "b", "proposal": "c"}]}', '{"success": false, "failures": [{"item": "a"}]}', '{"success": false, "failures": []}', '{"success": "yes", "failures": []}', '{"success": true, "failures": [], "commentary": " fine "}', "no json", '{"success": true,', "[1, 2]", 'text {"success": false, "failures": [{"item": "i", "reason": "r", "proposal": "p"}], "commentary": "c"} tail', '{"success": false, "failures": ' + json.dumps([{"item": str(i), "reason": "r", "proposal": "p"} for i in range(12)]) + "}", '{"success": true, "failures": "none"}', '{"success": null}', "```\n{}\n```", "```JSON {\"success\": true, \"failures\": []}"]: add("goal_verdict", raw, " fix the\tbug ") add("extract_json", raw) add("goal_task", "do x", "done", "[user] hi", ["a", "b"]) add("goal_task", "", "", "", []) sample_msgs = [{"role": "system", "content": "s"}, {"role": "user", "content": "do thing\nplease"}, {"role": "assistant", "content": "ok", "tool_calls": [{"id": "1", "type": "function", "function": {"name": "shell", "arguments": '{"command": "ls"}'}}]}, {"role": "tool", "tool_call_id": "1", "content": "out"}, {"role": "user", "content": [{"type": "text", "text": "part one"}, {"type": "text", "text": "part two"}, "x"]}] tools = [{"type": "function", "function": {"name": "t", "description": "d", "parameters": {"type": "object", "properties": {}, "required": []}}}] add("muse_prompt", sample_msgs, tools) add("muse_prompt", sample_msgs, None) add("muse_prompt", [], []) add("opencode_prompt", sample_msgs, tools) for raw in ["", "plain answer", '{"content": "hi", "tool_calls": []}', '```json\n{"content": "x", "tool_calls": [{"name": "a", "arguments": "{}"}]}\n```', 'noise {"schema": 1} then {"content": "late", "tool_calls": []} tail', '{"tool_calls": []}', '{"nothing": 1}', "{{{", '```\n```', "```json\n```", "``````", "```json{\"content\":\"z\"}```", '{"content": "a"} {"content": "b"}']: add("opencode_parse", raw) add("opencode_find_answer", raw) add("grok_parse_output", raw) add("claude_parse_output", raw) add("gemini_parse_output", raw) add("codex_parse_output", raw, "", False) add("codex_parse_output", "", raw, True) add("codex_parse_output", raw, raw, False) add("muse_valid_answer", raw) for obj in [{"is_error": True, "result": "bad"}, {"error": "x"}, {"error": {"a": 1}}, {"result": " answer "}, {"text": "t"}, {"structured_output": {"content": "c", "tool_calls": []}}, {"response": '{"content": "r", "tool_calls": []}'}, {"content": "c", "tool_calls": []}, {"output": "o"}, [1], 5, {"is_error": False, "result": 5}]: raw = json.dumps(obj) add("grok_parse_output", raw) add("claude_parse_output", raw) add("gemini_parse_output", raw) add("muse_valid_answer", raw) add("codex_parse_output", '{"type": "item.completed", "item": {"type": "agent_message", "text": "{\\"content\\": \\"q\\", \\"tool_calls\\": []}"}}\nnoise', "", False) add("codex_parse_output", '{"type": "turn.failed", "message": "boom"}', "", False) add("codex_parse_output", '{"type": "error", "error": {"message": "deep"}}', "", False) add("codex_parse_output", '{"type": "error", "error": {"nomsg": 1}}', "", False) add("opencode_parse_events", '{"type": "text", "part": {"text": "hello "}}\n{"type": "text", "part": {"text": "world"}}\nnot json\n', 0, "") add("opencode_parse_events", '{"type": "error", "error": {"data": {"message": "m", "statusCode": 429}}}', 1, "") add("opencode_parse_events", '{"type": "error", "error": {"name": "X"}}', 1, "") add("opencode_parse_events", "", 3, "stderr text " * 40) add("opencode_parse_events", "out", 3, "") add("opencode_parse_events", '[1]\n{"type": "text"}\n', 0, "e") for ident, errs in enumerate([[], ["a/b: x"], ["a: CLI not found", "b: err1", "b: err1", "c: err2", "d: err3"], ["muse: muse CLI not found"], ["x"] * 3 + ["y" * 500]]): add("summarize_errors", errs, 2, 320) add("summarize_errors", errs, 6, 900) for spec in ["a,b\nc, ,d", "", " x "]: add("split_models", spec) add("parse_backends", "lab|https://x/v1/|key|m1,m2|opencode;bad|x\nl2|http://h|k2") add("parse_backends", "a|b|$NOPE_NOT_SET") add("parse_backends", "") for model in ["a:free", "x-FREE", "Model-free", "paid", "", "free"]: add("is_free_model", model) for d in [{"error": {"message": " m "}}, {"error": {"type": "t"}}, {"error": " s "}, {"type": "error", "message": "mm"}, {"type": "error"}, {"ok": 1}, [1], {"error": {}}]: add("embedded_failure_message", d) for args in [(0, "a\nb\n", 1.234, False), (1, "", 0.05, False), (0, "x" * 2000, 12.55, True), (124, "ü\r\n", 3.25, False)]: add("stream_status", *args) for command in ["curl http://x", "wget https://y", "curl localhost", "xcurl http://x", "echo curl_http", "WGET HTTP://X", "curl-x http://z"]: add("curl_hint", command) add("create_skill_prompt", "n", "brief\nmore", "/tmp/skills/n") add("capability_block", False, False) add("capability_block", True, False) add("capability_block", False, True) add("orientation_block", "", True) add("orientation_block", HOME, False) add("orientation_block", "/nonexistent/dir/../x", False) add("sessions_block", HOME, "default") add("sessions_block", HOME + "/", "work") add("sessions_block", "/nonexistent", "t") workdir = tempfile.mkdtemp(prefix="agent-diff-work-") with open(os.path.join(workdir, "a.txt"), "w") as handle: handle.write("x") for text in ["see @a.txt, and @missing.txt!", "@a.txt)", "mail me@a.txt", "@", "@@a.txt", "x @%s/a.txt y" % workdir, "@~", "no mentions", "@a.txt @a.txt"]: add("expand_file_mentions", text, workdir) for raw in ["\xc3\xa9", "\xff\xfe", "\xe2\x82", "\xe2\x82\xac", "\xed\xa0\x80", "\xf0\x9f\x98", "\xf0\x9f\x98\x80", "\xc0\x80", "\xf4\x90\x80\x80", "ok", "\xe0\x80\x80\xe0\xa0\x80", "\x80\x80"]: add("utf8_replace", raw) for value in [None, True, 5, 2.5, "it's", 'say "hi"', "both ' and \"", "tab\there", "\x00\x1f\x7f", "é ​\U0001f600", [], {}, [1, "a", None, {"k": [True]}], {"a": {"b": 1.0}}, "\ud800"]: add("py_repr", value) add("py_str", value) return cases def main(): parser = argparse.ArgumentParser() parser.add_argument("--seed", type=int, default=1) parser.add_argument("--fuzz", type=int, default=150) parser.add_argument("--bin", default=os.path.join(HERE, "..", "build", "agent_diff")) parser.add_argument("-v", "--verbose", action="store_true") args = parser.parse_args() rng = random.Random(args.seed) cases = build_cases(rng, args.fuzz) payload = "\n".join(json.dumps(case) for case in cases) + "\n" env = dict(os.environ) done = subprocess.run([args.bin], input=payload.encode("utf-8", "surrogatepass"), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) if done.returncode != 0: sys.stderr.write("binary failed (%s): %s\n" % (done.returncode, done.stderr.decode("utf-8", "replace")[-2000:])) return 2 outputs = done.stdout.decode("utf-8").splitlines() if len(outputs) != len(cases): sys.stderr.write("expected %d result lines, got %d\n" % (len(cases), len(outputs))) return 2 failures = 0 per_fn = {} for case, line in zip(cases, outputs): expected_fn = EXPECT[case["fn"]] try: expected = expected_fn(*case["args"]) except Exception as exc: # noqa: BLE001 expected = {"exception": str(exc)} actual = json.loads(line) # documented limit: int() results beyond int64 clamp in C++ (Python ints are unbounded) if case["fn"] in ("py_int", "parse_duration") and isinstance(expected, int) and not -2 ** 63 <= expected < 2 ** 63: expected = 2 ** 63 - 1 if expected > 0 else -2 ** 63 # normalize through JSON so tuples/ints compare like the C++ side expected_norm = json.loads(json.dumps(expected)) stats = per_fn.setdefault(case["fn"], [0, 0]) stats[0] += 1 if dumps_sorted(expected_norm) != dumps_sorted(actual): failures += 1 stats[1] += 1 if failures <= 40 or args.verbose: sys.stdout.write("MISMATCH %s %s\n expected: %s\n actual: %s\n" % ( case["fn"], json.dumps(case["args"], ensure_ascii=True)[:300], dumps_sorted(expected_norm)[:400], dumps_sorted(actual)[:400])) sys.stdout.write("%d cases, %d mismatches\n" % (len(cases), failures)) for fn in sorted(per_fn): total, bad = per_fn[fn] if bad: sys.stdout.write(" %-26s %4d cases %4d mismatches\n" % (fn, total, bad)) return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())