From dae7fc9719e0053006a8a7b67ccdd22dbf8f6a95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:08:33 +0000 Subject: [PATCH 1/4] Initial plan From 88c2ee32ef8ebe6bf583e4f12a44bb44049ade63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:25:48 +0000 Subject: [PATCH 2/4] Add test demonstrating Reject messages not resent in current code Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../src/test/java/quickfix/SessionTest.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/quickfixj-core/src/test/java/quickfix/SessionTest.java b/quickfixj-core/src/test/java/quickfix/SessionTest.java index aa393eb06..8b4330a1a 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionTest.java @@ -1398,6 +1398,110 @@ public void testResendMessagesWithIncorrectChecksum() throws Exception { } } + // Test for issue #597 - demonstrates current behavior where Reject messages are NOT resent + // This test documents the bug before PR #1124's fix + @Test + public void testRejectMessagesNotResentInCurrentCode() throws Exception { + final UnitTestApplication application = new UnitTestApplication(); + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + + // Create session with default settings (ForceResendWhenCorruptedStore=false) + try (Session session = SessionFactoryTestSupport.createSession(sessionID, application, false, false, true, true, null)) { + // Use a responder that captures all sent messages + FailingResponder responder = new FailingResponder(100); // Allow many successful sends + session.setResponder(responder); + final SessionState state = getSessionState(session); + + // Logon + final Logon logonToSend = new Logon(); + setUpHeader(session.getSessionID(), logonToSend, true, 1); + logonToSend.setInt(HeartBtInt.FIELD, 30); + logonToSend.setInt(EncryptMethod.FIELD, EncryptMethod.NONE_OTHER); + logonToSend.toString(); // calculate length/checksum + session.next(logonToSend); + + // Send messages that will be stored: + // seq 2: application message + session.send(createAppMessage(2)); + // seq 3: Reject message (session-level but should be resent per FIX spec) + Message rejectMsg = createReject(3, 1); + session.send(rejectMsg); + // seq 4: application message + session.send(createAppMessage(4)); + // seq 5: Heartbeat message (session-level, should NOT be resent) + Message heartbeatMsg = createHeartbeatMessage(5); + session.send(heartbeatMsg); + + // Clear the sent messages from initial send + int initialMessageCount = responder.sentMessages.size(); + responder.sentMessages.clear(); + responder.sendCallCount = 0; + + // Request resend of messages 1-5 + Message resendRequest = createResendRequest(100, 1); + resendRequest.toString(); // calculate length/checksum + processMessage(session, resendRequest); + + // Verify messages sent during resend + List resentMessages = new ArrayList<>(); + for (String msgData : responder.sentMessages) { + resentMessages.add(new Message(msgData)); + } + + // Current behavior verification: + // 1. Should have sent: SequenceReset(1-2), AppMsg(2), SequenceReset(3-4), AppMsg(4), SequenceReset(5-6) + // The Reject message should be included in a gap fill (bug) + + boolean foundSeqReset1 = false; // Gap fill for Logon (seq 1) + boolean foundAppMsg2 = false; + boolean foundSeqReset3 = false; // Gap fill that includes Reject (seq 3) - this is the bug + boolean foundAppMsg4 = false; + boolean foundSeqReset5 = false; // Gap fill for Heartbeat (seq 5) + boolean foundRejectResend = false; + + for (Message msg : resentMessages) { + String msgType = msg.getHeader().getString(MsgType.FIELD); + int msgSeqNum = msg.getHeader().getInt(MsgSeqNum.FIELD); + + if (msgType.equals(SequenceReset.MSGTYPE)) { + boolean isGapFill = msg.isSetField(GapFillFlag.FIELD) && msg.getBoolean(GapFillFlag.FIELD); + int newSeqNo = msg.getInt(NewSeqNo.FIELD); + + if (isGapFill && msgSeqNum == 1 && newSeqNo == 2) { + foundSeqReset1 = true; // Gap fill over Logon + } else if (isGapFill && msgSeqNum == 3 && newSeqNo == 4) { + foundSeqReset3 = true; // Gap fill over Reject (bug - should not gap fill) + } else if (isGapFill && msgSeqNum == 5 && newSeqNo == 6) { + foundSeqReset5 = true; // Gap fill over Heartbeat + } + } else if (msgType.equals(News.MSGTYPE)) { + // Application message + boolean isPossDup = msg.getHeader().isSetField(PossDupFlag.FIELD) + && msg.getHeader().getBoolean(PossDupFlag.FIELD); + assertTrue("Application message should have PossDupFlag set", isPossDup); + + if (msgSeqNum == 2) { + foundAppMsg2 = true; + } else if (msgSeqNum == 4) { + foundAppMsg4 = true; + } + } else if (msgType.equals(Reject.MSGTYPE)) { + foundRejectResend = true; + } + } + + // Verify current behavior (documents the bug) + assertTrue("Should send gap fill for Logon (seq 1)", foundSeqReset1); + assertTrue("Should resend application message (seq 2)", foundAppMsg2); + assertTrue("Should send gap fill for Reject (seq 3) - this is the bug", foundSeqReset3); + assertTrue("Should resend application message (seq 4)", foundAppMsg4); + assertTrue("Should send gap fill for Heartbeat (seq 5)", foundSeqReset5); + assertFalse("Reject message should NOT be resent in current code (bug)", foundRejectResend); + + assertFalse(state.isResendRequested()); + } + } + // QFJ-493 @Test public void testGapFillSatisfiesResendRequest() throws Exception { From 2d3373fbe9ecd0be1019ff60f6974e7be80e9783 Mon Sep 17 00:00:00 2001 From: Christoph John Date: Thu, 29 Jan 2026 16:37:42 +0100 Subject: [PATCH 3/4] Add UnitTestResponder with sentMessages tracking --- .../test/java/quickfix/UnitTestResponder.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 quickfixj-core/src/test/java/quickfix/UnitTestResponder.java diff --git a/quickfixj-core/src/test/java/quickfix/UnitTestResponder.java b/quickfixj-core/src/test/java/quickfix/UnitTestResponder.java new file mode 100644 index 000000000..10edba78b --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/UnitTestResponder.java @@ -0,0 +1,18 @@ +package quickfix; + +import java.util.ArrayList; +import java.util.List; + +public class UnitTestResponder extends Responder { + List sentMessages = new ArrayList<>(); + + @Override + public void send(String message) { + sentMessages.add(message); + super.send(message); + } + + public List getSentMessages() { + return sentMessages; + } +} \ No newline at end of file From fad489eb029127f1c9e2c3ce67ed7abce534fe60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:54:48 +0000 Subject: [PATCH 4/4] Replace FailingResponder with mock Responder and ArgumentCaptor Co-authored-by: chrjohn <6644028+chrjohn@users.noreply.github.com> --- .../src/test/java/quickfix/SessionTest.java | 23 +++++++++++-------- .../test/java/quickfix/UnitTestResponder.java | 18 --------------- 2 files changed, 14 insertions(+), 27 deletions(-) delete mode 100644 quickfixj-core/src/test/java/quickfix/UnitTestResponder.java diff --git a/quickfixj-core/src/test/java/quickfix/SessionTest.java b/quickfixj-core/src/test/java/quickfix/SessionTest.java index 8b4330a1a..8ef1d6c15 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionTest.java @@ -1407,9 +1407,10 @@ public void testRejectMessagesNotResentInCurrentCode() throws Exception { // Create session with default settings (ForceResendWhenCorruptedStore=false) try (Session session = SessionFactoryTestSupport.createSession(sessionID, application, false, false, true, true, null)) { - // Use a responder that captures all sent messages - FailingResponder responder = new FailingResponder(100); // Allow many successful sends - session.setResponder(responder); + // Use a mock responder to capture all sent messages + Responder mockResponder = mock(Responder.class); + when(mockResponder.send(anyString())).thenReturn(true); + session.setResponder(mockResponder); final SessionState state = getSessionState(session); // Logon @@ -1432,19 +1433,23 @@ public void testRejectMessagesNotResentInCurrentCode() throws Exception { Message heartbeatMsg = createHeartbeatMessage(5); session.send(heartbeatMsg); - // Clear the sent messages from initial send - int initialMessageCount = responder.sentMessages.size(); - responder.sentMessages.clear(); - responder.sendCallCount = 0; + // Reset mock to track only resend messages + Mockito.reset(mockResponder); + when(mockResponder.send(anyString())).thenReturn(true); // Request resend of messages 1-5 Message resendRequest = createResendRequest(100, 1); resendRequest.toString(); // calculate length/checksum processMessage(session, resendRequest); - // Verify messages sent during resend + // Capture all messages sent during resend + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + verify(mockResponder, atLeastOnce()).send(messageCaptor.capture()); + List sentMessages = messageCaptor.getAllValues(); + + // Parse sent messages List resentMessages = new ArrayList<>(); - for (String msgData : responder.sentMessages) { + for (String msgData : sentMessages) { resentMessages.add(new Message(msgData)); } diff --git a/quickfixj-core/src/test/java/quickfix/UnitTestResponder.java b/quickfixj-core/src/test/java/quickfix/UnitTestResponder.java deleted file mode 100644 index 10edba78b..000000000 --- a/quickfixj-core/src/test/java/quickfix/UnitTestResponder.java +++ /dev/null @@ -1,18 +0,0 @@ -package quickfix; - -import java.util.ArrayList; -import java.util.List; - -public class UnitTestResponder extends Responder { - List sentMessages = new ArrayList<>(); - - @Override - public void send(String message) { - sentMessages.add(message); - super.send(message); - } - - public List getSentMessages() { - return sentMessages; - } -} \ No newline at end of file