diff --git a/chat.el b/chat.el index 83972f6..5398c4e 100644 --- a/chat.el +++ b/chat.el @@ -909,6 +909,9 @@ fn handle_chat_as_soul(body: String) -> String { let req_model: String = json_get(body, "model") let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model } + // Hard Bell: pre-LLM safety evaluation — multi-soul room conversations are real interactions. + let system_prompt = safety_augment_system(system_prompt, eff_message) + let raw_response: String = llm_call_system(model, system_prompt, eff_message) let is_error: Bool = str_starts_with(raw_response, "{\"error\"") @@ -955,6 +958,9 @@ fn handle_dharma_room_turn(body: String) -> String { identity + "\n\n" + engram_ctx } + // Hard Bell: pre-LLM safety evaluation — dharma room turns are real conversations. + let system_prompt = safety_augment_system(system_prompt, transcript) + let raw_response: String = llm_call_system(model, system_prompt, transcript) let is_error: Bool = str_starts_with(raw_response, "{\"error\"") @@ -1001,6 +1007,9 @@ fn handle_dharma_room_turn_agentic(body: String) -> String { let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct and stay in character.\n\n" + ctx let api_key: String = agentic_api_key() + // Hard Bell: pre-LLM safety evaluation on agentic dharma room turns. + let system = safety_augment_system(system, transcript) + let tools_json: String = agentic_tools_all() let safe_transcript: String = json_safe(transcript) let safe_sys: String = json_safe(system) diff --git a/neuron-api.el b/neuron-api.el index fb123fb..f95d30e 100644 --- a/neuron-api.el +++ b/neuron-api.el @@ -368,6 +368,7 @@ fn handle_api_define_process(body: String) -> String { let id: String = engram_node_full(content, "Process", label, el_from_float(0.8), el_from_float(0.8), el_from_float(0.9), "Canonical", tags) + if !api_persisted(id) { return api_not_persisted(id) } return "{\"id\":\"" + id + "\",\"ok\":true}" } diff --git a/routes.el b/routes.el index e7597f9..b0d6ae1 100644 --- a/routes.el +++ b/routes.el @@ -219,7 +219,9 @@ fn connectd_get(suffix: String) -> String { // so arbitrary JSON cannot reach the shell as a command-line argument. fn connectd_post(suffix: String, body: String) -> String { let eff: String = if str_eq(body, "") { "{}" } else { body } - let tmp: String = "/tmp/neuron-connectors-req.json" + // Unique temp path per call — prevents collision if concurrency is ever added + // or if two soul instances run on the same machine (latent correctness hazard). + let tmp: String = "/tmp/neuron-connectors-req-" + int_to_str(time_now()) + ".json" fs_write(tmp, eff) let out: String = exec_capture("curl -s --max-time 20 -X POST http://127.0.0.1:7771" + suffix + " -H 'Content-Type: application/json' -d @" + tmp) if str_eq(out, "") { diff --git a/soul.el b/soul.el index 9f3b56a..6812b0c 100644 --- a/soul.el +++ b/soul.el @@ -368,12 +368,19 @@ let is_genesis: Bool = str_eq(soul_cgi_id, "ntn-genesis") // sparse but the on-disk snapshot file is large, the load FAILED — seeding+saving now would // clobber the user's real memory (this is exactly how the 06-14 clobber happened). Read the // on-disk file (local mode only) and refuse the destructive seed+save when it looks populated. +// +// HTTP-engram guard (2026-06-17): when ENGRAM_URL is set the HTTP Engram owns persistence — +// the soul must NEVER write to the local snapshot regardless of node counts. safe_to_seed is +// unconditionally false in HTTP mode (not the persistence owner). let guard_disk: String = if str_eq(engram_url_raw, "") { fs_read(snapshot) } else { "" } let guard_disk_len: Int = str_len(guard_disk) // Ratio guard (2026-06-15 fix): refuse to seed/save whenever the in-memory load is FAR smaller than // the on-disk file implies (~16KB/node) — catches partial loads of ANY size, not just <50. The old // <50 threshold let a 63-node identity-only load clobber a 47MB/5000-node graph. -let safe_to_seed: Bool = !(guard_disk_len > 200000 && engram_node_count() < guard_disk_len / 16000) +// Multiplication form (2026-06-17): node_count * 16000 < disk_len avoids floor-division truncation +// (e.g., 250KB / 16000 = 15.6, floors to 15 — a 15-node graph wrongly passes the old guard). +// HTTP-engram guard: when using_http_engram the soul is not the persistence owner; never seed. +let safe_to_seed: Bool = !using_http_engram && !(guard_disk_len > 200000 && engram_node_count() * 16000 < guard_disk_len) if is_genesis && !safe_to_seed { println("[soul] GUARD: loaded " + int_to_str(engram_node_count()) + " nodes but snapshot file is " + int_to_str(guard_disk_len) diff --git a/tests/test_api_define_process.el b/tests/test_api_define_process.el new file mode 100644 index 0000000..68cc3b4 --- /dev/null +++ b/tests/test_api_define_process.el @@ -0,0 +1,109 @@ +// ── tests/test_api_define_process.el ───────────────────────────────────────── +// +// Test the handle_api_define_process read-back fix (neuron-api.el). +// +// Bug: handle_api_define_process was the only write handler that did NOT call +// api_persisted() after the write, returning {"id":"...","ok":true} even when +// the engram write failed (hallucinated save). +// +// Fix: added `if !api_persisted(id) { return api_not_persisted(id) }` before +// the return, consistent with all sibling handlers (remember, capture_knowledge, +// evolve_knowledge, promote_knowledge, node_create). +// +// Tests: +// 1. define_process returns ok==true and id resolves via engram_get_node_json. +// 2. Missing content returns the standard error. +// 3. Unnamed process uses default label and still persists. +// ────────────────────────────────────────────────────────────────────────────── + +import "../neuron-api.el" + +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_not_eq(label: String, got: String, not_want: String) -> Void { + if str_eq(got, not_want) { + let fail_count = fail_count + 1 + println(" FAIL: " + label + " (got: " + got + ", should differ)") + } else { + let pass_count = pass_count + 1 + println(" PASS: " + label) + } +} + +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) + } +} + +// ── Section 1: define_process — happy path with read-back ──────────────────── + +println("") +println("1. handle_api_define_process — write then verify id resolves") + +let proc_body: String = "{\"content\":\"Test process: run step A, then step B, then step C.\",\"name\":\"test-process-guard\"}" +let proc_result: String = handle_api_define_process(proc_body) + +let proc_ok: String = json_get(proc_result, "ok") +let proc_id: String = json_get(proc_result, "id") + +assert_eq("define_process -> ok==true", proc_ok, "true") +assert_not_eq("define_process -> id is non-empty", proc_id, "") + +let node_json: String = engram_get_node_json(proc_id) +let node_status: String = if str_eq(node_json, "") { "empty" } else { + if str_eq(node_json, "null") { "null" } else { "ok" } +} +assert_eq("define_process -> node read-back resolves (not empty/null)", node_status, "ok") +assert_contains("define_process -> node content contains process text", node_json, "Test process") + +// ── Section 2: define_process — missing content returns error ──────────────── + +println("") +println("2. handle_api_define_process — missing content returns error") + +let no_content_body: String = "{\"name\":\"nameless\"}" +let no_content_result: String = handle_api_define_process(no_content_body) +let no_content_error: String = json_get(no_content_result, "error") +assert_eq("missing content -> error is 'content is required'", no_content_error, "content is required") + +// ── Section 3: define_process — unnamed process gets default label ──────────── + +println("") +println("3. handle_api_define_process — unnamed process writes and read-back succeeds") + +let unnamed_body: String = "{\"content\":\"Unnamed test process for coverage.\"}" +let unnamed_result: String = handle_api_define_process(unnamed_body) +let unnamed_ok: String = json_get(unnamed_result, "ok") +let unnamed_id: String = json_get(unnamed_result, "id") +assert_eq("unnamed process -> ok==true", unnamed_ok, "true") +assert_not_eq("unnamed process -> id non-empty", unnamed_id, "") + +let unnamed_node: String = engram_get_node_json(unnamed_id) +let unnamed_status: String = if str_eq(unnamed_node, "") { "empty" } else { + if str_eq(unnamed_node, "null") { "null" } else { "ok" } +} +assert_eq("unnamed process -> node read-back ok", unnamed_status, "ok") + +// ── Summary ─────────────────────────────────────────────────────────────────── + +println("") +println("api_define_process tests: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed") diff --git a/tests/test_bell_safety.el b/tests/test_bell_safety.el new file mode 100644 index 0000000..c97e7b5 --- /dev/null +++ b/tests/test_bell_safety.el @@ -0,0 +1,266 @@ +// ── tests/test_bell_safety.el ───────────────────────────────────────────────── +// +// Unit tests for the Hard Bell safety layer added in feat/connectors-soul. +// Covers the public API exposed by safety.el: +// - safety_detect_bell_level: 'none' / 'soft' / 'hard' +// - safety_classify_hard_bell: 'self_harm' / 'abuse' +// - safety_normalize: smart-quote -> ASCII apostrophe normalisation +// - safety_augment_system: system prompt passthrough / augmentation +// - handle_safety_contact_post: validation + read-back +// +// El test convention: mutable pass_count / fail_count globals, assert_* helpers, +// println for each result. Mirrors the style in existing safety tests. +// ────────────────────────────────────────────────────────────────────────────── + +import "../safety.el" + +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_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) + } +} + +// ── Section 1: safety_detect_bell_level — 'none' ───────────────────────────── + +println("") +println("1. safety_detect_bell_level — neutral input -> 'none'") + +let level_neutral: String = safety_detect_bell_level("What time is the meeting tomorrow?") +assert_eq("neutral question -> none", level_neutral, "none") + +let level_positive: String = safety_detect_bell_level("I love sunny days") +assert_eq("positive statement -> none", level_positive, "none") + +let level_empty: String = safety_detect_bell_level("") +assert_eq("empty string -> none", level_empty, "none") + +// ── Section 2: safety_detect_bell_level — single soft phrase stays 'none' ──── + +println("") +println("2. safety_detect_bell_level — single soft phrase does not reach 'soft' (threshold=2)") + +let level_one_soft: String = safety_detect_bell_level("I have been feeling stressed lately") +assert_eq("single soft phrase alone -> none", level_one_soft, "none") + +// ── Section 3: safety_detect_bell_level — 'soft' with two+ soft phrases ────── + +println("") +println("3. safety_detect_bell_level — two soft phrases -> 'soft'") + +let level_two_soft: String = safety_detect_bell_level("I am feeling overwhelmed and really anxious about everything") +assert_eq("two soft phrases -> soft", level_two_soft, "soft") + +let level_multi_soft: String = safety_detect_bell_level("So depressed and hopeless, nothing matters anymore") +assert_eq("multiple soft phrases -> soft", level_multi_soft, "soft") + +// ── Section 4: safety_detect_bell_level — 'hard' (self_harm phrases) ───────── + +println("") +println("4. safety_detect_bell_level — self_harm phrases -> 'hard'") + +let level_kill_myself: String = safety_detect_bell_level("I want to kill myself") +assert_eq("'kill myself' -> hard", level_kill_myself, "hard") + +let level_suicide: String = safety_detect_bell_level("I have been suicidal for weeks") +assert_eq("'suicidal' -> hard", level_suicide, "hard") + +let level_want_die: String = safety_detect_bell_level("I want to die, nothing is worth it") +assert_eq("'want to die' -> hard", level_want_die, "hard") + +let level_overdose: String = safety_detect_bell_level("I am thinking about an overdose") +assert_eq("'overdose' -> hard", level_overdose, "hard") + +let level_self_harm: String = safety_detect_bell_level("My history of self-harm is back") +assert_eq("'self-harm' -> hard", level_self_harm, "hard") + +let level_no_reason: String = safety_detect_bell_level("There is no reason to live") +assert_eq("'no reason to live' -> hard", level_no_reason, "hard") + +// ── Section 5: safety_detect_bell_level — 'hard' (abuse phrases) ───────────── + +println("") +println("5. safety_detect_bell_level — abuse phrases -> 'hard'") + +let level_he_hit: String = safety_detect_bell_level("he hit me last night") +assert_eq("'he hit me' -> hard", level_he_hit, "hard") + +let level_dv: String = safety_detect_bell_level("I am experiencing domestic violence") +assert_eq("'domestic violence' -> hard", level_dv, "hard") + +let level_abused: String = safety_detect_bell_level("I am being abused") +assert_eq("'being abused' -> hard", level_abused, "hard") + +// ── Section 6: safety_detect_bell_level — 'hard' (general hard phrases) ────── + +println("") +println("6. safety_detect_bell_level — general hard phrases -> 'hard'") + +let level_hurting_me: String = safety_detect_bell_level("someone is hurting me right now") +assert_eq("'hurting me' -> hard", level_hurting_me, "hard") + +let level_being_hurt: String = safety_detect_bell_level("I am being hurt and need help") +assert_eq("'being hurt' -> hard", level_being_hurt, "hard") + +// ── Section 7: safety_classify_hard_bell — abuse -> 'abuse' ────────────────── + +println("") +println("7. safety_classify_hard_bell — abuse phrases route to 'abuse'") + +let class_he_hit: String = safety_classify_hard_bell("he hit me yesterday") +assert_eq("'he hit me' classifies as abuse", class_he_hit, "abuse") + +let class_dv: String = safety_classify_hard_bell("domestic violence in my home") +assert_eq("'domestic violence' classifies as abuse", class_dv, "abuse") + +let class_abused: String = safety_classify_hard_bell("I'm being abused by my partner") +assert_eq("'being abused' classifies as abuse", class_abused, "abuse") + +// ── Section 8: safety_classify_hard_bell — self_harm phrases ───────────────── + +println("") +println("8. safety_classify_hard_bell — self_harm phrases route to 'self_harm'") + +let class_kill: String = safety_classify_hard_bell("I want to kill myself") +assert_eq("'kill myself' classifies as self_harm", class_kill, "self_harm") + +let class_suicide: String = safety_classify_hard_bell("I am suicidal") +assert_eq("'suicidal' classifies as self_harm", class_suicide, "self_harm") + +let class_overdose: String = safety_classify_hard_bell("took too many pills") +assert_eq("'took too many' classifies as self_harm", class_overdose, "self_harm") + +// ── Section 9: safety_classify_hard_bell — general -> 'self_harm' ──────────── + +println("") +println("9. safety_classify_hard_bell — general hard phrases fall through to 'self_harm'") + +let class_going_kill: String = safety_classify_hard_bell("going to kill everything around me") +assert_eq("general hard phrase falls through to self_harm", class_going_kill, "self_harm") + +// ── Section 10: safety_normalize — curly apostrophe normalisation ───────────── + +println("") +println("10. safety_normalize — curly apostrophe normalisation") + +// U+2019 RIGHT SINGLE QUOTATION MARK (UTF-8: \xe2\x80\x99) must become ASCII ' +let smart_msg: String = "I can" + "\xe2\x80\x99" + "t go on anymore" +let normalized: String = safety_normalize(smart_msg) +assert_contains("smart-quote normalized to ASCII apostrophe", normalized, "can't go on") + +// After normalisation, detect_bell_level must fire 'hard' on the smart-quote variant +let level_smart: String = safety_detect_bell_level(smart_msg) +assert_eq("smart-quote 'can't go on' -> hard (after normalize)", level_smart, "hard") + +// ── Section 11: safety_augment_system — passthrough on neutral ─────────────── + +println("") +println("11. safety_augment_system — neutral input returns system unchanged") + +let base_sys: String = "You are a helpful assistant." +let aug_neutral: String = safety_augment_system(base_sys, "What is the weather?") +assert_eq("neutral message -> system unchanged", aug_neutral, base_sys) + +// ── Section 12: safety_augment_system — soft bell injects directive ────────── + +println("") +println("12. safety_augment_system — soft bell injects soft directive") + +let aug_soft: String = safety_augment_system(base_sys, "Feeling so overwhelmed and completely anxious") +assert_contains("soft augment -> contains original system", aug_soft, base_sys) +assert_contains("soft augment -> contains SUBSTRATE DIRECTIVE", aug_soft, "SUBSTRATE DIRECTIVE") +assert_contains("soft augment -> contains soft care text", aug_soft, "genuine care") + +// ── Section 13: safety_augment_system — hard self_harm injects 988 ─────────── + +println("") +println("13. safety_augment_system — hard self_harm injects crisis resources with 988") + +let aug_hard: String = safety_augment_system(base_sys, "I want to kill myself tonight") +assert_contains("hard self_harm -> contains SUBSTRATE DIRECTIVE", aug_hard, "SUBSTRATE DIRECTIVE") +assert_contains("hard self_harm -> includes 988 crisis line", aug_hard, "988") +assert_not_contains("hard self_harm -> no DV hotline (wrong routing)", aug_hard, "1-800-799-7233") + +// ── Section 14: safety_augment_system — hard abuse routes to abuse directive ── + +println("") +println("14. safety_augment_system — hard abuse injects abuse-specific directive") + +let aug_abuse: String = safety_augment_system(base_sys, "he hit me and I am afraid of him") +assert_contains("hard abuse -> DV hotline present", aug_abuse, "1-800-799-7233") +assert_contains("hard abuse -> mentions not notifying contact", aug_abuse, "safety contact") + +// ── Section 15: handle_safety_contact_post — validation ─────────────────────── + +println("") +println("15. handle_safety_contact_post — non-crisis without name returns error") + +let no_name_body: String = "{\"is_crisis_line\":false,\"contact_method\":\"phone\",\"contact_value\":\"555-1234\",\"relationship\":\"friend\"}" +let no_name_result: String = handle_safety_contact_post(no_name_body) +let no_name_ok: String = json_get(no_name_result, "ok") +let no_name_err: String = json_get(no_name_result, "error") +assert_eq("no name -> ok==false", no_name_ok, "false") +assert_eq("no name -> error is 'name is required'", no_name_err, "name is required") + +// ── Section 16: handle_safety_contact_post — write then read back ────────────── + +println("") +println("16. handle_safety_contact_post — write then read back verifies persistence") + +let contact_body: String = "{\"is_crisis_line\":false,\"name\":\"Test Contact\",\"contact_method\":\"phone\",\"contact_value\":\"555-9876\",\"relationship\":\"sibling\"}" +let write_result: String = handle_safety_contact_post(contact_body) +let write_ok: String = json_get(write_result, "ok") +assert_eq("contact write -> ok==true", write_ok, "true") +assert_contains("contact write -> result has configured", write_result, "\"configured\"") +assert_contains("contact write -> result has name", write_result, "Test Contact") + +let read_result: String = handle_safety_contact_get() +assert_eq("contact read-back -> configured==true", json_get(read_result, "configured"), "true") +assert_contains("contact read-back -> name matches", read_result, "Test Contact") + +// ── Section 17: handle_safety_contact_post — crisis line auto-fills ─────────── + +println("") +println("17. handle_safety_contact_post — crisis line auto-fills name and value") + +let crisis_body: String = "{\"is_crisis_line\":true}" +let crisis_result: String = handle_safety_contact_post(crisis_body) +let crisis_ok: String = json_get(crisis_result, "ok") +assert_eq("crisis line write -> ok==true", crisis_ok, "true") +assert_contains("crisis line -> name is Crisis Line", crisis_result, "Crisis Line") +assert_contains("crisis line -> value is 988", crisis_result, "988") + +// ── Summary ─────────────────────────────────────────────────────────────────── + +println("") +println("bell_safety tests: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed") diff --git a/tests/test_soul_guard.el b/tests/test_soul_guard.el new file mode 100644 index 0000000..c8a6ecf --- /dev/null +++ b/tests/test_soul_guard.el @@ -0,0 +1,124 @@ +// ── tests/test_soul_guard.el ────────────────────────────────────────────────── +// +// Logic tests for the genesis guard in soul.el (feat/connectors-soul). +// +// The guard is top-level imperative boot code. This file tests the predicate +// logic as pure functions to verify the conditions exhaustively: +// +// safe_to_seed = !using_http_engram && +// !(guard_disk_len > 200000 && loaded_nodes * 16000 < guard_disk_len) +// +// Scenarios: +// - Boundary: 199,999 bytes + sparse -> safe_to_seed == true +// - Boundary: 200,001 bytes + sparse -> safe_to_seed == false +// - Ratio: 47MB + 63 nodes -> false (the 2026-06-14 clobber scenario) +// - HTTP mode -> false unconditionally +// - Multiplication form vs old division form near 250KB boundary +// ────────────────────────────────────────────────────────────────────────────── + +let pass_count: Int = 0 +let fail_count: Int = 0 + +fn assert_eq_bool(label: String, got: Bool, expected: Bool) -> Void { + let got_s: String = if got { "true" } else { "false" } + let exp_s: String = if expected { "true" } else { "false" } + if str_eq(got_s, exp_s) { + let pass_count = pass_count + 1 + println(" PASS: " + label) + } else { + let fail_count = fail_count + 1 + println(" FAIL: " + label) + println(" got: " + got_s) + println(" expected: " + exp_s) + } +} + +// guard_predicate — mirrors the safe_to_seed expression in soul.el exactly. +fn guard_predicate(using_http: Bool, disk_len: Int, loaded_nodes: Int) -> Bool { + if using_http { return false } + let ratio_block: Bool = disk_len > 200000 && loaded_nodes * 16000 < disk_len + return !ratio_block +} + +// ── Section 1: 200KB boundary ───────────────────────────────────────────────── + +println("") +println("1. guard boundary — 199,999 bytes + sparse load -> safe_to_seed true") + +let safe_below: Bool = guard_predicate(false, 199999, 1) +assert_eq_bool("199,999 bytes + 1 node -> safe", safe_below, true) + +let safe_below_zero: Bool = guard_predicate(false, 199999, 0) +assert_eq_bool("199,999 bytes + 0 nodes -> safe (below 200KB threshold)", safe_below_zero, true) + +println("") +println("2. guard boundary — 200,001 bytes + sparse load -> safe_to_seed false") + +let unsafe_above: Bool = guard_predicate(false, 200001, 1) +assert_eq_bool("200,001 bytes + 1 node -> unsafe", unsafe_above, false) + +let unsafe_zero: Bool = guard_predicate(false, 200001, 0) +assert_eq_bool("200,001 bytes + 0 nodes -> unsafe", unsafe_zero, false) + +// ── Section 2: ratio guard — 47MB + 63 nodes ───────────────────────────────── + +println("") +println("3. guard ratio — 47MB + 63 nodes (the 2026-06-14 clobber scenario)") + +let clobber_blocked: Bool = guard_predicate(false, 47000000, 63) +assert_eq_bool("47MB + 63 nodes -> unsafe (clobber blocked)", clobber_blocked, false) + +// 47MB / 16000 = 2937.5 -> need >= 2938 nodes for safe +let clobber_safe: Bool = guard_predicate(false, 47000000, 2938) +assert_eq_bool("47MB + 2938 nodes -> safe (load correct)", clobber_safe, true) + +let boundary_blocked: Bool = guard_predicate(false, 47000000, 2937) +assert_eq_bool("47MB + 2937 nodes -> unsafe (just below ratio)", boundary_blocked, false) + +// ── Section 3: HTTP-engram mode — always false ──────────────────────────────── + +println("") +println("4. guard HTTP mode — always false regardless of disk/node counts") + +let http_zero: Bool = guard_predicate(true, 0, 0) +assert_eq_bool("HTTP mode + 0/0 -> unsafe", http_zero, false) + +let http_small: Bool = guard_predicate(true, 1000, 100) +assert_eq_bool("HTTP mode + small snapshot -> unsafe", http_small, false) + +let http_large: Bool = guard_predicate(true, 47000000, 2938) +assert_eq_bool("HTTP mode + large/fully-loaded -> unsafe", http_large, false) + +// ── Section 4: normal local mode — small/fresh snapshots ───────────────────── + +println("") +println("5. guard normal local mode — small/fresh snapshots") + +let fresh_genesis: Bool = guard_predicate(false, 0, 0) +assert_eq_bool("fresh genesis (0 bytes, 0 nodes) -> safe", fresh_genesis, true) + +let small_snapshot: Bool = guard_predicate(false, 50000, 5) +assert_eq_bool("50KB + 5 nodes -> safe (below 200KB threshold)", small_snapshot, true) + +// ── Section 5: multiplication vs division — 250KB boundary ─────────────────── + +println("") +println("6. guard multiplication form — avoids floor-division truncation at 250KB") + +// OLD (division): 250000 / 16000 = 15 (floors 15.625). 15 < 15 is false -> wrongly safe. +// NEW (multiplication): 15 * 16000 = 240000 < 250000 -> correctly unsafe. +let div_boundary: Bool = guard_predicate(false, 250000, 15) +assert_eq_bool("250,000 bytes + 15 nodes -> unsafe (multiplication form)", div_boundary, false) + +// With 16 nodes: 16 * 16000 = 256000 > 250000 -> safe. +let div_just_enough: Bool = guard_predicate(false, 250000, 16) +assert_eq_bool("250,000 bytes + 16 nodes -> safe", div_just_enough, true) + +// Exact equality: disk_len == node_count * 16000 -> not sparse -> safe. +let exact_match: Bool = guard_predicate(false, 32000, 2) +assert_eq_bool("exact ratio (32000 bytes, 2 nodes: 2*16000=32000) -> safe", exact_match, true) + +// ── Summary ─────────────────────────────────────────────────────────────────── + +println("") +println("soul_guard tests: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")