@remit/imap-worker 0.0.60 → 0.0.61
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/package.json +1 -1
- package/src/filter-config.test.ts +52 -0
- package/src/filter-config.ts +21 -1
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type PlacementMoveService,
|
|
16
16
|
} from "@remit/mailbox-service";
|
|
17
17
|
import { buildFilterConfig, type FilterConfigDeps } from "./filter-config.js";
|
|
18
|
+
import { _resetMessageEmbedderForTest } from "./message-embedder.js";
|
|
18
19
|
|
|
19
20
|
const anchorOnlyFilter = (destinationMailboxId: string): FilterItem =>
|
|
20
21
|
({
|
|
@@ -117,3 +118,54 @@ describe("buildFilterConfig", () => {
|
|
|
117
118
|
assert.equal(decision.move, undefined);
|
|
118
119
|
});
|
|
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
|
+
});
|
package/src/filter-config.ts
CHANGED
|
@@ -8,6 +8,10 @@ import type {
|
|
|
8
8
|
MessageEmbedder,
|
|
9
9
|
PlacementMoveService,
|
|
10
10
|
} from "@remit/mailbox-service";
|
|
11
|
+
import {
|
|
12
|
+
EMBEDDING_PROVIDER_OFF,
|
|
13
|
+
readEmbeddingProviderFromEnv,
|
|
14
|
+
} from "@remit/search-service/from-env";
|
|
11
15
|
import { getMessageEmbedder } from "./message-embedder.js";
|
|
12
16
|
|
|
13
17
|
export interface FilterConfigDeps {
|
|
@@ -17,6 +21,21 @@ export interface FilterConfigDeps {
|
|
|
17
21
|
placementMoveService?: PlacementMoveService;
|
|
18
22
|
}
|
|
19
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
|
+
|
|
20
39
|
/**
|
|
21
40
|
* Assemble the index-time filter config the body-sync pass runs (RFC 034). The
|
|
22
41
|
* embedder is provisioned from env exactly as the backend read-path and
|
|
@@ -33,11 +52,12 @@ export const buildFilterConfig = (
|
|
|
33
52
|
): FilterConfig | undefined => {
|
|
34
53
|
const { placementMoveService } = deps;
|
|
35
54
|
if (!placementMoveService) return undefined;
|
|
55
|
+
const resolved = embedder ?? embedderFromEnv();
|
|
36
56
|
return {
|
|
37
57
|
filterService: deps.filterService,
|
|
38
58
|
filterAnchorService: deps.filterAnchorService,
|
|
39
59
|
messageLabelService: deps.messageLabelService,
|
|
40
60
|
placementMoveService,
|
|
41
|
-
embedder:
|
|
61
|
+
...(resolved ? { embedder: resolved } : {}),
|
|
42
62
|
};
|
|
43
63
|
};
|