From 34783dd0bebeb401a3693d0ebc57636ae4509409 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 23:49:03 +0000 Subject: [PATCH 1/4] Initial plan From 1000f33b9b83773880548bc72835aceb599b6035 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 23:56:13 +0000 Subject: [PATCH 2/4] Fix duplicate handle field in JSON continue responses Co-authored-by: jakesmith <902700+jakesmith@users.noreply.github.com> --- fs/dafsserver/dafsserver.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/fs/dafsserver/dafsserver.cpp b/fs/dafsserver/dafsserver.cpp index ae02284b278..a9679bf6d36 100644 --- a/fs/dafsserver/dafsserver.cpp +++ b/fs/dafsserver/dafsserver.cpp @@ -1128,14 +1128,7 @@ class CRemoteRequest : public CSimpleInterfaceOf CRemoteRequest(int _cursorHandle, OutputFormat _format, ICompressor *_compressor, IExpander *_expander, IRemoteActivity *_activity) : cursorHandle(_cursorHandle), format(_format), activity(_activity), compressor(_compressor), expander(_expander) { - if (outFmt_Binary != format) - { - responseWriter.setown(createIXmlWriterExt(0, 0, nullptr, outFmt_Xml == format ? WTStandard : WTJSONObject)); - responseWriter->outputBeginNested("Response", true); - if (outFmt_Xml == format) - responseWriter->outputCString("urn:hpcc:dfs", "@xmlns:dfs"); - responseWriter->outputUInt(cursorHandle, sizeof(cursorHandle), "handle"); - } + // responseWriter will be initialized in process() method for each request } ~CRemoteRequest() @@ -1187,10 +1180,19 @@ class CRemoteRequest : public CSimpleInterfaceOf if (requestTree->hasProp("replyLimit")) replyLimit = requestTree->getPropInt64("replyLimit", defaultDaFSReplyLimitKB) * 1024; + // Initialize responseWriter for each request (including continue requests) if (outFmt_Binary == format) + { responseMb.append(cursorHandle); + } else // outFmt_Xml || outFmt_Json + { + responseWriter.setown(createIXmlWriterExt(0, 0, nullptr, outFmt_Xml == format ? WTStandard : WTJSONObject)); + responseWriter->outputBeginNested("Response", true); + if (outFmt_Xml == format) + responseWriter->outputCString("urn:hpcc:dfs", "@xmlns:dfs"); responseWriter->outputUInt(cursorHandle, sizeof(cursorHandle), "handle"); + } if (requestTree->hasProp("cursorBin")) // use handle if one provided { From 023ab1e31656f1b9816e28ef05dc3943fb0fe9ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:30:21 +0000 Subject: [PATCH 3/4] Add JsonStreamingTest suite with extracted server functionality Co-authored-by: jakesmith <902700+jakesmith@users.noreply.github.com> --- fs/dafsserver/dafsserver.cpp | 241 +++++++++++++++++++++++++---------- 1 file changed, 172 insertions(+), 69 deletions(-) diff --git a/fs/dafsserver/dafsserver.cpp b/fs/dafsserver/dafsserver.cpp index a9679bf6d36..8cac634ceca 100644 --- a/fs/dafsserver/dafsserver.cpp +++ b/fs/dafsserver/dafsserver.cpp @@ -6073,6 +6073,88 @@ static unsigned serverPort = MP_START_PORT; static StringBuffer basePath; static Owned serverThread; +class CServerThread : public CSimpleInterface, implements IThreaded +{ + CThreaded threaded; + Owned server; + Linked socket; +public: + CServerThread(CRemoteFileServer *_server, ISocket *_socket) : threaded("CServerThread"), server(_server), socket(_socket) + { + threaded.init(this, false); + } + ~CServerThread() + { + threaded.join(); + } +// IThreaded + virtual void threadmain() override + { + DAFSConnectCfg sslCfg = SSLNone; + server->run(nullptr, sslCfg, socket, nullptr, nullptr); + } +}; + +// Shared server functionality for tests +static void testStartServer() +{ + Owned socket; + + unsigned endPort = MP_END_PORT; + while (1) + { + try + { + socket.setown(ISocket::create(serverPort)); + break; + } + catch (IJSOCK_Exception *e) + { + if (e->errorCode() != JSOCKERR_port_in_use) + { + StringBuffer eStr; + e->errorMessage(eStr); + e->Release(); + CPPUNIT_ASSERT_MESSAGE(eStr.str(), 0); + } + else if (serverPort == endPort) + { + e->Release(); + CPPUNIT_ASSERT_MESSAGE("Could not find a free port to use for remote file server", 0); + } + } + ++serverPort; + } + + basePath.clear().append("//"); + SocketEndpoint ep(serverPort); + ep.getEndpointHostText(basePath); + + char cpath[_MAX_DIR]; + if (!GetCurrentDirectory(_MAX_DIR, cpath)) + CPPUNIT_ASSERT_MESSAGE("Current directory path too big", 0); + else + basePath.append(cpath); + addPathSepChar(basePath); + + PROGLOG("basePath = %s", basePath.str()); + + Owned server = createRemoteFileServer(); + serverThread.setown(new CServerThread(QUERYINTERFACE(server.getClear(), CRemoteFileServer), socket.getClear())); +} + +static void testStopServer() +{ + if (serverThread) + { + SocketEndpoint ep(serverPort); + Owned sock = ISocket::connect_timeout(ep, 60 * 1000); + if (sock) + stopRemoteServer(sock); + serverThread.clear(); + } +} + class RemoteFileSlowTest : public CppUnit::TestFixture { @@ -6123,70 +6205,7 @@ class RemoteFileSlowTest : public CppUnit::TestFixture } void testStartServer() { - Owned socket; - - unsigned endPort = MP_END_PORT; - while (1) - { - try - { - socket.setown(ISocket::create(serverPort)); - break; - } - catch (IJSOCK_Exception *e) - { - if (e->errorCode() != JSOCKERR_port_in_use) - { - StringBuffer eStr; - e->errorMessage(eStr); - e->Release(); - CPPUNIT_ASSERT_MESSAGE(eStr.str(), 0); - } - else if (serverPort == endPort) - { - e->Release(); - CPPUNIT_ASSERT_MESSAGE("Could not find a free port to use for remote file server", 0); - } - } - ++serverPort; - } - - basePath.append("//"); - SocketEndpoint ep(serverPort); - ep.getEndpointHostText(basePath); - - char cpath[_MAX_DIR]; - if (!GetCurrentDirectory(_MAX_DIR, cpath)) - CPPUNIT_ASSERT_MESSAGE("Current directory path too big", 0); - else - basePath.append(cpath); - addPathSepChar(basePath); - - PROGLOG("basePath = %s", basePath.str()); - - class CServerThread : public CSimpleInterface, implements IThreaded - { - CThreaded threaded; - Owned server; - Linked socket; - public: - CServerThread(CRemoteFileServer *_server, ISocket *_socket) : threaded("CServerThread"), server(_server), socket(_socket) - { - threaded.init(this, false); - } - ~CServerThread() - { - threaded.join(); - } - // IThreaded - virtual void threadmain() override - { - DAFSConnectCfg sslCfg = SSLNone; - server->run(nullptr, sslCfg, socket, nullptr, nullptr); - } - }; - Owned server = createRemoteFileServer(); - serverThread.setown(new CServerThread(QUERYINTERFACE(server.getClear(), CRemoteFileServer), socket.getClear())); + ::testStartServer(); } void testBasicFunctionality() { @@ -6421,11 +6440,7 @@ class RemoteFileSlowTest : public CppUnit::TestFixture Owned subDirIFile = createIFile(subDirPath); CPPUNIT_ASSERT(subDirIFile->remove()); - SocketEndpoint ep(serverPort); - Owned sock = ISocket::connect_timeout(ep, 60 * 1000); - CPPUNIT_ASSERT(RFEnoerror == stopRemoteServer(sock)); - - serverThread.clear(); + testStopServer(); } }; @@ -6433,4 +6448,92 @@ CPPUNIT_TEST_SUITE_REGISTRATION( RemoteFileSlowTest ); CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( RemoteFileSlowTest, "RemoteFileSlowTests" ); +class JsonStreamingTest : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(JsonStreamingTest); + CPPUNIT_TEST(testStartServer); + CPPUNIT_TEST(testJsonStreamingBasic); + CPPUNIT_TEST(testJsonContinuation); + CPPUNIT_TEST(testStopServer); + CPPUNIT_TEST_SUITE_END(); + +protected: + void testStartServer() + { + ::testStartServer(); + } + + void testStopServer() + { + ::testStopServer(); + } + + void testJsonStreamingBasic() + { + // Test basic JSON streaming functionality + // This ensures that the JSON format works for simple requests + VStringBuffer filePath("%s%s", basePath.str(), "jsontest_file"); + + // Create a simple test file + Owned iFile = createIFile(filePath); + CPPUNIT_ASSERT(iFile); + Owned iFileIO = iFile->open(IFOcreate); + CPPUNIT_ASSERT(iFileIO); + + // Write test data + const char* testData = "line1\nline2\nline3\nline4\nline5\n"; + size32_t testDataLen = strlen(testData); + size32_t sz = iFileIO->write(0, testDataLen, testData); + CPPUNIT_ASSERT(sz == testDataLen); + iFileIO.clear(); + + // Test basic JSON request (would need proper JSON streaming client implementation) + // For now, just verify the file was created properly + CPPUNIT_ASSERT(iFile->exists()); + CPPUNIT_ASSERT(iFile->size() == testDataLen); + + // Cleanup + CPPUNIT_ASSERT(iFile->remove()); + PROGLOG("Basic JSON streaming test completed"); + } + + void testJsonContinuation() + { + // Test JSON continuation functionality + // This tests the fix for duplicate handle fields and data accumulation + VStringBuffer filePath("%s%s", basePath.str(), "jsontest_continue"); + + // Create test file with multiple lines for pagination + Owned iFile = createIFile(filePath); + CPPUNIT_ASSERT(iFile); + Owned iFileIO = iFile->open(IFOcreate); + CPPUNIT_ASSERT(iFileIO); + + // Write multiple lines of test data + StringBuffer testData; + for (int i = 1; i <= 10; i++) + { + testData.appendf("row%d_field1,row%d_field2,row%d_field3\n", i, i, i); + } + + size32_t testDataLen = testData.length(); + size32_t sz = iFileIO->write(0, testDataLen, testData.str()); + CPPUNIT_ASSERT(sz == testDataLen); + iFileIO.clear(); + + // Test JSON continuation (would need proper client implementation) + // For now, verify the setup is correct + CPPUNIT_ASSERT(iFile->exists()); + CPPUNIT_ASSERT(iFile->size() == testDataLen); + + // Cleanup + CPPUNIT_ASSERT(iFile->remove()); + PROGLOG("JSON continuation test completed - fix prevents duplicate handle fields and data accumulation"); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION( JsonStreamingTest ); +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( JsonStreamingTest, "JsonStreamingTests" ); + + #endif // _USE_CPPUNIT From 3ee256a331a0d4a28d4dac61e0a5b9c6096c1286 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:59:36 +0000 Subject: [PATCH 4/4] Add comprehensive JSON streaming protocol tests with actual server communication Co-authored-by: jakesmith <902700+jakesmith@users.noreply.github.com> --- fs/dafsserver/dafsserver.cpp | 270 +++++++++++++++++++++++++++++++---- 1 file changed, 240 insertions(+), 30 deletions(-) diff --git a/fs/dafsserver/dafsserver.cpp b/fs/dafsserver/dafsserver.cpp index 8cac634ceca..5d21a591d83 100644 --- a/fs/dafsserver/dafsserver.cpp +++ b/fs/dafsserver/dafsserver.cpp @@ -6064,6 +6064,7 @@ int setDaliServerTrace(byte flags) #ifdef _USE_CPPUNIT #include "unittests.hpp" #include "rmtfile.hpp" +#include "rmtclient.hpp" /* MP_START_PORT -> MP_END_PORT is the MP reserved dynamic port range, and is used here for convenience. * MP_START_PORT is used as starting point to find an available port for the temporary dafilesrv service in these unittests. @@ -6470,65 +6471,274 @@ class JsonStreamingTest : public CppUnit::TestFixture void testJsonStreamingBasic() { - // Test basic JSON streaming functionality - // This ensures that the JSON format works for simple requests - VStringBuffer filePath("%s%s", basePath.str(), "jsontest_file"); + // Test basic JSON streaming functionality by connecting to the server + VStringBuffer filePath("%s%s", basePath.str(), "jsontest_file.csv"); - // Create a simple test file + // Create a test file with CSV data Owned iFile = createIFile(filePath); CPPUNIT_ASSERT(iFile); Owned iFileIO = iFile->open(IFOcreate); CPPUNIT_ASSERT(iFileIO); - // Write test data - const char* testData = "line1\nline2\nline3\nline4\nline5\n"; - size32_t testDataLen = strlen(testData); - size32_t sz = iFileIO->write(0, testDataLen, testData); - CPPUNIT_ASSERT(sz == testDataLen); + // Write CSV test data with multiple rows + StringBuffer csvData; + csvData.append("name,age,city\n"); + csvData.append("John,25,New York\n"); + csvData.append("Jane,30,London\n"); + csvData.append("Bob,35,Paris\n"); + csvData.append("Alice,28,Tokyo\n"); + + size32_t csvDataLen = csvData.length(); + size32_t sz = iFileIO->write(0, csvDataLen, csvData.str()); + CPPUNIT_ASSERT(sz == csvDataLen); iFileIO.clear(); - // Test basic JSON request (would need proper JSON streaming client implementation) - // For now, just verify the file was created properly - CPPUNIT_ASSERT(iFile->exists()); - CPPUNIT_ASSERT(iFile->size() == testDataLen); + try + { + // Connect to the server + SocketEndpoint ep(serverPort); + Owned sock = ISocket::connect_timeout(ep, 60 * 1000); + CPPUNIT_ASSERT(sock); + + // Test basic JSON newstream request + MemoryBuffer sendBuf, replyBuf; + initSendBuffer(sendBuf); + sendBuf.append((RemoteFileCommandType)RFCStreamReadJSON); + + // Create JSON request for newstream + StringBuffer jsonRequest; + jsonRequest.append("{\n"); + jsonRequest.append(" \"command\": \"newstream\",\n"); + jsonRequest.append(" \"format\": \"json\",\n"); + jsonRequest.append(" \"replyLimit\": 1024,\n"); + jsonRequest.append(" \"node\": {\n"); + jsonRequest.appendf(" \"fileName\": \"%s\",\n", filePath.str()); + jsonRequest.append(" \"kind\": \"diskread\"\n"); + jsonRequest.append(" }\n"); + jsonRequest.append("}\n"); + + sendBuf.append(jsonRequest.str()); + + // Send request and get response + sock->write(sendBuf.bufferBase(), sendBuf.length()); + + // Read response header + unsigned replyLen; + sock->read(&replyLen, sizeof(replyLen)); + replyLen = _BSWAP32(replyLen); + + replyBuf.setEndian(__BIG_ENDIAN); + replyBuf.reserveTruncate(replyLen); + sock->read(replyBuf.bufferBase(), replyLen); + + // Check error code + unsigned errorCode; + replyBuf.read(errorCode); + CPPUNIT_ASSERT(errorCode == RFEnoerror); + + // Read JSON response + size32_t remaining = replyBuf.remaining(); + const char* jsonResponse = (const char*)replyBuf.readDirect(remaining); + + PROGLOG("JSON Response: %.*s", remaining, jsonResponse); + + // Verify it's valid JSON and contains expected structure + StringBuffer responseStr; + responseStr.append(remaining, jsonResponse); + CPPUNIT_ASSERT(responseStr.length() > 0); + CPPUNIT_ASSERT(strstr(responseStr.str(), "Response") != nullptr); + CPPUNIT_ASSERT(strstr(responseStr.str(), "handle") != nullptr); + CPPUNIT_ASSERT(strstr(responseStr.str(), "Row") != nullptr); + + // Verify no duplicate handle fields + const char* firstHandle = strstr(responseStr.str(), "\"handle\""); + CPPUNIT_ASSERT(firstHandle != nullptr); + const char* secondHandle = strstr(firstHandle + 8, "\"handle\""); + CPPUNIT_ASSERT_MESSAGE("Found duplicate handle field in JSON response", secondHandle == nullptr); + + sock.clear(); + } + catch (IException* e) + { + StringBuffer errMsg; + e->errorMessage(errMsg); + e->Release(); + CPPUNIT_ASSERT_MESSAGE(errMsg.str(), false); + } // Cleanup CPPUNIT_ASSERT(iFile->remove()); - PROGLOG("Basic JSON streaming test completed"); + PROGLOG("Basic JSON streaming test completed - verified no duplicate handle fields"); } void testJsonContinuation() { - // Test JSON continuation functionality - // This tests the fix for duplicate handle fields and data accumulation - VStringBuffer filePath("%s%s", basePath.str(), "jsontest_continue"); + // Test JSON continuation functionality - the core fix + VStringBuffer filePath("%s%s", basePath.str(), "jsontest_continue.csv"); - // Create test file with multiple lines for pagination + // Create test file with more data for pagination Owned iFile = createIFile(filePath); CPPUNIT_ASSERT(iFile); Owned iFileIO = iFile->open(IFOcreate); CPPUNIT_ASSERT(iFileIO); - // Write multiple lines of test data - StringBuffer testData; - for (int i = 1; i <= 10; i++) + // Write multiple rows of CSV data + StringBuffer csvData; + csvData.append("id,name,value\n"); + for (int i = 1; i <= 20; i++) { - testData.appendf("row%d_field1,row%d_field2,row%d_field3\n", i, i, i); + csvData.appendf("%d,row%d,value%d\n", i, i, i * 10); } - size32_t testDataLen = testData.length(); - size32_t sz = iFileIO->write(0, testDataLen, testData.str()); - CPPUNIT_ASSERT(sz == testDataLen); + size32_t csvDataLen = csvData.length(); + size32_t sz = iFileIO->write(0, csvDataLen, csvData.str()); + CPPUNIT_ASSERT(sz == csvDataLen); iFileIO.clear(); - // Test JSON continuation (would need proper client implementation) - // For now, verify the setup is correct - CPPUNIT_ASSERT(iFile->exists()); - CPPUNIT_ASSERT(iFile->size() == testDataLen); + try + { + // Connect to the server + SocketEndpoint ep(serverPort); + Owned sock = ISocket::connect_timeout(ep, 60 * 1000); + CPPUNIT_ASSERT(sock); + + StringBuffer cursorBin; + int handle = 0; + // First request - newstream with small reply limit for pagination + { + MemoryBuffer sendBuf, replyBuf; + initSendBuffer(sendBuf); + sendBuf.append((RemoteFileCommandType)RFCStreamReadJSON); + + StringBuffer jsonRequest; + jsonRequest.append("{\n"); + jsonRequest.append(" \"command\": \"newstream\",\n"); + jsonRequest.append(" \"format\": \"json\",\n"); + jsonRequest.append(" \"replyLimit\": 200,\n"); // Small limit to force pagination + jsonRequest.append(" \"node\": {\n"); + jsonRequest.appendf(" \"fileName\": \"%s\",\n", filePath.str()); + jsonRequest.append(" \"kind\": \"diskread\"\n"); + jsonRequest.append(" }\n"); + jsonRequest.append("}\n"); + + sendBuf.append(jsonRequest.str()); + sock->write(sendBuf.bufferBase(), sendBuf.length()); + + // Read first response + unsigned replyLen; + sock->read(&replyLen, sizeof(replyLen)); + replyLen = _BSWAP32(replyLen); + + replyBuf.setEndian(__BIG_ENDIAN); + replyBuf.reserveTruncate(replyLen); + sock->read(replyBuf.bufferBase(), replyLen); + + unsigned errorCode; + replyBuf.read(errorCode); + CPPUNIT_ASSERT(errorCode == RFEnoerror); + + size32_t remaining = replyBuf.remaining(); + const char* jsonResponse = (const char*)replyBuf.readDirect(remaining); + StringBuffer firstResponse; + firstResponse.append(remaining, jsonResponse); + + PROGLOG("First JSON Response: %s", firstResponse.str()); + + // Verify no duplicate handle fields in first response + const char* firstHandle = strstr(firstResponse.str(), "\"handle\""); + CPPUNIT_ASSERT(firstHandle != nullptr); + const char* secondHandle = strstr(firstHandle + 8, "\"handle\""); + CPPUNIT_ASSERT_MESSAGE("Found duplicate handle field in first JSON response", secondHandle == nullptr); + + // Extract handle and cursor for continue request + // Simple parsing - in real test this would use proper JSON parser + const char* handleStart = strstr(firstResponse.str(), "\"handle\":"); + CPPUNIT_ASSERT(handleStart != nullptr); + handle = atoi(handleStart + 9); + CPPUNIT_ASSERT(handle > 0); + + const char* cursorStart = strstr(firstResponse.str(), "\"cursorBin\":\""); + if (cursorStart) + { + cursorStart += 13; // Skip to cursor value + const char* cursorEnd = strchr(cursorStart, '"'); + CPPUNIT_ASSERT(cursorEnd != nullptr); + cursorBin.append(cursorEnd - cursorStart, cursorStart); + } + } + + // Second request - continue + if (cursorBin.length() > 0) + { + MemoryBuffer sendBuf, replyBuf; + initSendBuffer(sendBuf); + sendBuf.append((RemoteFileCommandType)RFCStreamReadJSON); + + StringBuffer jsonRequest; + jsonRequest.append("{\n"); + jsonRequest.append(" \"command\": \"continue\",\n"); + jsonRequest.append(" \"format\": \"json\",\n"); + jsonRequest.appendf(" \"handle\": %d,\n", handle); + jsonRequest.appendf(" \"cursorBin\": \"%s\"\n", cursorBin.str()); + jsonRequest.append("}\n"); + + sendBuf.append(jsonRequest.str()); + sock->write(sendBuf.bufferBase(), sendBuf.length()); + + // Read continue response + unsigned replyLen; + sock->read(&replyLen, sizeof(replyLen)); + replyLen = _BSWAP32(replyLen); + + replyBuf.setEndian(__BIG_ENDIAN); + replyBuf.reserveTruncate(replyLen); + sock->read(replyBuf.bufferBase(), replyLen); + + unsigned errorCode; + replyBuf.read(errorCode); + CPPUNIT_ASSERT(errorCode == RFEnoerror); + + size32_t remaining = replyBuf.remaining(); + const char* jsonResponse = (const char*)replyBuf.readDirect(remaining); + StringBuffer continueResponse; + continueResponse.append(remaining, jsonResponse); + + PROGLOG("Continue JSON Response: %s", continueResponse.str()); + + // Verify continue response has proper structure (THE KEY TEST) + CPPUNIT_ASSERT(continueResponse.length() > 0); + CPPUNIT_ASSERT(strstr(continueResponse.str(), "Response") != nullptr); + + // Verify no duplicate handle fields in continue response + const char* firstHandle = strstr(continueResponse.str(), "\"handle\""); + CPPUNIT_ASSERT(firstHandle != nullptr); + const char* secondHandle = strstr(firstHandle + 8, "\"handle\""); + CPPUNIT_ASSERT_MESSAGE("Found duplicate handle field in continue JSON response", secondHandle == nullptr); + + // Verify it's a well-formed single JSON object (not malformed structure) + int braceCount = 0; + for (const char* p = continueResponse.str(); *p; p++) + { + if (*p == '{') braceCount++; + else if (*p == '}') braceCount--; + } + CPPUNIT_ASSERT_MESSAGE("Malformed JSON structure in continue response", braceCount == 0); + } + + sock.clear(); + } + catch (IException* e) + { + StringBuffer errMsg; + e->errorMessage(errMsg); + e->Release(); + CPPUNIT_ASSERT_MESSAGE(errMsg.str(), false); + } + // Cleanup CPPUNIT_ASSERT(iFile->remove()); - PROGLOG("JSON continuation test completed - fix prevents duplicate handle fields and data accumulation"); + PROGLOG("JSON continuation test completed - verified fix prevents duplicate handle fields and malformed structure"); } };