Skip to content

Commit 2fbffa3

Browse files
committed
Check every control character's round trip through the scripted chunks
everyControlCharacterSurvivesAsJson parsed each chunk but never read the text back, and any hexadecimal or short escape parses, so a quote() that wrote the wrong character passed for all 32 inputs. Only the separate round-trip test read a value, for one sample string whose CR exists only on Windows. A contentOf helper now joins the delta.content of a turn's chunks, the loop compares it with the input for every character from U+0000 to U+001F and for the quote/backslash and DEL/U+0085/U+2028 inputs, and the round-trip test uses the same helper. Shown red by mutation of quote(): CR written as \n fails with 'U+000D Expected: is "a\rb" but: was "a\nb"' (the old test stayed green on Linux), BS written as \f fails at U+0008; unchanged code 3/3 green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBFFnrhtvXiUeyxjmUc4R2
1 parent 50ae06a commit 2fbffa3

2 files changed

Lines changed: 27 additions & 13 deletions

File tree

‎BUGHUNT-FOUND.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,7 @@ Repos plus eins. Geprüfte Klassen ohne Fund stehen in [`BUGHUNT-CHECKED.md`](BU
15831583
- **Fix:** `quote` bekommt `\r`, `\t`, `\b`, `\f` und — das Tragende — einen `default`-Zweig, der alles unter `0x20` als `\uXXXX` schreibt, damit ein nicht aufgelistetes Steuerzeichen die Lücke nicht wieder aufreißen kann. Der Test selbst bleibt unangetastet: `System.lineSeparator()` dort ist richtig, weil genau das CRLF überhaupt erst ausübt. Ein `grep` über alle handgerollten JSON-Quote-Helfer im Repository zeigt, dass es diese eine Stelle war.
15841584
- **Tests:** Neu `ScriptedBackendTest` (3): jeder Chunk eines Text-Zuges parst als JSON (mit `System.lineSeparator()`, CRLF, LF und einzelnem CR); **jedes** der 32 Steuerzeichen einzeln, also der `default`-Zweig und nicht die vier benannten Fälle; und der Text kommt unverändert aus dem Chunk zurück.
15851585
- **Rot-Grün-Nachweis:** Mit der alten `quote`-Fassung kurz zurückgesetzt: `ScriptedBackendTest` 2 Failures + 1 Error (`System.lineSeparator(): chunk is not JSON`, `U+0000: chunk is not JSON`, `JsonParse Illegal unquoted character ((CTRL-CHAR, code 13))`) und `AgentSessionTest` 1 Failure. Mit dem Fix (Datei danach byte-identisch wiederhergestellt) beide dreimal hintereinander grün, und das ganze Modul 281/0/0/60 — die 60 sind die dokumentierten JLine-Fälle. `spotless:check` grün.
1586+
- **Nachtrag (Branch-Review 2026-10-20):** `#everyControlCharacterSurvivesAsJson` prüfte für die 32 Steuerzeichen nur, dass jeder Chunk parst; jedes `\uXXXX` und jede kurze Maske parst aber, so dass ein falsches Zeichen durchging – auf Linux ungeprüft blieben die Werte von `\r`, `\b` und `\f` (Fund J15-2). Der neue Helfer `contentOf(List<String>)` hängt das `delta.content` aller Chunks eines Zuges aneinander; der Test vergleicht damit jetzt für jedes Zeichen von U+0000 bis U+001F sowie für Anführungszeichen/Backslash und DEL/U+0085/U+2028 den Text mit der Eingabe, und `#theTextComesBackOutOfTheChunkUnchanged` nutzt denselben Helfer statt der eigenen Schleife. Gegenprobe rot: `\r` als `\n` geschrieben (`U+000D Expected: is "a\rb" but: was "a\nb"`; die alte Testfassung blieb damit auf Linux grün, 3/3), `\b` als `\f` (`U+0008`); unverändert 3/3 grün.
15861587
- **Status:** gefixt
15871588
- **Commit:** java-llama.cpp `d68aa3d`
15881589

‎llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ScriptedBackendTest.java‎

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ private static void assertEveryChunkIsJson(String what, List<String> chunks) {
3939
}
4040
}
4141

42+
/** The text a turn's chunks carry: the {@code delta.content} of every chunk, in order. */
43+
private static String contentOf(List<String> chunks) throws Exception {
44+
StringBuilder content = new StringBuilder();
45+
for (String chunk : chunks) {
46+
var delta = MAPPER.readTree(chunk).path("choices").path(0).path("delta");
47+
if (delta.path("content").isTextual()) {
48+
content.append(delta.path("content").asText());
49+
}
50+
}
51+
return content.toString();
52+
}
53+
4254
@Test
4355
void aTextTurnCarryingTheLineSeparatorIsJsonOnEveryPlatform() {
4456
assertEveryChunkIsJson(
@@ -52,29 +64,30 @@ void aTextTurnCarryingTheLineSeparatorIsJsonOnEveryPlatform() {
5264
/**
5365
* The default branch of {@code quote}, not the named cases: a control character nobody listed is
5466
* what reopened the gap, so every one of them is checked rather than the four with a short escape.
67+
* Valid JSON is not enough -- any hexadecimal or short escape parses -- so each must also come back
68+
* as itself: a wrong code point, or a CR written with the escape of an LF, parses just as well.
5569
*/
5670
@Test
57-
void everyControlCharacterSurvivesAsJson() {
71+
void everyControlCharacterSurvivesAsJson() throws Exception {
5872
for (int c = 0; c < 0x20; c++) {
73+
String what = "U+" + String.format("%04X", c);
5974
String text = "a" + (char) c + "b";
60-
assertEveryChunkIsJson("U+" + String.format("%04X", c), ScriptedBackend.textTurn(text));
75+
List<String> chunks = ScriptedBackend.textTurn(text);
76+
assertEveryChunkIsJson(what, chunks);
77+
assertThat(what, contentOf(chunks), is(text));
6178
}
62-
assertEveryChunkIsJson("quote and backslash", ScriptedBackend.textTurn("say \"hi\"\\ now"));
63-
assertEveryChunkIsJson("DEL and beyond", ScriptedBackend.textTurn("a\u007fb\u0085c
d"));
79+
String quoted = "say \"hi\"\\ now";
80+
assertEveryChunkIsJson("quote and backslash", ScriptedBackend.textTurn(quoted));
81+
assertThat("quote and backslash", contentOf(ScriptedBackend.textTurn(quoted)), is(quoted));
82+
String beyond = "a\u007fb\u0085c
d";
83+
assertEveryChunkIsJson("DEL and beyond", ScriptedBackend.textTurn(beyond));
84+
assertThat("DEL and beyond", contentOf(ScriptedBackend.textTurn(beyond)), is(beyond));
6485
}
6586

6687
/** A chunk must also carry the text back unchanged -- escaping may not lose or alter it. */
6788
@Test
6889
void theTextComesBackOutOfTheChunkUnchanged() throws Exception {
6990
String text = "working" + System.lineSeparator() + "\tdone \"quoted\" \\ U+0001:\u0001";
70-
List<String> chunks = ScriptedBackend.textTurn(text);
71-
String content = null;
72-
for (String chunk : chunks) {
73-
var delta = MAPPER.readTree(chunk).path("choices").path(0).path("delta");
74-
if (delta.path("content").isTextual()) {
75-
content = delta.path("content").asText();
76-
}
77-
}
78-
assertThat(content, is(text));
91+
assertThat(contentOf(ScriptedBackend.textTurn(text)), is(text));
7992
}
8093
}

0 commit comments

Comments
 (0)