@logtape/logtape 1.1.0-dev.338 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sink.test.ts CHANGED
@@ -11,6 +11,7 @@ import type { LogLevel } from "./level.ts";
11
11
  import type { LogRecord } from "./record.ts";
12
12
  import {
13
13
  type AsyncSink,
14
+ fingersCrossed,
14
15
  fromAsyncSink,
15
16
  getConsoleSink,
16
17
  getStreamSink,
@@ -873,3 +874,835 @@ test("fromAsyncSink() - empty async sink", async () => {
873
874
  // Test passes if no errors thrown
874
875
  assert(true);
875
876
  });
877
+
878
+ test("fingersCrossed() - basic functionality", () => {
879
+ const buffer: LogRecord[] = [];
880
+ const sink = fingersCrossed(buffer.push.bind(buffer));
881
+
882
+ // Add debug and info logs - should be buffered
883
+ sink(trace);
884
+ sink(debug);
885
+ sink(info);
886
+ assertEquals(buffer.length, 0); // Not flushed yet
887
+
888
+ // Add warning - still buffered (default trigger is error)
889
+ sink(warning);
890
+ assertEquals(buffer.length, 0);
891
+
892
+ // Add error - should trigger flush
893
+ sink(error);
894
+ assertEquals(buffer, [trace, debug, info, warning, error]);
895
+
896
+ // After trigger, logs pass through directly
897
+ sink(fatal);
898
+ assertEquals(buffer, [trace, debug, info, warning, error, fatal]);
899
+ });
900
+
901
+ test("fingersCrossed() - custom trigger level", () => {
902
+ const buffer: LogRecord[] = [];
903
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
904
+ triggerLevel: "warning",
905
+ });
906
+
907
+ // Add logs below warning
908
+ sink(trace);
909
+ sink(debug);
910
+ sink(info);
911
+ assertEquals(buffer.length, 0);
912
+
913
+ // Warning should trigger flush
914
+ sink(warning);
915
+ assertEquals(buffer, [trace, debug, info, warning]);
916
+
917
+ // Subsequent logs pass through
918
+ sink(error);
919
+ assertEquals(buffer, [trace, debug, info, warning, error]);
920
+ });
921
+
922
+ test("fingersCrossed() - buffer overflow protection", () => {
923
+ const buffer: LogRecord[] = [];
924
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
925
+ maxBufferSize: 3,
926
+ });
927
+
928
+ // Add more logs than buffer size
929
+ sink(trace);
930
+ sink(debug);
931
+ sink(info);
932
+ sink(warning); // Should drop trace
933
+ assertEquals(buffer.length, 0); // Still buffered
934
+
935
+ // Trigger flush
936
+ sink(error);
937
+ // Should only have last 3 records + error
938
+ assertEquals(buffer, [debug, info, warning, error]);
939
+ });
940
+
941
+ test("fingersCrossed() - multiple trigger events", () => {
942
+ const buffer: LogRecord[] = [];
943
+ const sink = fingersCrossed(buffer.push.bind(buffer));
944
+
945
+ // First batch
946
+ sink(debug);
947
+ sink(info);
948
+ sink(error); // Trigger
949
+ assertEquals(buffer, [debug, info, error]);
950
+
951
+ // After trigger, everything passes through
952
+ sink(debug);
953
+ assertEquals(buffer, [debug, info, error, debug]);
954
+
955
+ sink(error); // Another error
956
+ assertEquals(buffer, [debug, info, error, debug, error]);
957
+ });
958
+
959
+ test("fingersCrossed() - trigger includes fatal", () => {
960
+ const buffer: LogRecord[] = [];
961
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
962
+ triggerLevel: "error",
963
+ });
964
+
965
+ sink(debug);
966
+ sink(info);
967
+ assertEquals(buffer.length, 0);
968
+
969
+ // Fatal should also trigger (since it's >= error)
970
+ sink(fatal);
971
+ assertEquals(buffer, [debug, info, fatal]);
972
+ });
973
+
974
+ test("fingersCrossed() - category isolation descendant mode", () => {
975
+ const buffer: LogRecord[] = [];
976
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
977
+ isolateByCategory: "descendant",
978
+ });
979
+
980
+ // Create test records with different categories
981
+ const appDebug: LogRecord = {
982
+ ...debug,
983
+ category: ["app"],
984
+ };
985
+ const appModuleDebug: LogRecord = {
986
+ ...debug,
987
+ category: ["app", "module"],
988
+ };
989
+ const appModuleSubDebug: LogRecord = {
990
+ ...debug,
991
+ category: ["app", "module", "sub"],
992
+ };
993
+ const otherDebug: LogRecord = {
994
+ ...debug,
995
+ category: ["other"],
996
+ };
997
+ const appError: LogRecord = {
998
+ ...error,
999
+ category: ["app"],
1000
+ };
1001
+
1002
+ // Buffer logs in different categories
1003
+ sink(appDebug);
1004
+ sink(appModuleDebug);
1005
+ sink(appModuleSubDebug);
1006
+ sink(otherDebug);
1007
+ assertEquals(buffer.length, 0);
1008
+
1009
+ // Trigger in parent category
1010
+ sink(appError);
1011
+
1012
+ // Should flush parent and all descendants, but not other
1013
+ assertEquals(buffer.length, 4); // app, app.module, app.module.sub, and trigger
1014
+ assert(buffer.includes(appDebug));
1015
+ assert(buffer.includes(appModuleDebug));
1016
+ assert(buffer.includes(appModuleSubDebug));
1017
+ assert(buffer.includes(appError));
1018
+ assert(!buffer.includes(otherDebug));
1019
+ });
1020
+
1021
+ test("fingersCrossed() - category isolation ancestor mode", () => {
1022
+ const buffer: LogRecord[] = [];
1023
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1024
+ isolateByCategory: "ancestor",
1025
+ });
1026
+
1027
+ // Create test records
1028
+ const appDebug: LogRecord = {
1029
+ ...debug,
1030
+ category: ["app"],
1031
+ };
1032
+ const appModuleDebug: LogRecord = {
1033
+ ...debug,
1034
+ category: ["app", "module"],
1035
+ };
1036
+ const appModuleSubDebug: LogRecord = {
1037
+ ...debug,
1038
+ category: ["app", "module", "sub"],
1039
+ };
1040
+ const appModuleSubError: LogRecord = {
1041
+ ...error,
1042
+ category: ["app", "module", "sub"],
1043
+ };
1044
+
1045
+ // Buffer logs
1046
+ sink(appDebug);
1047
+ sink(appModuleDebug);
1048
+ sink(appModuleSubDebug);
1049
+ assertEquals(buffer.length, 0);
1050
+
1051
+ // Trigger in child category
1052
+ sink(appModuleSubError);
1053
+
1054
+ // Should flush child and all ancestors
1055
+ assertEquals(buffer.length, 4);
1056
+ assert(buffer.includes(appDebug));
1057
+ assert(buffer.includes(appModuleDebug));
1058
+ assert(buffer.includes(appModuleSubDebug));
1059
+ assert(buffer.includes(appModuleSubError));
1060
+ });
1061
+
1062
+ test("fingersCrossed() - category isolation both mode", () => {
1063
+ const buffer: LogRecord[] = [];
1064
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1065
+ isolateByCategory: "both",
1066
+ });
1067
+
1068
+ // Create test records
1069
+ const rootDebug: LogRecord = {
1070
+ ...debug,
1071
+ category: ["app"],
1072
+ };
1073
+ const parentDebug: LogRecord = {
1074
+ ...debug,
1075
+ category: ["app", "parent"],
1076
+ };
1077
+ const siblingDebug: LogRecord = {
1078
+ ...debug,
1079
+ category: ["app", "sibling"],
1080
+ };
1081
+ const childDebug: LogRecord = {
1082
+ ...debug,
1083
+ category: ["app", "parent", "child"],
1084
+ };
1085
+ const unrelatedDebug: LogRecord = {
1086
+ ...debug,
1087
+ category: ["other"],
1088
+ };
1089
+ const parentError: LogRecord = {
1090
+ ...error,
1091
+ category: ["app", "parent"],
1092
+ };
1093
+
1094
+ // Buffer logs
1095
+ sink(rootDebug);
1096
+ sink(parentDebug);
1097
+ sink(siblingDebug);
1098
+ sink(childDebug);
1099
+ sink(unrelatedDebug);
1100
+ assertEquals(buffer.length, 0);
1101
+
1102
+ // Trigger in middle category
1103
+ sink(parentError);
1104
+
1105
+ // Should flush ancestors and descendants, but not siblings or unrelated
1106
+ assertEquals(buffer.length, 4);
1107
+ assert(buffer.includes(rootDebug)); // Ancestor
1108
+ assert(buffer.includes(parentDebug)); // Same
1109
+ assert(buffer.includes(childDebug)); // Descendant
1110
+ assert(buffer.includes(parentError)); // Trigger
1111
+ assert(!buffer.includes(siblingDebug)); // Sibling
1112
+ assert(!buffer.includes(unrelatedDebug)); // Unrelated
1113
+ });
1114
+
1115
+ test("fingersCrossed() - custom category matcher", () => {
1116
+ const buffer: LogRecord[] = [];
1117
+
1118
+ // Custom matcher: only flush if categories share first element
1119
+ const customMatcher = (
1120
+ trigger: readonly string[],
1121
+ buffered: readonly string[],
1122
+ ): boolean => {
1123
+ return trigger[0] === buffered[0];
1124
+ };
1125
+
1126
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1127
+ isolateByCategory: customMatcher,
1128
+ });
1129
+
1130
+ // Create test records
1131
+ const app1Debug: LogRecord = {
1132
+ ...debug,
1133
+ category: ["app", "module1"],
1134
+ };
1135
+ const app2Debug: LogRecord = {
1136
+ ...debug,
1137
+ category: ["app", "module2"],
1138
+ };
1139
+ const otherDebug: LogRecord = {
1140
+ ...debug,
1141
+ category: ["other", "module"],
1142
+ };
1143
+ const appError: LogRecord = {
1144
+ ...error,
1145
+ category: ["app", "module3"],
1146
+ };
1147
+
1148
+ // Buffer logs
1149
+ sink(app1Debug);
1150
+ sink(app2Debug);
1151
+ sink(otherDebug);
1152
+ assertEquals(buffer.length, 0);
1153
+
1154
+ // Trigger
1155
+ sink(appError);
1156
+
1157
+ // Should flush all with same first category element
1158
+ assertEquals(buffer.length, 3);
1159
+ assert(buffer.includes(app1Debug));
1160
+ assert(buffer.includes(app2Debug));
1161
+ assert(buffer.includes(appError));
1162
+ assert(!buffer.includes(otherDebug));
1163
+ });
1164
+
1165
+ test("fingersCrossed() - isolated buffers maintain separate states", () => {
1166
+ const buffer: LogRecord[] = [];
1167
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1168
+ isolateByCategory: "descendant",
1169
+ });
1170
+
1171
+ // Create test records
1172
+ const app1Debug: LogRecord = {
1173
+ ...debug,
1174
+ category: ["app1"],
1175
+ };
1176
+ const app1Error: LogRecord = {
1177
+ ...error,
1178
+ category: ["app1"],
1179
+ };
1180
+ const app2Debug: LogRecord = {
1181
+ ...debug,
1182
+ category: ["app2"],
1183
+ };
1184
+ const app2Info: LogRecord = {
1185
+ ...info,
1186
+ category: ["app2"],
1187
+ };
1188
+
1189
+ // Buffer in app1
1190
+ sink(app1Debug);
1191
+
1192
+ // Trigger app1
1193
+ sink(app1Error);
1194
+ assertEquals(buffer, [app1Debug, app1Error]);
1195
+
1196
+ // Buffer in app2 (should still be buffering)
1197
+ sink(app2Debug);
1198
+ assertEquals(buffer, [app1Debug, app1Error]); // app2 still buffered
1199
+
1200
+ // Add more to triggered app1 (should pass through)
1201
+ sink(app1Debug);
1202
+ assertEquals(buffer, [app1Debug, app1Error, app1Debug]);
1203
+
1204
+ // app2 still buffering
1205
+ sink(app2Info);
1206
+ assertEquals(buffer, [app1Debug, app1Error, app1Debug]); // app2 still buffered
1207
+ });
1208
+
1209
+ test("fingersCrossed() - chronological order in category isolation", () => {
1210
+ const buffer: LogRecord[] = [];
1211
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1212
+ isolateByCategory: "both",
1213
+ });
1214
+
1215
+ // Create test records with different timestamps
1216
+ const app1: LogRecord = {
1217
+ ...debug,
1218
+ category: ["app"],
1219
+ timestamp: 1000,
1220
+ };
1221
+ const app2: LogRecord = {
1222
+ ...debug,
1223
+ category: ["app", "sub"],
1224
+ timestamp: 2000,
1225
+ };
1226
+ const app3: LogRecord = {
1227
+ ...info,
1228
+ category: ["app"],
1229
+ timestamp: 3000,
1230
+ };
1231
+ const app4: LogRecord = {
1232
+ ...debug,
1233
+ category: ["app", "sub"],
1234
+ timestamp: 4000,
1235
+ };
1236
+ const appError: LogRecord = {
1237
+ ...error,
1238
+ category: ["app"],
1239
+ timestamp: 5000,
1240
+ };
1241
+
1242
+ // Add out of order
1243
+ sink(app3);
1244
+ sink(app1);
1245
+ sink(app4);
1246
+ sink(app2);
1247
+
1248
+ // Trigger
1249
+ sink(appError);
1250
+
1251
+ // Should be sorted by timestamp
1252
+ assertEquals(buffer, [app1, app2, app3, app4, appError]);
1253
+ });
1254
+
1255
+ test("fingersCrossed() - empty buffer trigger", () => {
1256
+ const buffer: LogRecord[] = [];
1257
+ const sink = fingersCrossed(buffer.push.bind(buffer));
1258
+
1259
+ // Trigger immediately without any buffered logs
1260
+ sink(error);
1261
+ assertEquals(buffer, [error]);
1262
+
1263
+ // Continue to pass through
1264
+ sink(debug);
1265
+ assertEquals(buffer, [error, debug]);
1266
+ });
1267
+
1268
+ test("fingersCrossed() - buffer size per category in isolation mode", () => {
1269
+ const buffer: LogRecord[] = [];
1270
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1271
+ maxBufferSize: 2,
1272
+ isolateByCategory: "descendant",
1273
+ });
1274
+
1275
+ // Create records for different categories
1276
+ const app1Trace: LogRecord = { ...trace, category: ["app1"] };
1277
+ const app1Debug: LogRecord = { ...debug, category: ["app1"] };
1278
+ const app1Info: LogRecord = { ...info, category: ["app1"] };
1279
+ const app2Trace: LogRecord = { ...trace, category: ["app2"] };
1280
+ const app2Debug: LogRecord = { ...debug, category: ["app2"] };
1281
+ const app1Error: LogRecord = { ...error, category: ["app1"] };
1282
+
1283
+ // Fill app1 buffer beyond max
1284
+ sink(app1Trace);
1285
+ sink(app1Debug);
1286
+ sink(app1Info); // Should drop app1Trace
1287
+
1288
+ // Fill app2 buffer
1289
+ sink(app2Trace);
1290
+ sink(app2Debug);
1291
+
1292
+ // Trigger app1
1293
+ sink(app1Error);
1294
+
1295
+ // Should only have last 2 from app1 + error
1296
+ assertEquals(buffer.length, 3);
1297
+ assert(!buffer.some((r) => r === app1Trace)); // Dropped
1298
+ assert(buffer.includes(app1Debug));
1299
+ assert(buffer.includes(app1Info));
1300
+ assert(buffer.includes(app1Error));
1301
+ // app2 records should not be flushed
1302
+ assert(!buffer.includes(app2Trace));
1303
+ assert(!buffer.includes(app2Debug));
1304
+ });
1305
+
1306
+ test("fingersCrossed() - edge case: trigger level not in severity order", () => {
1307
+ const buffer: LogRecord[] = [];
1308
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1309
+ triggerLevel: "trace", // Lowest level triggers immediately
1310
+ });
1311
+
1312
+ // Everything should pass through immediately
1313
+ sink(trace);
1314
+ assertEquals(buffer, [trace]);
1315
+
1316
+ sink(debug);
1317
+ assertEquals(buffer, [trace, debug]);
1318
+ });
1319
+
1320
+ test("fingersCrossed() - edge case: invalid trigger level", () => {
1321
+ const buffer: LogRecord[] = [];
1322
+
1323
+ // Should throw TypeError during sink creation
1324
+ assertThrows(
1325
+ () => {
1326
+ fingersCrossed(buffer.push.bind(buffer), {
1327
+ triggerLevel: "invalid" as LogLevel,
1328
+ });
1329
+ },
1330
+ TypeError,
1331
+ "Invalid triggerLevel",
1332
+ );
1333
+ });
1334
+
1335
+ test("fingersCrossed() - edge case: very large buffer size", () => {
1336
+ const buffer: LogRecord[] = [];
1337
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1338
+ maxBufferSize: Number.MAX_SAFE_INTEGER,
1339
+ });
1340
+
1341
+ // Add many records
1342
+ for (let i = 0; i < 1000; i++) {
1343
+ sink(debug);
1344
+ }
1345
+ assertEquals(buffer.length, 0); // Still buffered
1346
+
1347
+ sink(error);
1348
+ assertEquals(buffer.length, 1001); // All 1000 + error
1349
+ });
1350
+
1351
+ test("fingersCrossed() - edge case: zero buffer size", () => {
1352
+ const buffer: LogRecord[] = [];
1353
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1354
+ maxBufferSize: 0,
1355
+ });
1356
+
1357
+ // Nothing should be buffered
1358
+ sink(debug);
1359
+ sink(info);
1360
+ assertEquals(buffer.length, 0);
1361
+
1362
+ // Trigger should still work
1363
+ sink(error);
1364
+ assertEquals(buffer, [error]); // Only the trigger
1365
+ });
1366
+
1367
+ test("fingersCrossed() - edge case: negative buffer size", () => {
1368
+ const buffer: LogRecord[] = [];
1369
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1370
+ maxBufferSize: -1,
1371
+ });
1372
+
1373
+ // Should behave like zero
1374
+ sink(debug);
1375
+ sink(info);
1376
+ assertEquals(buffer.length, 0);
1377
+
1378
+ sink(error);
1379
+ assertEquals(buffer, [error]);
1380
+ });
1381
+
1382
+ test("fingersCrossed() - edge case: same record logged multiple times", () => {
1383
+ const buffer: LogRecord[] = [];
1384
+ const sink = fingersCrossed(buffer.push.bind(buffer));
1385
+
1386
+ // Log same record multiple times
1387
+ sink(debug);
1388
+ sink(debug);
1389
+ sink(debug);
1390
+ assertEquals(buffer.length, 0);
1391
+
1392
+ sink(error);
1393
+ // All instances should be preserved
1394
+ assertEquals(buffer.length, 4);
1395
+ assertEquals(buffer, [debug, debug, debug, error]);
1396
+ });
1397
+
1398
+ test("fingersCrossed() - edge case: empty category array", () => {
1399
+ const buffer: LogRecord[] = [];
1400
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1401
+ isolateByCategory: "both",
1402
+ });
1403
+
1404
+ const emptyCategory: LogRecord = {
1405
+ ...debug,
1406
+ category: [],
1407
+ };
1408
+
1409
+ const normalCategory: LogRecord = {
1410
+ ...info,
1411
+ category: ["app"],
1412
+ };
1413
+
1414
+ const emptyError: LogRecord = {
1415
+ ...error,
1416
+ category: [],
1417
+ };
1418
+
1419
+ sink(emptyCategory);
1420
+ sink(normalCategory);
1421
+ assertEquals(buffer.length, 0);
1422
+
1423
+ // Trigger with empty category
1424
+ sink(emptyError);
1425
+
1426
+ // Only empty category should flush (no ancestors/descendants)
1427
+ assertEquals(buffer.length, 2);
1428
+ assert(buffer.includes(emptyCategory));
1429
+ assert(buffer.includes(emptyError));
1430
+ assert(!buffer.includes(normalCategory));
1431
+ });
1432
+
1433
+ test("fingersCrossed() - edge case: category with special characters", () => {
1434
+ const buffer: LogRecord[] = [];
1435
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1436
+ isolateByCategory: "descendant",
1437
+ });
1438
+
1439
+ // Category with null character (our separator)
1440
+ const specialCategory: LogRecord = {
1441
+ ...debug,
1442
+ category: ["app\0special", "sub"],
1443
+ };
1444
+
1445
+ const normalCategory: LogRecord = {
1446
+ ...info,
1447
+ category: ["app"],
1448
+ };
1449
+
1450
+ const specialError: LogRecord = {
1451
+ ...error,
1452
+ category: ["app\0special"],
1453
+ };
1454
+
1455
+ sink(specialCategory);
1456
+ sink(normalCategory);
1457
+ assertEquals(buffer.length, 0);
1458
+
1459
+ // Should still work correctly despite special characters
1460
+ sink(specialError);
1461
+
1462
+ assertEquals(buffer.length, 2);
1463
+ assert(buffer.includes(specialCategory));
1464
+ assert(buffer.includes(specialError));
1465
+ assert(!buffer.includes(normalCategory));
1466
+ });
1467
+
1468
+ test("fingersCrossed() - edge case: rapid alternating triggers", () => {
1469
+ const buffer: LogRecord[] = [];
1470
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1471
+ isolateByCategory: "both",
1472
+ });
1473
+
1474
+ const app1Debug: LogRecord = { ...debug, category: ["app1"] };
1475
+ const app2Debug: LogRecord = { ...debug, category: ["app2"] };
1476
+ const app1Error: LogRecord = { ...error, category: ["app1"] };
1477
+ const app2Error: LogRecord = { ...error, category: ["app2"] };
1478
+
1479
+ // Rapidly alternate between categories and triggers
1480
+ sink(app1Debug);
1481
+ sink(app2Debug);
1482
+ sink(app1Error); // Trigger app1
1483
+ assertEquals(buffer.length, 2); // app1Debug + app1Error
1484
+
1485
+ sink(app2Error); // Trigger app2
1486
+ assertEquals(buffer.length, 4); // Previous + app2Debug + app2Error
1487
+
1488
+ // After both triggered, everything passes through
1489
+ sink(app1Debug);
1490
+ sink(app2Debug);
1491
+ assertEquals(buffer.length, 6);
1492
+ });
1493
+
1494
+ test("fingersCrossed() - edge case: custom matcher throws error", () => {
1495
+ const buffer: LogRecord[] = [];
1496
+
1497
+ const errorMatcher = (): boolean => {
1498
+ throw new Error("Matcher error");
1499
+ };
1500
+
1501
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1502
+ isolateByCategory: errorMatcher,
1503
+ });
1504
+
1505
+ const app1Debug: LogRecord = { ...debug, category: ["app1"] };
1506
+ const app2Debug: LogRecord = { ...debug, category: ["app2"] };
1507
+ const app1Error: LogRecord = { ...error, category: ["app1"] };
1508
+
1509
+ sink(app1Debug);
1510
+ sink(app2Debug);
1511
+
1512
+ // Should handle error gracefully and still trigger
1513
+ try {
1514
+ sink(app1Error);
1515
+ } catch {
1516
+ // Should not throw to caller
1517
+ }
1518
+
1519
+ // At minimum, trigger record should be sent
1520
+ assert(buffer.includes(app1Error));
1521
+ });
1522
+
1523
+ test("fingersCrossed() - edge case: circular category references", () => {
1524
+ const buffer: LogRecord[] = [];
1525
+
1526
+ // Custom matcher that creates circular logic
1527
+ const circularMatcher = (
1528
+ _trigger: readonly string[],
1529
+ _buffered: readonly string[],
1530
+ ): boolean => {
1531
+ // Always return true, creating a circular flush
1532
+ return true;
1533
+ };
1534
+
1535
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1536
+ isolateByCategory: circularMatcher,
1537
+ });
1538
+
1539
+ const app1: LogRecord = { ...debug, category: ["app1"] };
1540
+ const app2: LogRecord = { ...debug, category: ["app2"] };
1541
+ const app3: LogRecord = { ...debug, category: ["app3"] };
1542
+ const trigger: LogRecord = { ...error, category: ["trigger"] };
1543
+
1544
+ sink(app1);
1545
+ sink(app2);
1546
+ sink(app3);
1547
+ assertEquals(buffer.length, 0);
1548
+
1549
+ // Should flush all despite circular logic
1550
+ sink(trigger);
1551
+ assertEquals(buffer.length, 4);
1552
+
1553
+ // All buffers should be cleared after flush
1554
+ const newDebug: LogRecord = { ...debug, category: ["new"] };
1555
+ sink(newDebug);
1556
+ assertEquals(buffer.length, 4); // New category should be buffered
1557
+ });
1558
+
1559
+ test("fingersCrossed() - edge case: timestamps in wrong order", () => {
1560
+ const buffer: LogRecord[] = [];
1561
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1562
+ isolateByCategory: "both",
1563
+ });
1564
+
1565
+ const future: LogRecord = {
1566
+ ...debug,
1567
+ category: ["app"],
1568
+ timestamp: Date.now() + 10000, // Future
1569
+ };
1570
+
1571
+ const past: LogRecord = {
1572
+ ...info,
1573
+ category: ["app", "sub"],
1574
+ timestamp: Date.now() - 10000, // Past
1575
+ };
1576
+
1577
+ const present: LogRecord = {
1578
+ ...warning,
1579
+ category: ["app"],
1580
+ timestamp: Date.now(),
1581
+ };
1582
+
1583
+ const trigger: LogRecord = {
1584
+ ...error,
1585
+ category: ["app"],
1586
+ timestamp: Date.now() + 5000,
1587
+ };
1588
+
1589
+ // Add in random order
1590
+ sink(future);
1591
+ sink(past);
1592
+ sink(present);
1593
+
1594
+ // Trigger
1595
+ sink(trigger);
1596
+
1597
+ // Should be sorted by timestamp
1598
+ assertEquals(buffer[0], past);
1599
+ assertEquals(buffer[1], present);
1600
+ assertEquals(buffer[2], future);
1601
+ assertEquals(buffer[3], trigger);
1602
+ });
1603
+
1604
+ test("fingersCrossed() - edge case: NaN and Infinity in timestamps", () => {
1605
+ const buffer: LogRecord[] = [];
1606
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1607
+ isolateByCategory: "both",
1608
+ });
1609
+
1610
+ const nanTime: LogRecord = {
1611
+ ...debug,
1612
+ category: ["app"],
1613
+ timestamp: NaN,
1614
+ };
1615
+
1616
+ const infinityTime: LogRecord = {
1617
+ ...info,
1618
+ category: ["app"],
1619
+ timestamp: Infinity,
1620
+ };
1621
+
1622
+ const negInfinityTime: LogRecord = {
1623
+ ...warning,
1624
+ category: ["app"],
1625
+ timestamp: -Infinity,
1626
+ };
1627
+
1628
+ const normalTime: LogRecord = {
1629
+ ...error,
1630
+ category: ["app"],
1631
+ timestamp: 1000,
1632
+ };
1633
+
1634
+ sink(nanTime);
1635
+ sink(infinityTime);
1636
+ sink(negInfinityTime);
1637
+
1638
+ // Should handle special values without crashing
1639
+ sink(normalTime);
1640
+
1641
+ // Check all records are present (order might vary with NaN)
1642
+ assertEquals(buffer.length, 4);
1643
+ assert(buffer.includes(nanTime));
1644
+ assert(buffer.includes(infinityTime));
1645
+ assert(buffer.includes(negInfinityTime));
1646
+ assert(buffer.includes(normalTime));
1647
+ });
1648
+
1649
+ test("fingersCrossed() - edge case: undefined properties in record", () => {
1650
+ const buffer: LogRecord[] = [];
1651
+ const sink = fingersCrossed(buffer.push.bind(buffer));
1652
+
1653
+ const weirdRecord: LogRecord = {
1654
+ ...debug,
1655
+ properties: {
1656
+ normal: "value",
1657
+ undef: undefined,
1658
+ nullish: null,
1659
+ nan: NaN,
1660
+ inf: Infinity,
1661
+ },
1662
+ };
1663
+
1664
+ sink(weirdRecord);
1665
+ sink(error);
1666
+
1667
+ // Should preserve all properties as-is
1668
+ assertEquals(buffer[0].properties, weirdRecord.properties);
1669
+ });
1670
+
1671
+ test("fingersCrossed() - edge case: very deep category hierarchy", () => {
1672
+ const buffer: LogRecord[] = [];
1673
+ const sink = fingersCrossed(buffer.push.bind(buffer), {
1674
+ isolateByCategory: "both",
1675
+ });
1676
+
1677
+ // Create very deep hierarchy
1678
+ const deepCategory = Array.from({ length: 100 }, (_, i) => `level${i}`);
1679
+ const parentCategory = deepCategory.slice(0, 50);
1680
+
1681
+ const deepRecord: LogRecord = {
1682
+ ...debug,
1683
+ category: deepCategory,
1684
+ };
1685
+
1686
+ const parentRecord: LogRecord = {
1687
+ ...info,
1688
+ category: parentCategory,
1689
+ };
1690
+
1691
+ const deepError: LogRecord = {
1692
+ ...error,
1693
+ category: deepCategory,
1694
+ };
1695
+
1696
+ sink(deepRecord);
1697
+ sink(parentRecord);
1698
+ assertEquals(buffer.length, 0);
1699
+
1700
+ // Should handle deep hierarchies
1701
+ sink(deepError);
1702
+
1703
+ // Both should flush (ancestor relationship)
1704
+ assertEquals(buffer.length, 3);
1705
+ assert(buffer.includes(deepRecord));
1706
+ assert(buffer.includes(parentRecord));
1707
+ assert(buffer.includes(deepError));
1708
+ });