// ── test_bridge_serialization.el ────────────────────────────────────────────── // // Tests for PR #20 fix/bridge-save-serialization: // - bridge_save raw JSON serialization (BLOCKER 1 & 2 regression guards) // - agentic_resume error-path handling // - Legacy fallback: old string-escaped fields still readable // - Corrupt/missing bridge state error envelope // - Empty messages/tools_json guard in bridge_save // // What CANNOT be tested here without a live Anthropic API: // - agentic_resume golden-path (calls agentic_loop which hits the API) // - Full save/resume round-trip with a real tool_result // // To run: // elc chat.el && ./soul --test tests/test_bridge_serialization.el // // ────────────────────────────────────────────────────────────────────────────── import "../chat.el" // ── Test harness ────────────────────────────────────────────────────────────── let pass_count: Int = 0 let fail_count: Int = 0 fn assert_eq(label: String, got: String, expected: String) -> Void { if str_eq(got, expected) { let pass_count = pass_count + 1 println(" PASS: " + label) } else { let fail_count = fail_count + 1 println(" FAIL: " + label) println(" got: " + got) println(" expected: " + expected) } } fn assert_true(label: String, cond: Bool) -> Void { if cond { let pass_count = pass_count + 1 println(" PASS: " + label) } else { let fail_count = fail_count + 1 println(" FAIL: " + label) } } fn assert_false(label: String, cond: Bool) -> Void { assert_true(label, !cond) } fn assert_contains(label: String, haystack: String, needle: String) -> Void { if str_contains(haystack, needle) { let pass_count = pass_count + 1 println(" PASS: " + label) } else { let fail_count = fail_count + 1 println(" FAIL: " + label) println(" missing '" + needle + "' in: " + haystack) } } fn assert_not_contains(label: String, haystack: String, needle: String) -> Void { if str_contains(haystack, needle) { let fail_count = fail_count + 1 println(" FAIL: " + label) println(" unexpected '" + needle + "' found in: " + haystack) } else { let pass_count = pass_count + 1 println(" PASS: " + label) } } fn assert_not_empty(label: String, s: String) -> Void { if str_eq(s, "") { let fail_count = fail_count + 1 println(" FAIL: " + label + " (got empty string)") } else { let pass_count = pass_count + 1 println(" PASS: " + label) } } // ── Section 1: bridge_save — empty messages guard ───────────────────────────── // // BLOCKER 2 regression guard: bridge_save must refuse to write a blob when // messages or tools_json is empty, as the resulting JSON would be syntactically // invalid (bare colon with no value). println("") println("1. bridge_save — empty messages guard") let sid1: String = "test-session-empty-messages" state_set("mcp_bridge:" + sid1, "") let save1_ok: Bool = bridge_save(sid1, "claude-sonnet-4-5", "sys", "[]", "", "", "call-1", "anthropic") assert_false("empty messages -> bridge_save returns false", save1_ok) let saved1: String = state_get("mcp_bridge:" + sid1) assert_eq("empty messages -> no blob written to state", saved1, "") // ── Section 2: bridge_save — empty tools_json guard ─────────────────────────── println("") println("2. bridge_save — empty tools_json guard") let sid2: String = "test-session-empty-tools" state_set("mcp_bridge:" + sid2, "") let save2_ok: Bool = bridge_save(sid2, "claude-sonnet-4-5", "sys", "", "[{\"role\":\"user\",\"content\":\"hi\"}]", "", "call-2", "anthropic") assert_false("empty tools_json -> bridge_save returns false", save2_ok) let saved2: String = state_get("mcp_bridge:" + sid2) assert_eq("empty tools_json -> no blob written to state", saved2, "") // ── Section 3: bridge_save — golden path writes raw JSON fields ─────────────── // // Verifies that messages_raw and tools_raw are stored as inline JSON (not // string-escaped) so that json_get_raw retrieves them without corruption. println("") println("3. bridge_save — golden path writes messages_raw and tools_raw as raw JSON") let sid3: String = "test-session-golden" state_set("mcp_bridge:" + sid3, "") let msgs3: String = "[{\"role\":\"user\",\"content\":\"hello\"}]" let tools3: String = "[{\"name\":\"read_file\"}]" let save3_ok: Bool = bridge_save(sid3, "claude-sonnet-4-5", "You are a helper.", tools3, msgs3, "read_file", "toolu_abc", "anthropic") assert_true("valid args -> bridge_save returns true", save3_ok) let blob3: String = state_get("mcp_bridge:" + sid3) assert_not_empty("valid args -> blob written to state", blob3) // messages_raw should be stored as a raw JSON array (not a quoted string) // so json_get_raw on the blob returns the array directly let raw_msgs3: String = json_get_raw(blob3, "messages_raw") assert_contains("messages_raw field present in blob", blob3, "messages_raw") assert_eq("messages_raw round-trips without corruption", raw_msgs3, msgs3) let raw_tools3: String = json_get_raw(blob3, "tools_raw") assert_eq("tools_raw round-trips without corruption", raw_tools3, tools3) // Scalar fields should still be present as normal string-escaped JSON fields let model3: String = json_get(blob3, "model") assert_eq("model field preserved in blob", model3, "claude-sonnet-4-5") let tool_use_id3: String = json_get(blob3, "tool_use_id") assert_eq("tool_use_id field preserved in blob", tool_use_id3, "toolu_abc") // Verify the blob does NOT contain old-style double-escaped fields assert_not_contains("no legacy 'messages' string field in new-format blob", blob3, "\"messages\":\"") assert_not_contains("no legacy 'tools_json' string field in new-format blob", blob3, "\"tools_json\":\"") // ── Section 4: agentic_resume — unknown session_id returns error envelope ────── println("") println("4. agentic_resume — unknown session_id (empty state)") let sid4: String = "test-session-unknown-xyzzy" state_set("mcp_bridge:" + sid4, "") let resume4: String = agentic_resume(sid4, "toolu_xyz", "some result") assert_contains("unknown session_id -> error field present", resume4, "\"error\"") assert_contains("unknown session_id -> reply field present", resume4, "\"reply\"") assert_contains("unknown session_id -> 'unknown session_id' message", resume4, "unknown session_id") let reply4: String = json_get(resume4, "reply") assert_eq("unknown session_id -> reply is empty string", reply4, "") // ── Section 5: agentic_resume — syntactically invalid JSON in state ─────────── println("") println("5. agentic_resume — syntactically invalid JSON blob in state") let sid5: String = "test-session-corrupt-json" // Write a non-JSON value that state_get would return as-is state_set("mcp_bridge:" + sid5, "NOT_JSON_AT_ALL") let resume5: String = agentic_resume(sid5, "toolu_xyz", "some result") // The function may take multiple paths here; in all cases it must not crash and // must return a JSON envelope with at least an error or empty reply field. // When json_get_raw returns "" on unparseable input, the guard catches it. assert_contains("corrupt JSON blob -> resume returns JSON", resume5, "\"reply\"") // ── Section 6: agentic_resume — blob with no messages produces error envelope ─ println("") println("6. agentic_resume — blob missing messages_raw and messages fields") let sid6: String = "test-session-no-messages" // Blob with only model/safe_sys — no messages or tools state_set("mcp_bridge:" + sid6, "{\"model\":\"claude-sonnet-4-5\",\"safe_sys\":\"sys\",\"tool_use_id\":\"toolu_abc\"}") let resume6: String = agentic_resume(sid6, "toolu_abc", "result") assert_contains("missing messages -> error field present", resume6, "\"error\"") assert_contains("missing messages -> error mentions corrupt state", resume6, "corrupt bridge state") let reply6: String = json_get(resume6, "reply") assert_eq("missing messages -> reply is empty string", reply6, "") // ── Section 7: Legacy fallback — old-format blob (string-escaped fields) ────── // // BLOCKER 1 regression guard: sessions saved before the fix used 'messages' // and 'tools_json' as string-escaped fields. The fallback path in agentic_resume // must read them correctly. We verify the fallback resolves the correct values // before the function reaches the api call (which we cannot make in tests). // // We test the fallback by writing a legacy blob and verifying that // agentic_resume does NOT return the "corrupt bridge state" error // (which would mean the fallback is broken), instead it gets past the guard // and then fails on the API call (outside our test scope). // // NOTE: We cannot confirm a successful API-dependent round-trip in this test; // the goal is only to confirm the state-reading fallback path resolves values. println("") println("7. Legacy fallback — old-format blob with string-escaped 'messages' field") let sid7: String = "test-session-legacy-format" // Simulate an old-format blob: messages and tools_json as json_safe-escaped strings. // json_safe escapes " to \" so the stored value is a JSON string containing the array. let legacy_msgs: String = "[{\"role\":\"user\",\"content\":\"legacy hello\"}]" let legacy_tools: String = "[{\"name\":\"read_file\"}]" // Build the blob the OLD way: string-escaped let safe_msgs: String = json_safe(legacy_msgs) let safe_tools: String = json_safe(legacy_tools) let legacy_blob: String = "{\"model\":\"claude-sonnet-4-5\",\"safe_sys\":\"sys\",\"messages\":\"" + safe_msgs + "\",\"tools_json\":\"" + safe_tools + "\",\"tool_use_id\":\"toolu_legacy\"}" state_set("mcp_bridge:" + sid7, legacy_blob) let resume7: String = agentic_resume(sid7, "toolu_legacy", "legacy result") // The fallback should successfully read the fields and NOT return "corrupt bridge state" assert_not_contains("legacy blob -> no 'corrupt bridge state' error (fallback working)", resume7, "corrupt bridge state") // It will fail on API call in test env, but should get past the state-reading guard // Accept "unknown session_id" NOT happening - the blob was found, just API fails // ── Section 8: bridge_save with tool_use_id containing special chars ────────── println("") println("8. bridge_save — tool_use_id with JSON-special characters is escaped") let sid8: String = "test-session-special-chars" state_set("mcp_bridge:" + sid8, "") let special_id: String = "toolu_test\"quoted\"" let msgs8: String = "[{\"role\":\"user\",\"content\":\"hi\"}]" let tools8: String = "[{\"name\":\"read_file\"}]" let save8_ok: Bool = bridge_save(sid8, "claude-sonnet-4-5", "sys", tools8, msgs8, "", special_id, "anthropic") assert_true("special chars in tool_use_id -> bridge_save returns true", save8_ok) let blob8: String = state_get("mcp_bridge:" + sid8) // The blob must be parseable (json_get succeeds on it) let retrieved_id: String = json_get(blob8, "tool_use_id") assert_eq("tool_use_id with quotes round-trips via json_safe", retrieved_id, special_id) // ── Section 9: the "wire" field (OpenAI-tools port, 2026-08-06) ─────────────── // // A suspended turn must resume on the SAME wire format it suspended on: an OpenAI-lane // bridge answered with an Anthropic-shaped tool_result (or vice versa) is a dead run. // bridge_save therefore stamps the blob with "wire", and agentic_resume branches on it. // // §9c is the important one. json_get is a first-substring-match scanner, so any key that // appears inside the UNESCAPED conversation embedded in messages_raw can be matched // instead of the blob's own field — that exact class of bug produced the round-9 resume // failure (json_get(blob,"tool_use_id") matching a web_search_tool_result's id inside the // replayed conversation). "wire" is written as a json_safe'd SCALAR ahead of both raw // fields precisely so a decoy in model-controlled bytes can never win. This test plants // that decoy on purpose. If someone later moves the field after messages_raw, this fails. println("") println("9. bridge_save — wire tagging and its field-order guarantee") // 9a. an OpenAI-lane suspension round-trips as "openai" let sid9: String = "test-session-wire-openai" state_set("mcp_bridge:" + sid9, "") let msgs9: String = "[{\"role\":\"user\",\"content\":\"hi\"}]" let tools9: String = "[{\"name\":\"read_file\"}]" let save9_ok: Bool = bridge_save(sid9, "llama-3.3-70b-versatile", "sys", tools9, msgs9, "", "call_abc", "openai") assert_true("openai wire -> bridge_save returns true", save9_ok) let blob9: String = state_get("mcp_bridge:" + sid9) assert_eq("wire round-trips as openai", json_get(blob9, "wire"), "openai") // 9b. an Anthropic-lane suspension round-trips as "anthropic" let sid9b: String = "test-session-wire-anthropic" state_set("mcp_bridge:" + sid9b, "") let save9b_ok: Bool = bridge_save(sid9b, "claude-sonnet-4-5", "sys", tools9, msgs9, "", "toolu_abc", "anthropic") assert_true("anthropic wire -> bridge_save returns true", save9b_ok) let blob9b: String = state_get("mcp_bridge:" + sid9b) assert_eq("wire round-trips as anthropic", json_get(blob9b, "wire"), "anthropic") // 9c. FIELD-ORDER GUARD: a decoy "wire" inside the conversation must NOT be matched. let sid9c: String = "test-session-wire-decoy" state_set("mcp_bridge:" + sid9c, "") let msgs9c: String = "[{\"role\":\"user\",\"content\":\"please save this literal text: \\\"wire\\\":\\\"anthropic\\\" end\"}]" let save9c_ok: Bool = bridge_save(sid9c, "llama-3.3-70b-versatile", "sys", tools9, msgs9c, "", "call_decoy", "openai") assert_true("decoy conversation -> bridge_save returns true", save9c_ok) let blob9c: String = state_get("mcp_bridge:" + sid9c) assert_eq("blob's own wire wins over a decoy planted in messages_raw", json_get(blob9c, "wire"), "openai") // 9d. LEGACY blob (written before the port) has no wire field: json_get yields "", // which agentic_resume treats as the Anthropic path — old suspensions still resume. let sid9d: String = "test-session-wire-legacy" let legacy_blob: String = "{\"model\":\"claude-sonnet-4-5\",\"safe_sys\":\"sys\",\"tools_log\":\"\"" + ",\"tool_use_id\":\"toolu_legacy\",\"tools_raw\":[{\"name\":\"read_file\"}]" + ",\"messages_raw\":[{\"role\":\"user\",\"content\":\"hi\"}]}" state_set("mcp_bridge:" + sid9d, legacy_blob) let blob9d: String = state_get("mcp_bridge:" + sid9d) assert_eq("legacy blob has no wire field -> empty (resumes as anthropic)", json_get(blob9d, "wire"), "") assert_eq("legacy blob still reads its tool_use_id", json_get(blob9d, "tool_use_id"), "toolu_legacy") // 9e. THE HARDER DECOY: a LEGACY blob (no wire field of its own) that carries the bytes // of a wire tag deeper inside, where an unbounded first-match scan would find it and // misroute the resume onto the wrong loop — the round-9 defect class exactly. // // WHAT IS AND IS NOT REACHABLE (measured here, not assumed — an earlier version of this // test asserted the wrong thing and was corrected by running it): // * NOT reachable from ordinary conversation TEXT. Any quote a user or model writes is // backslash-escaped when it is serialized into the blob, so prose containing // "wire":"openai" is stored as \"wire\":\"openai\" and does not match a scan for the // unescaped key. §9f pins that. // * REACHABLE from STRUCTURAL keys, which are embedded raw. Conversation and tool // objects keep real quotes — that is precisely how round 9's scan found a // web_search_tool_result's tool_use_id. A connector-supplied tool schema or a future // message field literally named "wire" would be found the same way. // The bound removes the whole class rather than reasoning about which keys exist today. let sid9e: String = "test-session-wire-legacy-decoy" let decoy_blob: String = "{\"model\":\"claude-sonnet-4-5\",\"safe_sys\":\"sys\",\"tools_log\":\"\"" + ",\"tool_use_id\":\"toolu_legacy\"" + ",\"tools_raw\":[{\"name\":\"read_file\",\"wire\":\"openai\"}]" + ",\"messages_raw\":[{\"role\":\"user\",\"content\":\"hi\"}]}" state_set("mcp_bridge:" + sid9e, decoy_blob) let blob9e: String = state_get("mcp_bridge:" + sid9e) // Unbounded read (what NOT to do) — proves the hazard this guard exists for is real. assert_eq("unbounded scan DOES find a structural decoy (why the bound is needed)", json_get(blob9e, "wire"), "openai") // Bounded read — the same computation agentic_resume performs. let d_traw: Int = str_index_of(blob9e, ",\"tools_raw\":") let d_tjson: Int = str_index_of(blob9e, ",\"tools_json\":") let d_mraw: Int = str_index_of(blob9e, ",\"messages_raw\":") let d_msgs: Int = str_index_of(blob9e, ",\"messages\":") let dcut1: Int = if d_traw > 0 { d_traw } else { str_len(blob9e) } let dcut2: Int = if d_tjson > 0 && d_tjson < dcut1 { d_tjson } else { dcut1 } let dcut3: Int = if d_mraw > 0 && d_mraw < dcut2 { d_mraw } else { dcut2 } let dcut: Int = if d_msgs > 0 && d_msgs < dcut3 { d_msgs } else { dcut3 } let head9e: String = str_slice(blob9e, 0, dcut) assert_eq("bounded scan ignores the decoy -> legacy blob resumes as anthropic", json_get(head9e, "wire"), "") assert_not_contains("scalar head excludes the bulk fields entirely", head9e, "read_file") // 9f. Escaping bounds the severity: prose CANNOT inject a scalar-looking key, because // its quotes are escaped on the way in. Documented as a measured fact, so nobody has to // re-derive it the next time this question comes up. let sid9f: String = "test-session-wire-prose" let prose_blob: String = "{\"model\":\"claude-sonnet-4-5\",\"safe_sys\":\"sys\",\"tools_log\":\"\"" + ",\"tool_use_id\":\"toolu_legacy\",\"tools_raw\":[{\"name\":\"read_file\"}]" + ",\"messages_raw\":[{\"role\":\"user\",\"content\":\"remember this: \\\"wire\\\":\\\"openai\\\"\"}]}" state_set("mcp_bridge:" + sid9f, prose_blob) let blob9f: String = state_get("mcp_bridge:" + sid9f) assert_eq("escaped prose cannot spoof the key even unbounded (severity bound)", json_get(blob9f, "wire"), "") // ── Summary ──────────────────────────────────────────────────────────────────── println("") println("test_bridge_serialization.el: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")