/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /* * Unit tests for the decode-time repetition-cycle detector * (src/serve/repetition_loop_detector.c). * * The failure being guarded: saber-orch (Qwen3.6-35B-A3B, Q3_K_M) at ~40k * context stopped producing new text and emitted one ~470-byte paragraph 31 * times, generating the whole 14,535-byte "answer" to completion and reporting * it as a normal finish. * * THE NEGATIVE TESTS ARE THE POINT. Truncating a legitimate long code block is * a worse failure than the collapse it prevents, because it corrupts good output * silently. So most of this file feeds the detector output that legitimately * repeats — a zero-filled byte table, `return NULL;` a hundred times, a JSON * array of similar objects, a markdown table, a log excerpt with one line 300 * times, ASCII art, an enumerated list of near-identical steps — and asserts it * stays quiet. Each of those also prints how close it came to the firing bar, * so the margin is a measured number and not an opinion. * * Token stand-in. The detector consumes token IDS, so the corpora here are run * through a deliberately DENSE word/punctuation splitter (≤4 chars per piece, * split at character-class boundaries) rather than a real BPE. Denser * tokenization means every legitimate repeating block gets a LONGER period in * tokens than a real tokenizer would give it, which pushes it closer to the * min-period bar — the negative tests are therefore stricter here than in * production, not laxer. * * The detector depends only on libc, so this links standalone (no engine/CUDA). */ #include "lib/test.h" #include "../src/serve/repetition_loop_detector.h" #include #include #include #include /* ============================================================ * Growable text buffer — the corpora are built programmatically so a "200 * identical rows" case is 200 rows and not an abbreviation of one. * ============================================================ */ typedef struct { char *buf; size_t len, cap; } strbuf; static void sb_init(strbuf *s) { s->cap = 4096; s->len = 0; s->buf = malloc(s->cap); if (s->buf) s->buf[0] = '\0'; } static void sb_free(strbuf *s) { free(s->buf); s->buf = NULL; s->len = s->cap = 0; } static void sb_addf(strbuf *s, const char *fmt, ...) { if (!s->buf) return; for (;;) { va_list ap; va_start(ap, fmt); int n = vsnprintf(s->buf + s->len, s->cap - s->len, fmt, ap); va_end(ap); if (n < 0) return; if ((size_t)n < s->cap - s->len) { s->len += (size_t)n; return; } size_t want = s->cap * 2; while (want < s->len + (size_t)n + 1) want *= 2; char *grown = realloc(s->buf, want); if (!grown) return; s->buf = grown; s->cap = want; } } /* ============================================================ * Dense token stand-in (see the file header for why it is dense on purpose). * ============================================================ */ static int char_class(unsigned char c) { if (c == ' ' || c == '\t' || c == '\n' || c == '\r') return 0; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') return 1; return 2; } static uint32_t piece_id(const char *p, size_t n) { uint32_t h = 2166136261u; for (size_t i = 0; i < n; i++) { h ^= (unsigned char)p[i]; h *= 16777619u; } return h & 0x7fffffffu; /* stay inside a plausible vocab-ish range */ } /* Split `text` into ≤4-char pieces that never span a character-class boundary, * and hash each piece to an id. Returns the count; the array is malloc'd. */ static uint32_t *tokenize_dense(const char *text, int *n_out) { size_t len = strlen(text); uint32_t *out = malloc((len + 1) * sizeof(*out)); int n = 0; size_t i = 0; if (!out) { *n_out = 0; return NULL; } while (i < len) { int cls = char_class((unsigned char)text[i]); size_t start = i; while (i < len && char_class((unsigned char)text[i]) == cls && (i - start) < 4) i++; out[n++] = piece_id(text + start, i - start); } *n_out = n; return out; } /* ============================================================ * Driver: feed a whole stream, report the trip point and the CLOSEST any * stretch of it came to the firing bar (as a percentage). That percentage is * the false-positive margin for a legitimate corpus. * ============================================================ */ static int feed_stream(rld_detector *rd, const uint32_t *t, int n, double *closest_pct) { int trip = -1; double best = 0.0; for (int i = 0; i < n; i++) { bool hit = rld_feed(rd, t[i]); if (rd->locked_period > 0) { double need = (double)rd->locked_period * (rd->cfg.min_repeats - 1); double pct = need > 0.0 ? (double)rd->verified_run / need : 0.0; if (pct > best) best = pct; } if (hit && trip < 0) trip = i; } if (closest_pct) *closest_pct = best * 100.0; return trip; } /* Run one legitimate corpus through a default-configured detector and assert it * does not fire, printing the margin so the number is on the record. */ static int assert_legit(const char *label, const char *text, double *pct_out) { rld_detector rd; int n = 0; uint32_t *toks = tokenize_dense(text, &n); double pct = 0.0; int trip; if (!toks) return -1; if (!rld_init(&rd, NULL)) { free(toks); return -1; } trip = feed_stream(&rd, toks, n, &pct); printf(" %-28s %6d tokens, closest to firing: %5.1f%% of the bar\n", label, n, pct); rld_free(&rd); free(toks); if (pct_out) *pct_out = pct; return trip; } /* ============================================================ * The measured failure: one paragraph, cycled. (The text is the run-11 * transcript's repeated paragraph.) * ============================================================ */ static const char *COLLAPSE_PARAGRAPH = "with foreign key relationships, sessions with metadata fields like title " "and creation timestamps, messages that reference those sessions through a " "foreign key constraint and an index on session_id to optimize lookups.\n" "Then there's the file_snapshots table structure starting to take shape as " "well.\n"; static void repeat_into(strbuf *s, const char *unit, int times) { for (int i = 0; i < times; i++) sb_addf(s, "%s", unit); } /* ============================================================ * POSITIVE — the collapse is caught. * ============================================================ */ static void test_collapse_paragraph_trips(void) { strbuf s; sb_init(&s); repeat_into(&s, COLLAPSE_PARAGRAPH, 31); int n_unit = 0, n = 0; uint32_t *unit = tokenize_dense(COLLAPSE_PARAGRAPH, &n_unit); uint32_t *toks = tokenize_dense(s.buf, &n); ASSERT_NOT_NULL(unit); ASSERT_NOT_NULL(toks); rld_detector rd; ASSERT_TRUE(rld_init(&rd, NULL)); int trip = feed_stream(&rd, toks, n, NULL); printf(" cycle unit = %d tokens; tripped at token %d of %d\n", n_unit, trip, n); ASSERT_TRUE(trip >= 0); ASSERT_TRUE(rd.tripped); ASSERT_INT_EQ(rd.cycle_tokens, n_unit); ASSERT_INT_EQ(rd.repeats, 12); /* Fires at the end of the 12th copy — 19 of the 31 copies never generated. */ ASSERT_INT_EQ(trip, 12 * n_unit - 1); rld_free(&rd); free(unit); free(toks); sb_free(&s); } /* The collapse rarely starts at token 0 — the model writes real prose first. * The detector is position-agnostic: the trip must land the same number of * tokens after the cycle STARTS, whatever came before it. */ static void test_collapse_after_real_prose_trips(void) { strbuf s; sb_init(&s); sb_addf(&s, "%s", "Looking at the schema more carefully, the CREATE TABLE IF NOT EXISTS " "will not add the new column to a database that predates it, so the " "CREATE INDEX on that column fails and sqlite3_exec returns an error " "before the store is ever initialized. The fix is to detect the old " "shape and migrate it. "); int n_pre = 0; uint32_t *pre = tokenize_dense(s.buf, &n_pre); ASSERT_NOT_NULL(pre); repeat_into(&s, COLLAPSE_PARAGRAPH, 20); int n_unit = 0, n = 0; uint32_t *unit = tokenize_dense(COLLAPSE_PARAGRAPH, &n_unit); uint32_t *toks = tokenize_dense(s.buf, &n); ASSERT_NOT_NULL(unit); ASSERT_NOT_NULL(toks); rld_detector rd; ASSERT_TRUE(rld_init(&rd, NULL)); int trip = feed_stream(&rd, toks, n, NULL); ASSERT_TRUE(trip >= 0); ASSERT_INT_EQ(rd.cycle_tokens, n_unit); ASSERT_INT_EQ(rd.repeats, 12); ASSERT_INT_EQ(trip, n_pre + 12 * n_unit - 1); rld_free(&rd); free(pre); free(unit); free(toks); sb_free(&s); } /* Eleven copies is under the bar and must NOT fire — the twelfth is what fires. * This is the exact boundary the whole false-positive argument rests on. */ static void test_eleven_copies_do_not_trip(void) { strbuf s; sb_init(&s); repeat_into(&s, COLLAPSE_PARAGRAPH, 11); int n = 0; uint32_t *toks = tokenize_dense(s.buf, &n); ASSERT_NOT_NULL(toks); rld_detector rd; ASSERT_TRUE(rld_init(&rd, NULL)); double pct = 0.0; int trip = feed_stream(&rd, toks, n, &pct); printf(" 11 copies reached %.1f%% of the bar (12 fires)\n", pct); ASSERT_INT_EQ(trip, -1); ASSERT_FALSE(rd.tripped); rld_free(&rd); free(toks); sb_free(&s); } /* A cycle that keeps getting interrupted by novel content is not a collapse — * the model is still saying new things. Four copies, a new sentence, four * more, forever: never fires however long it runs. */ static void test_cycle_broken_by_novel_content_never_trips(void) { strbuf s; sb_init(&s); for (int round = 0; round < 12; round++) { repeat_into(&s, COLLAPSE_PARAGRAPH, 4); sb_addf(&s, "Now round %d introduces a genuinely different observation " "about the migration path and the index rebuild.\n", round); } int n = 0; uint32_t *toks = tokenize_dense(s.buf, &n); ASSERT_NOT_NULL(toks); rld_detector rd; ASSERT_TRUE(rld_init(&rd, NULL)); double pct = 0.0; int trip = feed_stream(&rd, toks, n, &pct); printf(" %d tokens, closest to firing: %.1f%%\n", n, pct); ASSERT_INT_EQ(trip, -1); rld_free(&rd); free(toks); sb_free(&s); } /* ============================================================ * NEGATIVE — legitimate output that repeats. None of these may fire. * ============================================================ */ /* A long C function whose body really does contain the same line over and over, * plus a zero-filled lookup table. This is the shape the guard must never * truncate: the model's job is largely writing code like this. */ static void test_c_source_with_identical_lines(void) { strbuf s; sb_init(&s); sb_addf(&s, "static const uint8_t crc_seed[256] = {\n"); for (int i = 0; i < 32; i++) sb_addf(&s, " 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n"); sb_addf(&s, "};\n\n" "tc_tool_call *tc_parse_tool_calls(const char *raw, int *n_out)\n" "{\n"); for (int i = 0; i < 40; i++) { sb_addf(&s, " if (!raw)\n"); sb_addf(&s, " return NULL;\n"); } for (int i = 0; i < 60; i++) sb_addf(&s, " return NULL;\n"); sb_addf(&s, " return calls;\n}\n"); double pct = 0.0; int trip = assert_legit("C source + zero table", s.buf, &pct); ASSERT_INT_EQ(trip, -1); sb_free(&s); } /* A pure exact cycle with a SHORT fundamental period: 200 byte-identical table * rows. The detector locks onto the fundamental period (one row), not a * multiple of it, so this is rejected on the period bar and never accumulates. * If the smallest-period rule ever regresses, this is the test that catches it. */ static void test_zero_filled_table_two_hundred_rows(void) { strbuf s; sb_init(&s); sb_addf(&s, "static const uint32_t pad[1600] = {\n"); for (int i = 0; i < 200; i++) sb_addf(&s, " 0x00000000, 0x00000000, 0x00000000, 0x00000000,\n"); sb_addf(&s, "};\n"); double pct = 0.0; int trip = assert_legit("200 identical table rows", s.buf, &pct); ASSERT_INT_EQ(trip, -1); sb_free(&s); } /* A JSON array of 80 objects with the same SHAPE and different values — the * everyday "dump the rows" answer. */ static void test_json_array_of_similar_objects(void) { strbuf s; sb_init(&s); sb_addf(&s, "[\n"); for (int i = 0; i < 80; i++) sb_addf(&s, " {\n \"id\": %d,\n \"name\": \"session_%04d\",\n" " \"created_at\": \"2026-08-%02dT%02d:15:00Z\",\n" " \"message_count\": %d,\n \"archived\": %s\n }%s\n", i, i, (i % 28) + 1, i % 24, i * 7, (i & 1) ? "true" : "false", i == 79 ? "" : ","); sb_addf(&s, "]\n"); double pct = 0.0; int trip = assert_legit("JSON array, 80 objects", s.buf, &pct); ASSERT_INT_EQ(trip, -1); sb_free(&s); } /* A markdown table: separator row, then 60 data rows. */ static void test_markdown_table(void) { strbuf s; sb_init(&s); sb_addf(&s, "| id | table | column | type | nullable | indexed |\n"); sb_addf(&s, "| --- | --- | --- | --- | --- | --- |\n"); for (int i = 0; i < 60; i++) sb_addf(&s, "| %d | points | scope_%02d | TEXT | no | yes |\n", i, i); double pct = 0.0; int trip = assert_legit("markdown table, 60 rows", s.buf, &pct); ASSERT_INT_EQ(trip, -1); sb_free(&s); } /* A log excerpt where ONE line recurs 300 times — a real thing a model is asked * to reproduce, and a pure exact cycle at a short fundamental period. */ static void test_log_excerpt_same_line_three_hundred_times(void) { strbuf s; sb_init(&s); sb_addf(&s, "The relevant part of the log is:\n\n"); for (int i = 0; i < 300; i++) sb_addf(&s, "context_store: schema failed: no such column: scope\n"); sb_addf(&s, "\nThat repeats until the process gives up.\n"); double pct = 0.0; int trip = assert_legit("log line x300", s.buf, &pct); ASSERT_INT_EQ(trip, -1); sb_free(&s); } /* Box-drawing ASCII art: an identical border band repeated 50 times. */ static void test_ascii_art_border_band(void) { strbuf s; sb_init(&s); sb_addf(&s, "+--------------------------------------------------+\n"); for (int i = 0; i < 50; i++) sb_addf(&s, "| |\n"); sb_addf(&s, "+--------------------------------------------------+\n"); double pct = 0.0; int trip = assert_legit("ASCII art border band", s.buf, &pct); ASSERT_INT_EQ(trip, -1); sb_free(&s); } /* SQL schema: 70 column definitions, same shape, different names. */ static void test_sql_schema_column_lines(void) { strbuf s; sb_init(&s); sb_addf(&s, "CREATE TABLE IF NOT EXISTS points (\n"); for (int i = 0; i < 70; i++) sb_addf(&s, " column_%03d TEXT NOT NULL DEFAULT '',\n", i); sb_addf(&s, " PRIMARY KEY (column_000)\n);\n"); double pct = 0.0; int trip = assert_legit("SQL schema, 70 columns", s.buf, &pct); ASSERT_INT_EQ(trip, -1); sb_free(&s); } /* An enumerated procedure: 40 near-identical steps differing only by number. * Structurally this is the closest legitimate output gets to a paragraph loop. */ static void test_enumerated_near_identical_steps(void) { strbuf s; sb_init(&s); for (int i = 1; i <= 40; i++) sb_addf(&s, "%d. Open the file at src/context/context_store.c and locate the " "schema string, then confirm that the column referenced by index " "number %d is present before the CREATE INDEX statement runs.\n", i, i); double pct = 0.0; int trip = assert_legit("enumerated steps x40", s.buf, &pct); ASSERT_INT_EQ(trip, -1); sb_free(&s); } /* THE WORST CASE. A byte-identical multi-line block well over the period bar, * repeated 11 times back to back with nothing in between — legitimate output * has no reason to do this, and this is as close as anything gets to firing. * The margin it leaves is the number to quote when the thresholds are argued. */ static void test_worst_case_identical_block_eleven_times(void) { const char *block = " {\n" " \"kind\": \"function\",\n" " \"name\": \"read_file\",\n" " \"description\": \"Read a UTF-8 text file from the workspace\",\n" " \"parameters\": { \"path\": { \"type\": \"string\" } },\n" " \"returns\": { \"type\": \"string\" },\n" " \"idempotent\": true\n" " },\n"; strbuf s; sb_init(&s); repeat_into(&s, block, 11); int n_unit = 0; uint32_t *unit = tokenize_dense(block, &n_unit); ASSERT_NOT_NULL(unit); ASSERT_TRUE(n_unit >= 48); /* it really is over the period bar */ double pct = 0.0; int trip = assert_legit("identical block x11", s.buf, &pct); ASSERT_INT_EQ(trip, -1); /* It locked and climbed — this is a genuine near-miss, not a corpus the * detector ignored for some unrelated reason. */ ASSERT_TRUE(pct > 80.0); ASSERT_TRUE(pct < 100.0); free(unit); sb_free(&s); } /* Ordinary varied prose must never lock at all. */ static void test_varied_prose_never_locks(void) { strbuf s; sb_init(&s); for (int i = 0; i < 120; i++) sb_addf(&s, "Paragraph %d considers a different part of the store: the %s path " "and how it interacts with the %s boundary when the session id is " "reused across two conversations at once.\n", i, (i & 1) ? "append-resume" : "full-prefill", (i % 3) ? "system-prefix" : "context-cap"); double pct = 0.0; int trip = assert_legit("varied prose", s.buf, &pct); ASSERT_INT_EQ(trip, -1); sb_free(&s); } /* ============================================================ * Configuration * ============================================================ */ static void test_defaults_are_the_documented_ones(void) { rld_config cfg = {0}; rld_default_config(&cfg); ASSERT_INT_EQ(cfg.min_period, 48); ASSERT_INT_EQ(cfg.max_period, 512); ASSERT_INT_EQ(cfg.min_repeats, 12); ASSERT_TRUE(rld_enabled()); } /* A cycle LONGER than max_period is not tracked (the ring cannot see that far * back) — a documented limit, asserted so it cannot become a surprise. */ static void test_cycle_longer_than_max_period_is_not_tracked(void) { rld_config cfg = { .min_period = 8, .max_period = 32, .min_repeats = 4 }; rld_detector rd; ASSERT_TRUE(rld_init(&rd, &cfg)); uint32_t unit[200]; for (int i = 0; i < 200; i++) unit[i] = (uint32_t)(1000 + i); int trip = -1; for (int r = 0; r < 10 && trip < 0; r++) for (int i = 0; i < 200; i++) if (rld_feed(&rd, unit[i])) { trip = 1; break; } ASSERT_INT_EQ(trip, -1); rld_free(&rd); } /* A NULL / inert detector is safe to feed and free: an allocation failure must * never be able to fail a completion. */ static void test_inert_detector_is_safe(void) { rld_detector rd; memset(&rd, 0, sizeof(rd)); for (int i = 0; i < 5000; i++) ASSERT_FALSE(rld_feed(&rd, (uint32_t)(i % 3))); rld_free(&rd); rld_free(&rd); /* idempotent */ ASSERT_FALSE(rld_feed(NULL, 7)); ASSERT_FALSE(rld_init(NULL, NULL)); } /* Raising min_repeats really does hold fire — the operator knob works. */ static void test_min_repeats_knob_holds_fire(void) { rld_config cfg = { .min_period = 48, .max_period = 512, .min_repeats = 64 }; strbuf s; sb_init(&s); repeat_into(&s, COLLAPSE_PARAGRAPH, 31); int n = 0; uint32_t *toks = tokenize_dense(s.buf, &n); ASSERT_NOT_NULL(toks); rld_detector rd; ASSERT_TRUE(rld_init(&rd, &cfg)); ASSERT_INT_EQ(feed_stream(&rd, toks, n, NULL), -1); rld_free(&rd); free(toks); sb_free(&s); } int main(void) { printf("\n=== repetition_loop_detector (decode-time cycle guard) ===\n\n"); printf(" -- positive: the measured collapse --\n"); RUN_TEST(collapse_paragraph_trips); RUN_TEST(collapse_after_real_prose_trips); RUN_TEST(eleven_copies_do_not_trip); RUN_TEST(cycle_broken_by_novel_content_never_trips); printf("\n -- negative: legitimate output that repeats --\n"); RUN_TEST(c_source_with_identical_lines); RUN_TEST(zero_filled_table_two_hundred_rows); RUN_TEST(json_array_of_similar_objects); RUN_TEST(markdown_table); RUN_TEST(log_excerpt_same_line_three_hundred_times); RUN_TEST(ascii_art_border_band); RUN_TEST(sql_schema_column_lines); RUN_TEST(enumerated_near_identical_steps); RUN_TEST(worst_case_identical_block_eleven_times); RUN_TEST(varied_prose_never_locks); printf("\n -- configuration --\n"); RUN_TEST(defaults_are_the_documented_ones); RUN_TEST(cycle_longer_than_max_period_is_not_tracked); RUN_TEST(inert_detector_is_safe); RUN_TEST(min_repeats_knob_holds_fire); TEST_REPORT(); return __ft_failed == 0 ? 0 : 1; }