From 39266bd2b050b98aa4b857855f580180dac56937 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 16:38:35 +0000 Subject: [PATCH] test: restore PIT 100% (ChatTranscript.removePendingUserTurn) + jsonSchemaToGrammar guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the mutation-coverage gate that PR #258 (audit correctness fixes) dropped to 97%. - ChatTranscript.removePendingUserTurn() was added in #258 (for Session.cancelStream's stream rollback) with zero test coverage. ChatTranscript is a PIT target class (value.*), so all 6 of its mutants survived (NO_COVERAGE) → score 97% < 100% threshold, failing the Ubuntu job. Add three ChatTranscriptTest cases — trailing user turn removed (true + rolled back), assistant-ending round (false + intact), empty transcript (false + no throw) — which kill all 6 mutants (2 NegateConditionals, 2 MathMutator, the true/false return-value mutators). - Add testJsonSchemaToGrammarRejectsMalformedSchema: a regression for the Tier 1 JNI exception-boundary fix. A malformed schema passed to the public static LlamaModel.jsonSchemaToGrammar(String) used to let a C++ json::parse exception cross the JNI boundary and abort the JVM; it must now surface as a catchable LlamaException. (Model-gated by the class @BeforeAll, like the existing happy-path test, so it is exercised in the CI Java Tests jobs.) Verified locally: full configured PIT (mvn test-compile org.pitest:pitest-maven:mutationCoverage) 234/234 mutations killed (100%), BUILD SUCCESS; ChatTranscriptTest 15/15 green; Spotless clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Qg1mYW7hHjtVvAfMeMEmPq --- .../net/ladenthin/llama/LlamaModelTest.java | 8 ++++ .../llama/value/ChatTranscriptTest.java | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/test/java/net/ladenthin/llama/LlamaModelTest.java b/src/test/java/net/ladenthin/llama/LlamaModelTest.java index c89cb3fb..ce3d7d16 100644 --- a/src/test/java/net/ladenthin/llama/LlamaModelTest.java +++ b/src/test/java/net/ladenthin/llama/LlamaModelTest.java @@ -691,6 +691,14 @@ public void testJsonSchemaToGrammar() { assertEquals(expectedGrammar, actualGrammar); } + @Test + public void testJsonSchemaToGrammarRejectsMalformedSchema() { + // Regression for the JNI exception-boundary fix (audit Tier 1, PR #258): a malformed schema + // string used to let a C++ json::parse exception escape across the JNI boundary and abort the + // JVM. It must now surface as a catchable LlamaException instead of crashing the process. + assertThrows(LlamaException.class, () -> LlamaModel.jsonSchemaToGrammar("{ this is not valid json")); + } + @Test public void testTemplate() { diff --git a/src/test/java/net/ladenthin/llama/value/ChatTranscriptTest.java b/src/test/java/net/ladenthin/llama/value/ChatTranscriptTest.java index 7051c6a4..c064fddb 100644 --- a/src/test/java/net/ladenthin/llama/value/ChatTranscriptTest.java +++ b/src/test/java/net/ladenthin/llama/value/ChatTranscriptTest.java @@ -109,6 +109,44 @@ void appendUserAndAssistantSeparatelyMatchAppendRound() { assertThat("atomic-round and split-commit must converge", b.snapshot(), is(a.snapshot())); } + @Test + @DisplayName("removePendingUserTurn drops a trailing user turn and reports true") + void removePendingUserTurnDropsTrailingUserTurn() { + ChatTranscript t = new ChatTranscript(null); + t.appendUserTurn("pending question"); + assertThat("precondition: one dangling user turn", t.size(), is(1)); + + boolean removed = t.removePendingUserTurn(); + + assertThat("a dangling user turn must be reported as removed", removed, is(true)); + assertThat("transcript is rolled back to its pre-stream (empty) shape", t.size(), is(0)); + } + + @Test + @DisplayName("removePendingUserTurn is a no-op when the last turn is an assistant turn") + void removePendingUserTurnKeepsCommittedRound() { + ChatTranscript t = new ChatTranscript(null); + t.appendRound("q", "a"); // last turn is "assistant", not a dangling user turn + + boolean removed = t.removePendingUserTurn(); + + assertThat("a committed round has no dangling user turn to remove", removed, is(false)); + assertThat("the committed round must be left intact", t.size(), is(2)); + assertThat(t.snapshot().get(0).getRole(), is("user")); + assertThat(t.snapshot().get(1).getRole(), is("assistant")); + } + + @Test + @DisplayName("removePendingUserTurn is a safe no-op on an empty transcript") + void removePendingUserTurnNoOpOnEmptyTranscript() { + ChatTranscript t = new ChatTranscript("system"); + + boolean removed = t.removePendingUserTurn(); + + assertThat("an empty transcript has nothing to remove", removed, is(false)); + assertThat(t.size(), is(0)); + } + @Test @DisplayName("messagesWithPendingUserTurn does NOT mutate the transcript") void messagesWithPendingUserTurnDoesNotMutate() {