@remit/imap-worker 0.0.70 → 0.0.72

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.
@@ -3,6 +3,7 @@ import { beforeEach, describe, it, mock } from "node:test";
3
3
  import type { ThreadMessageItem } from "@remit/data-ports";
4
4
  import type { Logger } from "@remit/logger-lambda";
5
5
  import { renderMetrics, resetMetrics } from "@remit/logger-lambda";
6
+ import { noopLogger } from "@remit/logger-lambda/noop-logger";
6
7
  import type { MessageDeleteEvent } from "../events.js";
7
8
  import {
8
9
  deleteAllThreadMessagesForMessage,
@@ -256,16 +257,6 @@ describe("deleteAllThreadMessagesForMessage (#212)", () => {
256
257
  });
257
258
  });
258
259
 
259
- const noopLog = {
260
- info: () => {},
261
- warn: () => {},
262
- error: () => {},
263
- debug: () => {},
264
- fatal: () => {},
265
- trace: () => {},
266
- child: () => noopLog,
267
- } as unknown as Logger;
268
-
269
260
  interface Call {
270
261
  method: string;
271
262
  args: unknown[];
@@ -543,7 +534,7 @@ describe("handleMessageDelete", () => {
543
534
  });
544
535
 
545
536
  it("moves to trash, rewrites the uid, and flips the thread row to deleted", async () => {
546
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
537
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
547
538
 
548
539
  assert.deepEqual(called("message.updateUid")[0]?.args, [
549
540
  "msg-1",
@@ -569,7 +560,7 @@ describe("handleMessageDelete", () => {
569
560
  sourceNoLongerHoldsTheUid();
570
561
  h.destinationSearchUids = [77];
571
562
 
572
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
563
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
573
564
 
574
565
  assert.deepEqual(called("message.updateUid")[0]?.args, [
575
566
  "msg-1",
@@ -598,7 +589,7 @@ describe("handleMessageDelete", () => {
598
589
  sourceNoLongerHoldsTheUid();
599
590
  h.destinationSearchUids = [12, 77];
600
591
 
601
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
592
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
602
593
 
603
594
  assert.deepEqual(called("message.updateUid")[0]?.args, [
604
595
  "msg-1",
@@ -623,7 +614,7 @@ describe("handleMessageDelete", () => {
623
614
  return { uidvalidity: 1 };
624
615
  }) as Connection["openBox"];
625
616
 
626
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
617
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
627
618
 
628
619
  assert.deepEqual(
629
620
  opened,
@@ -650,7 +641,7 @@ describe("handleMessageDelete", () => {
650
641
  h.destinationSearchUids = [];
651
642
 
652
643
  await assert.rejects(
653
- handleMessageDelete(moveEvent, noopLog, 1, deps()),
644
+ handleMessageDelete(moveEvent, noopLogger, 1, deps()),
654
645
  /unconfirmed/,
655
646
  );
656
647
 
@@ -683,7 +674,7 @@ describe("handleMessageDelete", () => {
683
674
  sourceNoLongerHoldsTheUid();
684
675
  h.destinationSearchUids = [];
685
676
 
686
- await handleMessageDelete(moveEvent, noopLog, 3, deps());
677
+ await handleMessageDelete(moveEvent, noopLogger, 3, deps());
687
678
 
688
679
  assert.equal(
689
680
  called("threadMessage.deleteMany").length,
@@ -710,7 +701,7 @@ describe("handleMessageDelete", () => {
710
701
  sourceNoLongerHoldsTheUid();
711
702
  h.messageRow = {};
712
703
 
713
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
704
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
714
705
 
715
706
  assert.equal(
716
707
  called("connection.search").filter((c) =>
@@ -739,7 +730,7 @@ describe("handleMessageDelete", () => {
739
730
  sourceNoLongerHoldsTheUid();
740
731
  h.messageRow = undefined;
741
732
 
742
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
733
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
743
734
 
744
735
  assert.equal(called("message.update").length, 0);
745
736
  assert.equal(called("message.delete").length, 0);
@@ -757,7 +748,7 @@ describe("handleMessageDelete", () => {
757
748
  h.destinationSearchUids = [100];
758
749
 
759
750
  await assert.rejects(
760
- handleMessageDelete(moveEvent, noopLog, 1, deps()),
751
+ handleMessageDelete(moveEvent, noopLogger, 1, deps()),
761
752
  /unconfirmed/,
762
753
  );
763
754
 
@@ -797,7 +788,7 @@ describe("handleMessageDelete", () => {
797
788
  },
798
789
  ];
799
790
 
800
- await handleMessageDelete(moveEvent, noopLog, 3, deps());
791
+ await handleMessageDelete(moveEvent, noopLogger, 3, deps());
801
792
 
802
793
  assert.equal(called("message.delete").length, 0);
803
794
  assert.equal(called("threadMessage.deleteMany").length, 0);
@@ -845,7 +836,7 @@ describe("handleMessageDelete", () => {
845
836
  h.connection.moveMessages = async () => ({ uidMap: new Map() });
846
837
  h.destinationSearchUids = [100];
847
838
 
848
- await handleMessageDelete(moveEvent, noopLog, 3, deps());
839
+ await handleMessageDelete(moveEvent, noopLogger, 3, deps());
849
840
 
850
841
  assert.deepEqual(
851
842
  called("emitEvent").map((c) => c.args[0]),
@@ -874,7 +865,7 @@ describe("handleMessageDelete", () => {
874
865
  moveThrows();
875
866
 
876
867
  await assert.rejects(
877
- handleMessageDelete(moveEvent, noopLog, 1, deps()),
868
+ handleMessageDelete(moveEvent, noopLogger, 1, deps()),
878
869
  /UID MOVE failed/,
879
870
  );
880
871
 
@@ -891,7 +882,7 @@ describe("handleMessageDelete", () => {
891
882
  moveThrows();
892
883
  sourceNoLongerHoldsTheUid();
893
884
 
894
- await handleMessageDelete(moveEvent, noopLog, 3, deps());
885
+ await handleMessageDelete(moveEvent, noopLogger, 3, deps());
895
886
 
896
887
  assert.equal(
897
888
  called("threadMessage.deleteMany").length,
@@ -905,7 +896,7 @@ describe("handleMessageDelete", () => {
905
896
  it("settles a still-present message at the ceiling back onto its source", async () => {
906
897
  moveThrows();
907
898
 
908
- await handleMessageDelete(moveEvent, noopLog, 3, deps());
899
+ await handleMessageDelete(moveEvent, noopLogger, 3, deps());
909
900
 
910
901
  assert.deepEqual(
911
902
  called("message.updateForMove").at(-1)?.args,
@@ -927,7 +918,7 @@ describe("handleMessageDelete", () => {
927
918
  }) as Connection["openBox"];
928
919
 
929
920
  await assert.rejects(
930
- handleMessageDelete(moveEvent, noopLog, 1, deps()),
921
+ handleMessageDelete(moveEvent, noopLogger, 1, deps()),
931
922
  /unconfirmed/,
932
923
  );
933
924
 
@@ -958,7 +949,7 @@ describe("handleMessageDelete", () => {
958
949
  operation,
959
950
  } as unknown as MessageDeleteEvent;
960
951
 
961
- await handleMessageDelete(malformed, noopLog, 1, deps());
952
+ await handleMessageDelete(malformed, noopLogger, 1, deps());
962
953
 
963
954
  assert.equal(
964
955
  called("connection.deleteMessages").length,
@@ -991,7 +982,7 @@ describe("handleMessageDelete", () => {
991
982
  destinationMailboxPath: undefined,
992
983
  } as MessageDeleteEvent;
993
984
 
994
- await handleMessageDelete(destinationless, noopLog, 1, deps());
985
+ await handleMessageDelete(destinationless, noopLogger, 1, deps());
995
986
 
996
987
  assert.equal(called("connection.deleteMessages").length, 0);
997
988
  assert.equal(called("message.delete").length, 0);
@@ -1002,7 +993,7 @@ describe("handleMessageDelete", () => {
1002
993
  });
1003
994
 
1004
995
  it("expunges on the server and removes every thread row before the message row", async () => {
1005
- await handleMessageDelete(permanentEvent, noopLog, 1, deps());
996
+ await handleMessageDelete(permanentEvent, noopLogger, 1, deps());
1006
997
 
1007
998
  assert.deepEqual(called("connection.deleteMessages")[0]?.args, [[10]]);
1008
999
  assert.equal(called("threadMessage.delete").length, 2);
@@ -1021,7 +1012,7 @@ describe("handleMessageDelete", () => {
1021
1012
  };
1022
1013
  sourceNoLongerHoldsTheUid();
1023
1014
 
1024
- await handleMessageDelete(permanentEvent, noopLog, 1, deps());
1015
+ await handleMessageDelete(permanentEvent, noopLogger, 1, deps());
1025
1016
 
1026
1017
  assert.equal(called("message.delete").length, 1);
1027
1018
  assert.equal(called("threadMessage.delete").length, 2);
@@ -1042,7 +1033,7 @@ describe("handleMessageDelete", () => {
1042
1033
  sourceNoLongerHoldsTheUid();
1043
1034
 
1044
1035
  await assert.rejects(
1045
- handleMessageDelete(moveEvent, noopLog, 1, deps()),
1036
+ handleMessageDelete(moveEvent, noopLogger, 1, deps()),
1046
1037
  /not found/,
1047
1038
  );
1048
1039
 
@@ -1070,7 +1061,7 @@ describe("handleMessageDelete", () => {
1070
1061
  };
1071
1062
 
1072
1063
  await assert.rejects(
1073
- handleMessageDelete(moveEvent, noopLog, 1, deps()),
1064
+ handleMessageDelete(moveEvent, noopLogger, 1, deps()),
1074
1065
  /NONEXISTENT/,
1075
1066
  );
1076
1067
 
@@ -1092,7 +1083,7 @@ describe("handleMessageDelete", () => {
1092
1083
  h.connection.fetchMessages = async () => [];
1093
1084
 
1094
1085
  await assert.rejects(
1095
- handleMessageDelete(permanentEvent, noopLog, 1, deps()),
1086
+ handleMessageDelete(permanentEvent, noopLogger, 1, deps()),
1096
1087
  /NONEXISTENT/,
1097
1088
  );
1098
1089
 
@@ -1117,7 +1108,7 @@ describe("handleMessageDelete", () => {
1117
1108
  };
1118
1109
 
1119
1110
  await assert.rejects(
1120
- handleMessageDelete(permanentEvent, noopLog, 1, deps()),
1111
+ handleMessageDelete(permanentEvent, noopLogger, 1, deps()),
1121
1112
  /NONEXISTENT mailbox does not exist/,
1122
1113
  );
1123
1114
 
@@ -1138,7 +1129,7 @@ describe("handleMessageDelete", () => {
1138
1129
  throw new Error("TRYCREATE: no such mailbox");
1139
1130
  };
1140
1131
 
1141
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
1132
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
1142
1133
 
1143
1134
  assert.equal(called("connection.createMailbox").length, 0);
1144
1135
  assert.deepEqual(
@@ -1158,7 +1149,7 @@ describe("handleMessageDelete", () => {
1158
1149
  schemaVersion: undefined,
1159
1150
  } as unknown as MessageDeleteEvent;
1160
1151
 
1161
- await handleMessageDelete(unversioned, noopLog, 1, deps());
1152
+ await handleMessageDelete(unversioned, noopLogger, 1, deps());
1162
1153
 
1163
1154
  assert.equal(h.getConnectionCount, 0);
1164
1155
  assert.equal(called("connection.deleteMessages").length, 0);
@@ -1179,7 +1170,7 @@ describe("handleMessageDelete", () => {
1179
1170
  schemaVersion: undefined,
1180
1171
  } as unknown as MessageDeleteEvent;
1181
1172
 
1182
- await handleMessageDelete(unversioned, noopLog, 1, deps());
1173
+ await handleMessageDelete(unversioned, noopLogger, 1, deps());
1183
1174
 
1184
1175
  assert.deepEqual(
1185
1176
  called("threadMessage.update").map((c) => c.args[1]),
@@ -1198,7 +1189,7 @@ describe("handleMessageDelete", () => {
1198
1189
  schemaVersion: undefined,
1199
1190
  } as unknown as MessageDeleteEvent;
1200
1191
 
1201
- await handleMessageDelete(unversioned, noopLog, 1, deps());
1192
+ await handleMessageDelete(unversioned, noopLogger, 1, deps());
1202
1193
 
1203
1194
  assert.equal(called("connection.deleteMessages").length, 0);
1204
1195
  assert.deepEqual(called("message.delete")[0]?.args, ["msg-1"]);
@@ -1215,7 +1206,7 @@ describe("handleMessageDelete", () => {
1215
1206
  };
1216
1207
 
1217
1208
  await assert.rejects(
1218
- handleMessageDelete(moveEvent, noopLog, 1, deps()),
1209
+ handleMessageDelete(moveEvent, noopLogger, 1, deps()),
1219
1210
  /server exploded/,
1220
1211
  );
1221
1212
 
@@ -1234,7 +1225,7 @@ describe("handleMessageDelete", () => {
1234
1225
  it("hands the optimistic trash move back when openBox trips a UIDVALIDITY mismatch", async () => {
1235
1226
  h.connection.openBox = async () => ({ uidvalidity: 999 });
1236
1227
 
1237
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
1228
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
1238
1229
 
1239
1230
  assert.equal(
1240
1231
  (called("mailbox.update")[0]?.args[2] as { cursorState?: string })
@@ -1269,7 +1260,7 @@ describe("handleMessageDelete", () => {
1269
1260
 
1270
1261
  await handleMessageDelete(
1271
1262
  moveEvent,
1272
- noopLog,
1263
+ noopLogger,
1273
1264
  MESSAGE_DELETE_MAX_ATTEMPTS,
1274
1265
  deps(),
1275
1266
  );
@@ -1403,7 +1394,7 @@ describe("handleMessageDelete", () => {
1403
1394
  cursorState: "rebuilding",
1404
1395
  };
1405
1396
 
1406
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
1397
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
1407
1398
 
1408
1399
  assert.equal(called("threadMessage.findAllByMessageId").length, 1);
1409
1400
  });
@@ -1487,7 +1478,7 @@ describe("handleMessageDelete", () => {
1487
1478
  h.sourceSearchUids = [4];
1488
1479
  h.destinationSearchUids = [12];
1489
1480
 
1490
- await handleMessageDelete(moveEvent, noopLog, 2, deps());
1481
+ await handleMessageDelete(moveEvent, noopLogger, 2, deps());
1491
1482
 
1492
1483
  assert.equal(called("message.updateUid").length, 0);
1493
1484
  assert.deepEqual(
@@ -1507,7 +1498,7 @@ describe("handleMessageDelete", () => {
1507
1498
  cursorState: "rebuilding",
1508
1499
  };
1509
1500
 
1510
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
1501
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
1511
1502
 
1512
1503
  assert.deepEqual(
1513
1504
  called("emitEvent").map((c) => c.args[0]),
@@ -1525,7 +1516,7 @@ describe("handleMessageDelete", () => {
1525
1516
  cursorState: "rebuilding",
1526
1517
  };
1527
1518
 
1528
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
1519
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
1529
1520
 
1530
1521
  assert.equal(h.getConnectionCount, 0);
1531
1522
  assert.deepEqual(
@@ -1549,7 +1540,7 @@ describe("handleMessageDelete", () => {
1549
1540
  };
1550
1541
  h.allThreadMessages = [];
1551
1542
 
1552
- await handleMessageDelete(permanentEvent, noopLog, 1, deps());
1543
+ await handleMessageDelete(permanentEvent, noopLogger, 1, deps());
1553
1544
 
1554
1545
  assert.equal(h.getConnectionCount, 0);
1555
1546
  assert.deepEqual(called("message.delete")[0]?.args, ["msg-1"]);
@@ -1568,7 +1559,7 @@ describe("handleMessageDelete", () => {
1568
1559
  };
1569
1560
  h.allThreadMessages = [];
1570
1561
 
1571
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
1562
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
1572
1563
 
1573
1564
  assert.deepEqual(called("message.delete")[0]?.args, ["msg-1"]);
1574
1565
  assert.equal(
@@ -1583,7 +1574,7 @@ describe("handleMessageDelete", () => {
1583
1574
  name: "NotFoundError",
1584
1575
  });
1585
1576
 
1586
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
1577
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
1587
1578
 
1588
1579
  assert.equal(h.getConnectionCount, 0);
1589
1580
  assert.equal(called("message.updateUid").length, 0);
@@ -1597,7 +1588,7 @@ describe("handleMessageDelete", () => {
1597
1588
  deletedAt: Date.now(),
1598
1589
  };
1599
1590
 
1600
- await handleMessageDelete(moveEvent, noopLog, 1, deps());
1591
+ await handleMessageDelete(moveEvent, noopLogger, 1, deps());
1601
1592
 
1602
1593
  assert.equal(h.getConnectionCount, 0);
1603
1594
  });
@@ -1606,7 +1597,7 @@ describe("handleMessageDelete", () => {
1606
1597
  h.account = null;
1607
1598
 
1608
1599
  await assert.rejects(
1609
- handleMessageDelete(moveEvent, noopLog, 1, deps()),
1600
+ handleMessageDelete(moveEvent, noopLogger, 1, deps()),
1610
1601
  /not found/,
1611
1602
  );
1612
1603
  });
@@ -11,6 +11,7 @@ import {
11
11
  QuarantineService,
12
12
  resolveExhaustedBodySyncFailures,
13
13
  } from "@remit/mailbox-service";
14
+ import { buildFilterConfig } from "@remit/mailbox-service/filter-config";
14
15
  import { attemptBudget } from "@remit/sqs-client/attempt-budget";
15
16
  import { env } from "expect-env";
16
17
  import { isAccountDeleted } from "../account-check.js";
@@ -20,7 +21,6 @@ import {
20
21
  createConnectionScopeWithCredentials,
21
22
  } from "../connection-scope.js";
22
23
  import type { SyncMessageBodyEvent } from "../events.js";
23
- import { buildFilterConfig } from "../filter-config.js";
24
24
  import { isNotFoundError } from "../is-not-found.js";
25
25
  import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
26
26
  import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
@@ -1,171 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- FilterItem,
5
- IFilterAnchorRepository,
6
- IFilterRepository,
7
- IMessageLabelRepository,
8
- } from "@remit/data-ports";
9
- import { FilterMatchOperator, FilterState } from "@remit/domain-enums";
10
- import {
11
- type FilterMessage,
12
- FilterPipeline,
13
- type MessageEmbedder,
14
- NO_ACTION,
15
- type PlacementMoveService,
16
- } from "@remit/mailbox-service";
17
- import { buildFilterConfig, type FilterConfigDeps } from "./filter-config.js";
18
- import { _resetMessageEmbedderForTest } from "./message-embedder.js";
19
-
20
- const anchorOnlyFilter = (destinationMailboxId: string): FilterItem =>
21
- ({
22
- filterId: "flt-semantic",
23
- accountConfigId: "cfg-1",
24
- name: "receipts",
25
- scope: "Standing",
26
- state: FilterState.Active,
27
- hasAnchor: true,
28
- ruleChangedAt: 1,
29
- matchOperator: FilterMatchOperator.Or,
30
- literalClauses: [],
31
- actionLabelId: NO_ACTION,
32
- actionMailboxId: destinationMailboxId,
33
- createdAt: 1,
34
- updatedAt: 1,
35
- }) as unknown as FilterItem;
36
-
37
- const CURRENT_MODEL = "test-model@3";
38
-
39
- const repos = (filter: FilterItem): FilterConfigDeps => ({
40
- filterService: {
41
- listByAccountAndState: async () => [filter],
42
- refreshExpiry: async (f: FilterItem) => f,
43
- } as unknown as IFilterRepository,
44
- filterAnchorService: {
45
- get: async () => ({
46
- anchorEmbedding: [1, 0, 0],
47
- anchorEmbeddingId: CURRENT_MODEL,
48
- }),
49
- } as unknown as IFilterAnchorRepository,
50
- messageLabelService: {} as unknown as IMessageLabelRepository,
51
- placementMoveService: {} as unknown as PlacementMoveService,
52
- });
53
-
54
- const message: FilterMessage = {
55
- from: "billing@stripe.com",
56
- fromName: "Stripe",
57
- subject: "Your receipt",
58
- text: "Thanks for your payment",
59
- listId: "",
60
- };
61
-
62
- describe("buildFilterConfig", () => {
63
- it("returns undefined without a placement mover — no move path, filters stay off", () => {
64
- const deps = repos(anchorOnlyFilter("mbx-archive"));
65
- deps.placementMoveService = undefined;
66
-
67
- assert.equal(buildFilterConfig(deps), undefined);
68
- });
69
-
70
- it("wires an embedder into the config", () => {
71
- const config = buildFilterConfig(repos(anchorOnlyFilter("mbx-archive")));
72
-
73
- assert.ok(config);
74
- assert.ok(config.embedder);
75
- });
76
-
77
- it("lights up a semantic anchor-only filter on the body-sync pass", async () => {
78
- let embedCalls = 0;
79
- const embedder: MessageEmbedder = {
80
- embed: async () => {
81
- embedCalls += 1;
82
- return [1, 0, 0];
83
- },
84
- embeddingId: CURRENT_MODEL,
85
- };
86
- const config = buildFilterConfig(
87
- repos(anchorOnlyFilter("mbx-archive")),
88
- embedder,
89
- );
90
- assert.ok(config);
91
-
92
- const decision = await new FilterPipeline(config, {
93
- info: () => {},
94
- }).evaluate("cfg-1", "m-1", message);
95
-
96
- assert.deepEqual(decision.move, {
97
- destinationMailboxId: "mbx-archive",
98
- filterId: "flt-semantic",
99
- });
100
- assert.equal(embedCalls, 1);
101
- });
102
-
103
- it("does not match when the message embedding diverges from the anchor", async () => {
104
- const embedder: MessageEmbedder = {
105
- embed: async () => [0, 1, 0],
106
- embeddingId: CURRENT_MODEL,
107
- };
108
- const config = buildFilterConfig(
109
- repos(anchorOnlyFilter("mbx-archive")),
110
- embedder,
111
- );
112
- assert.ok(config);
113
-
114
- const decision = await new FilterPipeline(config, {
115
- info: () => {},
116
- }).evaluate("cfg-1", "m-2", message);
117
-
118
- assert.equal(decision.move, undefined);
119
- });
120
- });
121
-
122
- // An instance with semantic search off (issue #1068). `off` selects an embedder
123
- // that throws by design, and the throw lands in the pipeline's per-filter catch:
124
- // one error-level `filter_anchor_match_failed` for every semantic filter on
125
- // every message it syncs. The pipeline already has a designed skip for a
126
- // missing embedder, so the config passes none.
127
- describe("buildFilterConfig with semantic search off", () => {
128
- const withProviderOff = async (body: () => Promise<void>): Promise<void> => {
129
- const previous = process.env.SEARCH_EMBEDDING_PROVIDER;
130
- process.env.SEARCH_EMBEDDING_PROVIDER = "off";
131
- _resetMessageEmbedderForTest();
132
- try {
133
- await body();
134
- } finally {
135
- if (previous === undefined) {
136
- delete process.env.SEARCH_EMBEDDING_PROVIDER;
137
- } else {
138
- process.env.SEARCH_EMBEDDING_PROVIDER = previous;
139
- }
140
- _resetMessageEmbedderForTest();
141
- }
142
- };
143
-
144
- it("wires no embedder", async () => {
145
- await withProviderOff(async () => {
146
- const config = buildFilterConfig(repos(anchorOnlyFilter("mbx-archive")));
147
-
148
- assert.ok(config);
149
- assert.equal(config.embedder, undefined);
150
- });
151
- });
152
-
153
- it("skips the semantic filter without logging an error", async () => {
154
- await withProviderOff(async () => {
155
- const config = buildFilterConfig(repos(anchorOnlyFilter("mbx-archive")));
156
- assert.ok(config);
157
-
158
- const errors: unknown[] = [];
159
- const decision = await new FilterPipeline(config, {
160
- info: () => {},
161
- debug: () => {},
162
- warn: () => {},
163
- error: (fields) => errors.push(fields.alert),
164
- }).evaluate("cfg-1", "m-off", message);
165
-
166
- assert.deepEqual(decision.labels, []);
167
- assert.equal(decision.move, undefined);
168
- assert.deepEqual(errors, []);
169
- });
170
- });
171
- });
@@ -1,63 +0,0 @@
1
- import type {
2
- IFilterAnchorRepository,
3
- IFilterRepository,
4
- IMessageLabelRepository,
5
- } from "@remit/data-ports";
6
- import type {
7
- FilterConfig,
8
- MessageEmbedder,
9
- PlacementMoveService,
10
- } from "@remit/mailbox-service";
11
- import {
12
- EMBEDDING_PROVIDER_OFF,
13
- readEmbeddingProviderFromEnv,
14
- } from "@remit/search-service/from-env";
15
- import { getMessageEmbedder } from "./message-embedder.js";
16
-
17
- export interface FilterConfigDeps {
18
- filterService: IFilterRepository;
19
- filterAnchorService: IFilterAnchorRepository;
20
- messageLabelService: IMessageLabelRepository;
21
- placementMoveService?: PlacementMoveService;
22
- }
23
-
24
- /**
25
- * The env-selected embedder, or none on an instance with semantic search off.
26
- *
27
- * `off` embeds nothing by design, and its embedder says so by throwing
28
- * (EmbeddingDisabledError). The pipeline has a designed skip for a *missing*
29
- * embedder — semantic filters are passed over and logged at debug — but a
30
- * present embedder that throws lands in the per-filter catch instead, which is
31
- * an error-level `filter_anchor_match_failed` for every semantic filter on
32
- * every synced message. Same outcome, a log nobody can read.
33
- */
34
- const embedderFromEnv = (): MessageEmbedder | undefined =>
35
- readEmbeddingProviderFromEnv() === EMBEDDING_PROVIDER_OFF
36
- ? undefined
37
- : getMessageEmbedder();
38
-
39
- /**
40
- * Assemble the index-time filter config the body-sync pass runs (RFC 034). The
41
- * embedder is provisioned from env exactly as the backend read-path and
42
- * search-index worker provision theirs, so a semantic (anchor-only) filter is
43
- * evaluated on incoming mail instead of silently skipped.
44
- *
45
- * Absent the placement mover there is no move path, so filters stay off — a
46
- * matched filter's actions reuse the same enqueue plumbing the placement mover
47
- * owns.
48
- */
49
- export const buildFilterConfig = (
50
- deps: FilterConfigDeps,
51
- embedder?: MessageEmbedder,
52
- ): FilterConfig | undefined => {
53
- const { placementMoveService } = deps;
54
- if (!placementMoveService) return undefined;
55
- const resolved = embedder ?? embedderFromEnv();
56
- return {
57
- filterService: deps.filterService,
58
- filterAnchorService: deps.filterAnchorService,
59
- messageLabelService: deps.messageLabelService,
60
- placementMoveService,
61
- ...(resolved ? { embedder: resolved } : {}),
62
- };
63
- };
@@ -1,39 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import {
4
- _resetMessageEmbedderForTest,
5
- getMessageEmbedder,
6
- } from "./message-embedder.js";
7
-
8
- describe("getMessageEmbedder", () => {
9
- afterEach(() => {
10
- _resetMessageEmbedderForTest();
11
- });
12
-
13
- it("adapts the batch embedding service into a single-text message embedder", async () => {
14
- const vector = await getMessageEmbedder().embed("invoice from stripe");
15
-
16
- assert.ok(Array.isArray(vector));
17
- assert.ok(vector.length > 0);
18
- assert.ok(vector.every((value) => typeof value === "number"));
19
- });
20
-
21
- it("embeds the same text deterministically", async () => {
22
- const embedder = getMessageEmbedder();
23
- const a = await embedder.embed("your monthly receipt");
24
- const b = await embedder.embed("your monthly receipt");
25
-
26
- assert.deepEqual(a, b);
27
- });
28
-
29
- it("memoizes the embedder across calls", () => {
30
- assert.strictEqual(getMessageEmbedder(), getMessageEmbedder());
31
- });
32
-
33
- it("rebuilds after a reset", () => {
34
- const first = getMessageEmbedder();
35
- _resetMessageEmbedderForTest();
36
-
37
- assert.notStrictEqual(getMessageEmbedder(), first);
38
- });
39
- });
@@ -1,33 +0,0 @@
1
- import type { MessageEmbedder } from "@remit/mailbox-service";
2
- import { buildEmbeddingServiceFromEnv } from "@remit/search-service/from-env";
3
-
4
- /**
5
- * Adapt the batch {@link EmbeddingService} the rest of the stack composes from
6
- * env into the single-text {@link MessageEmbedder} the filter pipeline needs: one
7
- * incoming message becomes one message-level vector to compare against a filter's
8
- * persisted anchor (RFC 034 Decision 2.1/2.3).
9
- *
10
- * The embedder is selected by the same `SEARCH_EMBEDDING_*` env the backend and
11
- * search-index worker read, so the worker embeds under the identical model the
12
- * anchors were built with. The instance is memoized across warm invocations so a
13
- * local Transformers.js model loads once per container rather than per event.
14
- */
15
- let cached: MessageEmbedder | undefined;
16
-
17
- export const getMessageEmbedder = (): MessageEmbedder => {
18
- if (cached) return cached;
19
- const service = buildEmbeddingServiceFromEnv();
20
- cached = {
21
- embed: async (text: string): Promise<number[]> => {
22
- const [vector] = await service.embed([text]);
23
- return vector;
24
- },
25
- embeddingId: service.embeddingId,
26
- };
27
- return cached;
28
- };
29
-
30
- /** Reset the singleton — test use only. */
31
- export const _resetMessageEmbedderForTest = (): void => {
32
- cached = undefined;
33
- };