@@ -1234,6 +1234,91 @@ void main() {
12341234 // ===== WEBSOCKET TESTS =====
12351235
12361236 group ('WebSocket Events' , () {
1237+ test (
1238+ 'WS task_created DURING initial load does NOT create duplicate' ,
1239+ () async {
1240+ // BUG: WebSocket task_created arrives DURING initial HTTP load,
1241+ // BEFORE HTTP response. Then HTTP arrives and REPLACES the list,
1242+ // but HTTP data + WS data = duplicate!
1243+ //
1244+ // Timeline:
1245+ // 1. Login succeeds
1246+ // 2. Component starts GET /tasks (async)
1247+ // 3. WebSocket connects, server sends task_created event
1248+ // 4. WS handler adds task to state (currently empty)
1249+ // 5. HTTP /tasks responds with SAME task
1250+ // 6. _loadTasks does tasksState.set() - REPLACES state!
1251+ //
1252+ // Result: WS added task + HTTP replaced with same task = OK normally
1253+ // BUT if we don't deduplicate in set(), we can get race issues.
1254+ //
1255+ // Actually the REAL bug: WS arrives AFTER HTTP completes, but for
1256+ // a task that was ALREADY in the HTTP response. Let's test that.
1257+
1258+ Future <Result <JSObject , String >> racingFetch (
1259+ String url, {
1260+ String method = 'GET' ,
1261+ String ? token,
1262+ Map <String , Object ?>? body,
1263+ }) async {
1264+ if (url.contains ('/auth/login' )) {
1265+ return Success (
1266+ createJSObject ({
1267+ 'success' : true ,
1268+ 'data' : {
1269+ 'token' : 'tok' ,
1270+ 'user' : {'name' : 'Alice' },
1271+ },
1272+ }),
1273+ );
1274+ }
1275+ if (url.contains ('/tasks' ) && method == 'GET' ) {
1276+ // HTTP returns task, but ALSO WS will send same task later
1277+ return Success (
1278+ createJSObject ({
1279+ 'success' : true ,
1280+ 'data' : [
1281+ {'id' : 'task_47' , 'title' : 'Existing Task' , 'completed' : false },
1282+ ],
1283+ }),
1284+ );
1285+ }
1286+ throw StateError ('No mock for $method $url ' );
1287+ }
1288+
1289+ final result = render (MobileApp (fetchFn: racingFetch));
1290+
1291+ // Login
1292+ final inputs = result.container.querySelectorAll ('input' );
1293+ await userType (inputs[0 ], 'a@b.com' );
1294+ await userType (inputs[1 ], 'pass' );
1295+ fireClick (result.container.querySelectorAll ('button' ).first);
1296+
1297+ // Wait for task list to load
1298+ await waitForText (result, 'Existing Task' );
1299+
1300+ // WS sends task_created for same task that's already loaded!
1301+ // This simulates server broadcasting to all clients
1302+ simulateWsMessage (
1303+ '{"type":"task_created","data":'
1304+ '{"id":"task_47","title":"Existing Task","completed":false}}' ,
1305+ );
1306+
1307+ await Future <void >.delayed (const Duration (milliseconds: 100 ));
1308+
1309+ // Count occurrences - must be exactly 1!
1310+ final textContent = result.container.textContent;
1311+ final matches = 'Existing Task' .allMatches (textContent).length;
1312+ expect (
1313+ matches,
1314+ 1 ,
1315+ reason: 'WS task_created for existing task should NOT duplicate!' ,
1316+ );
1317+
1318+ result.unmount ();
1319+ },
1320+ );
1321+
12371322 test ('task_created event adds task to list' , () async {
12381323 final mockFetch = createMockFetch ({
12391324 '/auth/login' : {
0 commit comments