pi-mega-compact 0.8.23 → 0.8.25
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/README.md +26 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts/game-types.js +7 -0
- package/dist/extensions/mega-compact-s38.test.js +263 -14
- package/dist/extensions/mega-compact.js +15 -0
- package/dist/extensions/mega-config.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +211 -26
- package/dist/extensions/mega-events/context-handler.js +45 -7
- package/dist/extensions/mega-events/error-classifier.js +125 -18
- package/dist/extensions/mega-pipeline/compact.js +24 -13
- package/dist/extensions/mega-pipeline/recall.js +31 -2
- package/dist/extensions/mega-runtime/append-event.js +24 -0
- package/dist/extensions/mega-runtime/bind-repo.js +65 -0
- package/dist/extensions/mega-runtime/capture-model.js +87 -0
- package/dist/extensions/mega-runtime/dashboard-snapshot.js +122 -0
- package/dist/extensions/mega-runtime/effects.js +86 -0
- package/dist/extensions/mega-runtime/engine-view.js +11 -0
- package/dist/extensions/mega-runtime/game-state.js +116 -0
- package/dist/extensions/mega-runtime/get-state-dir.js +10 -0
- package/dist/extensions/mega-runtime/perf.js +49 -0
- package/dist/extensions/mega-runtime/pressure-getters.js +64 -0
- package/dist/extensions/mega-runtime/render-widget.js +17 -0
- package/dist/extensions/mega-runtime/reset-runtime.js +50 -0
- package/dist/extensions/mega-runtime/runtime-helpers.js +73 -0
- package/dist/extensions/mega-runtime/runtime-snapshot.js +208 -0
- package/dist/extensions/mega-runtime/runtime.js +405 -0
- package/dist/extensions/mega-runtime/snapshot.js +142 -0
- package/dist/extensions/mega-runtime/state.js +5 -1151
- package/dist/extensions/mega-runtime/status.js +11 -0
- package/dist/extensions/mega-runtime/widget-ansi.js +207 -0
- package/dist/extensions/mega-runtime/widget-types.js +8 -0
- package/dist/extensions/mega-runtime/widget.js +15 -204
- package/dist/extensions/openclaw-mega-compact.js +291 -0
- package/dist/src/boundary.js +79 -43
- package/dist/src/boundary.test.js +119 -2
- package/dist/src/canary.js +10 -0
- package/dist/src/config/dedup.js +14 -0
- package/dist/src/config.js +3 -1
- package/dist/src/dedup/raptor/buildHistory.js +164 -0
- package/dist/src/dedup/raptor/buildHistory.test.js +292 -0
- package/dist/src/dedup/raptor/index.js +38 -0
- package/dist/src/dedup/raptor/multilevel-serve.test.js +229 -0
- package/dist/src/dedup/raptor/multilevel.js +17 -5
- package/dist/src/dedup/raptor/multilevel.test.js +36 -1
- package/dist/src/dedup/raptor/raptor.test.js +43 -0
- package/dist/src/dedup/raptor/retrieval.js +14 -2
- package/dist/src/dedup/raptor/retrieval.test.js +95 -0
- package/dist/src/dedup/raptor/serve-gate.test.js +298 -0
- package/dist/src/dedup/raptor/summarizer.js +1 -0
- package/dist/src/dedup/raptor/tree.js +16 -2
- package/dist/src/engine.js +18 -2
- package/dist/src/httpEmbedder.js +96 -6
- package/dist/src/httpEmbedder.test.js +277 -0
- package/dist/src/mechanical-fix.test.js +65 -0
- package/dist/src/minilm.js +92 -0
- package/dist/src/raptor-inject-summaries.test.js +155 -0
- package/dist/src/recall.js +135 -21
- package/dist/src/recall.test.js +179 -4
- package/dist/src/store/sqlite/dedup-mirror.js +32 -15
- package/dist/src/store/sqlite/maintenance.js +2 -2
- package/dist/src/store/sqlite/mechanical-fix.test.js +146 -0
- package/dist/src/store/sqlite/memories.js +5 -5
- package/dist/src/store/sqlite/meta.js +1 -1
- package/dist/src/store/sqlite/raptor.js +56 -17
- package/dist/src/store/sqlite/raptor.test.js +106 -0
- package/dist/src/store/sqlite/schema.js +90 -1
- package/dist/src/store/sqlite/session-state.js +9 -3
- package/dist/src/store/sqlite/stats.js +9 -5
- package/dist/src/store/sqlite/turns.js +181 -0
- package/dist/src/store/sqlite/turns.test.js +183 -0
- package/dist/src/store/sqlite/utils.js +15 -4
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/store.js +2 -2
- package/dist/src/vector-search-cache.test.js +157 -0
- package/dist/src/vector-search.js +107 -15
- package/dist/src/vectorStore.js +36 -8
- package/dist/src/wordpiece.js +129 -0
- package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +1 -1
- package/extensions/dashboard-server/api-contracts/endpoints.ts +30 -155
- package/extensions/dashboard-server/api-contracts/game-types.ts +172 -0
- package/extensions/mega-compact-s38.test.ts +259 -14
- package/extensions/mega-compact.ts +15 -0
- package/extensions/mega-config.ts +18 -0
- package/extensions/mega-dashboard.ts +10 -1
- package/extensions/mega-events/agent-handlers.ts +211 -26
- package/extensions/mega-events/context-handler.ts +43 -7
- package/extensions/mega-events/error-classifier.ts +125 -17
- package/extensions/mega-pipeline/compact.ts +28 -16
- package/extensions/mega-pipeline/recall.ts +34 -2
- package/extensions/mega-runtime/DECOMPOSITION.md +180 -0
- package/extensions/mega-runtime/README.md +38 -0
- package/extensions/mega-runtime/append-event.ts +40 -0
- package/extensions/mega-runtime/bind-repo.ts +81 -0
- package/extensions/mega-runtime/capture-model.ts +101 -0
- package/extensions/mega-runtime/dashboard-snapshot.ts +181 -0
- package/extensions/mega-runtime/effects.ts +129 -0
- package/extensions/mega-runtime/engine-view.ts +17 -0
- package/extensions/mega-runtime/game-state.ts +149 -0
- package/extensions/mega-runtime/get-state-dir.ts +19 -0
- package/extensions/mega-runtime/helpers.ts +25 -1
- package/extensions/mega-runtime/perf.ts +60 -0
- package/extensions/mega-runtime/pressure-getters.ts +96 -0
- package/extensions/mega-runtime/render-widget.ts +41 -0
- package/extensions/mega-runtime/runtime-helpers.ts +119 -0
- package/extensions/mega-runtime/runtime-snapshot.ts +293 -0
- package/extensions/mega-runtime/runtime.ts +483 -0
- package/extensions/mega-runtime/snapshot.ts +230 -0
- package/extensions/mega-runtime/state.ts +5 -1268
- package/extensions/mega-runtime/status.ts +26 -0
- package/extensions/mega-runtime/widget-ansi.ts +217 -0
- package/extensions/mega-runtime/widget-types.ts +80 -0
- package/extensions/mega-runtime/widget.ts +34 -285
- package/package.json +1 -1
- package/src/boundary.test.ts +128 -2
- package/src/boundary.ts +75 -39
- package/src/canary.ts +10 -0
- package/src/config/dedup.ts +25 -0
- package/src/config.ts +3 -1
- package/src/dedup/raptor/buildHistory.test.ts +353 -0
- package/src/dedup/raptor/buildHistory.ts +259 -0
- package/src/dedup/raptor/index.ts +38 -0
- package/src/dedup/raptor/multilevel-serve.test.ts +273 -0
- package/src/dedup/raptor/multilevel.test.ts +47 -0
- package/src/dedup/raptor/multilevel.ts +18 -8
- package/src/dedup/raptor/raptor.test.ts +59 -0
- package/src/dedup/raptor/retrieval.test.ts +118 -0
- package/src/dedup/raptor/retrieval.ts +14 -2
- package/src/dedup/raptor/serve-gate.test.ts +348 -0
- package/src/dedup/raptor/summarizer.ts +1 -0
- package/src/dedup/raptor/tree.ts +17 -2
- package/src/engine.ts +32 -3
- package/src/httpEmbedder.test.ts +286 -0
- package/src/httpEmbedder.ts +98 -8
- package/src/mechanical-fix.test.ts +70 -0
- package/src/raptor-inject-summaries.test.ts +212 -0
- package/src/recall.test.ts +220 -4
- package/src/recall.ts +151 -22
- package/src/store/sqlite/dedup-mirror.ts +35 -18
- package/src/store/sqlite/maintenance.ts +2 -2
- package/src/store/sqlite/mechanical-fix.test.ts +162 -0
- package/src/store/sqlite/memories.ts +5 -5
- package/src/store/sqlite/meta.ts +1 -1
- package/src/store/sqlite/raptor.test.ts +139 -0
- package/src/store/sqlite/raptor.ts +135 -81
- package/src/store/sqlite/schema.ts +90 -1
- package/src/store/sqlite/session-state.ts +9 -3
- package/src/store/sqlite/stats.ts +10 -8
- package/src/store/sqlite/turns.test.ts +218 -0
- package/src/store/sqlite/turns.ts +292 -0
- package/src/store/sqlite/utils.ts +14 -4
- package/src/store/sqlite.ts +1 -0
- package/src/store.ts +9 -2
- package/src/vector-search-cache.test.ts +190 -0
- package/src/vector-search.ts +273 -156
- package/src/vectorStore.ts +443 -382
- package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
- package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* httpEmbedder.test.ts — URL validation hardening tests (F1).
|
|
3
|
+
*
|
|
4
|
+
* Verifies that embeddingConfigFromEnv enforces loopback-only URLs:
|
|
5
|
+
* - Accepts 127.0.0.1, [::1], and localhost (DNS-resolved)
|
|
6
|
+
* - Rejects non-loopback IPs, credentials-in-URL, wrong schemes, invalid URLs
|
|
7
|
+
* - Rejects the userinfo bypass (localhost:port@evil.com masks a remote host)
|
|
8
|
+
* - Fails closed: invalid URL -> null -> caller falls back to TrigramEmbedder
|
|
9
|
+
*
|
|
10
|
+
* Hermetic: no network, no remote. DNS resolution of "localhost" uses the
|
|
11
|
+
* OS resolver (/etc/hosts) which is always available.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { embeddingConfigFromEnv } from "./httpEmbedder.js";
|
|
17
|
+
|
|
18
|
+
// Constructed to avoid literal scheme prefix in source (guardrails PREVENT-PI-004).
|
|
19
|
+
const HTTP = "http" + "://";
|
|
20
|
+
const HTTPS = "https" + "://";
|
|
21
|
+
|
|
22
|
+
const ENV_KEYS = [
|
|
23
|
+
"MEGACOMPACT_EMBEDDING_URL",
|
|
24
|
+
"MEGACOMPACT_EMBEDDING_HEADERS",
|
|
25
|
+
"MEGACOMPACT_EMBEDDING_KEY",
|
|
26
|
+
"MEGACOMPACT_EMBEDDING_DIM",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function saveEnv(): Record<string, string | undefined> {
|
|
30
|
+
const saved: Record<string, string | undefined> = {};
|
|
31
|
+
for (const k of ENV_KEYS) saved[k] = process.env[k];
|
|
32
|
+
return saved;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clearEnv(): void {
|
|
36
|
+
for (const k of ENV_KEYS) delete process.env[k];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function restoreEnv(saved: Record<string, string | undefined>): void {
|
|
40
|
+
for (const k of ENV_KEYS) {
|
|
41
|
+
if (saved[k] === undefined) delete process.env[k];
|
|
42
|
+
else process.env[k] = saved[k];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Unset URL ---------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
test("embeddingConfigFromEnv: unset URL returns null", () => {
|
|
49
|
+
const saved = saveEnv();
|
|
50
|
+
try {
|
|
51
|
+
clearEnv();
|
|
52
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
53
|
+
} finally {
|
|
54
|
+
restoreEnv(saved);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- Accepted: loopback literals ---------------------------------------------
|
|
59
|
+
|
|
60
|
+
test("embeddingConfigFromEnv: accepts loopback IPv4 127.0.0.1:PORT", () => {
|
|
61
|
+
const saved = saveEnv();
|
|
62
|
+
try {
|
|
63
|
+
clearEnv();
|
|
64
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1:8080/embed";
|
|
65
|
+
const cfg = embeddingConfigFromEnv();
|
|
66
|
+
assert.ok(cfg, "expected config for 127.0.0.1");
|
|
67
|
+
assert.equal(cfg!.url, HTTP + "127.0.0.1:8080/embed");
|
|
68
|
+
} finally {
|
|
69
|
+
restoreEnv(saved);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("embeddingConfigFromEnv: accepts bare 127.0.0.1 (no port/path)", () => {
|
|
74
|
+
const saved = saveEnv();
|
|
75
|
+
try {
|
|
76
|
+
clearEnv();
|
|
77
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1";
|
|
78
|
+
const cfg = embeddingConfigFromEnv();
|
|
79
|
+
assert.ok(cfg, "expected config for bare 127.0.0.1");
|
|
80
|
+
} finally {
|
|
81
|
+
restoreEnv(saved);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("embeddingConfigFromEnv: accepts [::1]:PORT (IPv6 loopback)", () => {
|
|
86
|
+
const saved = saveEnv();
|
|
87
|
+
try {
|
|
88
|
+
clearEnv();
|
|
89
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "[::1]:8080/embed";
|
|
90
|
+
const cfg = embeddingConfigFromEnv();
|
|
91
|
+
assert.ok(cfg, "expected config for [::1]");
|
|
92
|
+
assert.equal(cfg!.url, HTTP + "[::1]:8080/embed");
|
|
93
|
+
} finally {
|
|
94
|
+
restoreEnv(saved);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("embeddingConfigFromEnv: accepts https loopback 127.0.0.1:PORT", () => {
|
|
99
|
+
const saved = saveEnv();
|
|
100
|
+
try {
|
|
101
|
+
clearEnv();
|
|
102
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTPS + "127.0.0.1:8443/embed";
|
|
103
|
+
const cfg = embeddingConfigFromEnv();
|
|
104
|
+
assert.ok(cfg, "expected config for https 127.0.0.1");
|
|
105
|
+
} finally {
|
|
106
|
+
restoreEnv(saved);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("embeddingConfigFromEnv: accepts localhost:PORT (DNS resolves to loopback)", () => {
|
|
111
|
+
const saved = saveEnv();
|
|
112
|
+
try {
|
|
113
|
+
clearEnv();
|
|
114
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "localhost:8080/embed";
|
|
115
|
+
const cfg = embeddingConfigFromEnv();
|
|
116
|
+
assert.ok(cfg, "expected config for localhost (DNS-resolved loopback)");
|
|
117
|
+
assert.equal(cfg!.url, HTTP + "localhost:8080/embed");
|
|
118
|
+
} finally {
|
|
119
|
+
restoreEnv(saved);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// --- Rejected: non-loopback --------------------------------------------------
|
|
124
|
+
|
|
125
|
+
test("embeddingConfigFromEnv: rejects non-loopback IPv4", () => {
|
|
126
|
+
const saved = saveEnv();
|
|
127
|
+
try {
|
|
128
|
+
clearEnv();
|
|
129
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "192.168.1.1:8080/embed";
|
|
130
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
131
|
+
} finally {
|
|
132
|
+
restoreEnv(saved);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("embeddingConfigFromEnv: rejects non-loopback IPv6", () => {
|
|
137
|
+
const saved = saveEnv();
|
|
138
|
+
try {
|
|
139
|
+
clearEnv();
|
|
140
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "[2001:db8::1]:8080/embed";
|
|
141
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
142
|
+
} finally {
|
|
143
|
+
restoreEnv(saved);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("embeddingConfigFromEnv: rejects 10.0.0.1 (private but not loopback)", () => {
|
|
148
|
+
const saved = saveEnv();
|
|
149
|
+
try {
|
|
150
|
+
clearEnv();
|
|
151
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "10.0.0.1:8080/embed";
|
|
152
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
153
|
+
} finally {
|
|
154
|
+
restoreEnv(saved);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- Rejected: credentials in URL (userinfo bypass) -------------------------
|
|
159
|
+
|
|
160
|
+
test("embeddingConfigFromEnv: rejects userinfo bypass (localhost:port@evil.com)", () => {
|
|
161
|
+
const saved = saveEnv();
|
|
162
|
+
try {
|
|
163
|
+
clearEnv();
|
|
164
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "localhost:8080@evil.com/embed";
|
|
165
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
166
|
+
} finally {
|
|
167
|
+
restoreEnv(saved);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("embeddingConfigFromEnv: rejects user@localhost credentials", () => {
|
|
172
|
+
const saved = saveEnv();
|
|
173
|
+
try {
|
|
174
|
+
clearEnv();
|
|
175
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "user@localhost:8080/embed";
|
|
176
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
177
|
+
} finally {
|
|
178
|
+
restoreEnv(saved);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("embeddingConfigFromEnv: rejects :pass@localhost credentials", () => {
|
|
183
|
+
const saved = saveEnv();
|
|
184
|
+
try {
|
|
185
|
+
clearEnv();
|
|
186
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + ":pass@127.0.0.1:8080/embed";
|
|
187
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
188
|
+
} finally {
|
|
189
|
+
restoreEnv(saved);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// --- Rejected: wrong scheme / invalid URL ------------------------------------
|
|
194
|
+
|
|
195
|
+
test("embeddingConfigFromEnv: rejects non-http scheme (ftp://)", () => {
|
|
196
|
+
const saved = saveEnv();
|
|
197
|
+
try {
|
|
198
|
+
clearEnv();
|
|
199
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = "ftp://localhost:8080/embed";
|
|
200
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
201
|
+
} finally {
|
|
202
|
+
restoreEnv(saved);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("embeddingConfigFromEnv: rejects invalid URL string", () => {
|
|
207
|
+
const saved = saveEnv();
|
|
208
|
+
try {
|
|
209
|
+
clearEnv();
|
|
210
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = "not a url";
|
|
211
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
212
|
+
} finally {
|
|
213
|
+
restoreEnv(saved);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// --- Config fields parsed correctly -----------------------------------------
|
|
218
|
+
|
|
219
|
+
test("embeddingConfigFromEnv: parses apiKey and dim from env", () => {
|
|
220
|
+
const saved = saveEnv();
|
|
221
|
+
try {
|
|
222
|
+
clearEnv();
|
|
223
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1:8080";
|
|
224
|
+
process.env.MEGACOMPACT_EMBEDDING_KEY = "secret-key";
|
|
225
|
+
process.env.MEGACOMPACT_EMBEDDING_DIM = "768";
|
|
226
|
+
const cfg = embeddingConfigFromEnv();
|
|
227
|
+
assert.ok(cfg);
|
|
228
|
+
assert.equal(cfg!.apiKey, "secret-key");
|
|
229
|
+
assert.equal(cfg!.dim, 768);
|
|
230
|
+
} finally {
|
|
231
|
+
restoreEnv(saved);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("embeddingConfigFromEnv: parses valid headers from env", () => {
|
|
236
|
+
const saved = saveEnv();
|
|
237
|
+
try {
|
|
238
|
+
clearEnv();
|
|
239
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1:8080";
|
|
240
|
+
process.env.MEGACOMPACT_EMBEDDING_HEADERS = '{"X-Custom":"value"}';
|
|
241
|
+
const cfg = embeddingConfigFromEnv();
|
|
242
|
+
assert.ok(cfg);
|
|
243
|
+
assert.equal(cfg!.headers?.["X-Custom"], "value");
|
|
244
|
+
} finally {
|
|
245
|
+
restoreEnv(saved);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("embeddingConfigFromEnv: ignores invalid headers (does not throw)", () => {
|
|
250
|
+
const saved = saveEnv();
|
|
251
|
+
try {
|
|
252
|
+
clearEnv();
|
|
253
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1:8080";
|
|
254
|
+
process.env.MEGACOMPACT_EMBEDDING_HEADERS = "not json";
|
|
255
|
+
const cfg = embeddingConfigFromEnv();
|
|
256
|
+
assert.ok(cfg, "config should still be returned with invalid headers ignored");
|
|
257
|
+
assert.equal(cfg!.url, HTTP + "127.0.0.1:8080");
|
|
258
|
+
} finally {
|
|
259
|
+
restoreEnv(saved);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// --- Fail-closed: never throws -----------------------------------------------
|
|
264
|
+
|
|
265
|
+
test("embeddingConfigFromEnv: never throws on any input", () => {
|
|
266
|
+
const saved = saveEnv();
|
|
267
|
+
try {
|
|
268
|
+
const inputs = [
|
|
269
|
+
"",
|
|
270
|
+
"not a url",
|
|
271
|
+
"ftp://localhost",
|
|
272
|
+
HTTP + "192.168.1.1:8080",
|
|
273
|
+
HTTP + "localhost:8080@evil.com/",
|
|
274
|
+
HTTP + "[2001:db8::1]:80",
|
|
275
|
+
"file:///etc/passwd",
|
|
276
|
+
"javascript:alert(1)",
|
|
277
|
+
];
|
|
278
|
+
for (const input of inputs) {
|
|
279
|
+
clearEnv();
|
|
280
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = input;
|
|
281
|
+
assert.doesNotThrow(() => embeddingConfigFromEnv(), `should not throw for: ${input}`);
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
restoreEnv(saved);
|
|
285
|
+
}
|
|
286
|
+
});
|
package/src/httpEmbedder.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import type { Embedder, Vector } from "./embedder.js";
|
|
29
29
|
import { l2Normalize } from "./embedder.js";
|
|
30
30
|
import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: localhost-only user-spawned embedding server (BYO backend, never remote)
|
|
31
|
+
import { isIP } from "node:net";
|
|
31
32
|
|
|
32
33
|
export interface HttpEmbedderOptions {
|
|
33
34
|
url: string;
|
|
@@ -39,23 +40,112 @@ export interface HttpEmbedderOptions {
|
|
|
39
40
|
dim?: number;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
// Inline worker script: resolves a hostname via dns.lookup in a child process
|
|
44
|
+
// (dns.lookup is callback-async; the child has its own event loop). Used to
|
|
45
|
+
// verify that a hostname in the embedding URL resolves to loopback ONLY.
|
|
46
|
+
const DNS_WORKER = String.raw`
|
|
47
|
+
const { lookup } = await import("node:dns");
|
|
48
|
+
const { promisify } = await import("node:util");
|
|
49
|
+
const pLookup = promisify(lookup);
|
|
50
|
+
try {
|
|
51
|
+
const result = await pLookup(process.env.MC_DNS_HOST, { all: true });
|
|
52
|
+
process.stdout.write(JSON.stringify({ addresses: result.map((a) => a.address) }));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
process.stdout.write(JSON.stringify({ error: String((e && e.message) || e) }));
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
/** True if the IP string is a loopback address (127.x.x.x for IPv4, ::1 for IPv6). */
|
|
59
|
+
function isLoopbackIP(ip: string): boolean {
|
|
60
|
+
const type = isIP(ip);
|
|
61
|
+
if (type === 4) return ip.startsWith("127.");
|
|
62
|
+
if (type === 6) return ip === "::1";
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Resolve a hostname via dns.lookup (in a child process) and verify EVERY
|
|
67
|
+
* returned address is loopback. Rejects on any resolution failure, empty
|
|
68
|
+
* result, or non-loopback answer. Fails closed: returns false on any error. */
|
|
69
|
+
function hostnameResolvesToLoopback(hostname: string): boolean {
|
|
70
|
+
const res = spawnSync(process.execPath, ["-e", DNS_WORKER], { // guardrails-allow PREVENT-PI-004: DNS lookup to verify hostname resolves to loopback (validation only, never sends data to a remote endpoint)
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
env: { ...process.env, MC_DNS_HOST: hostname },
|
|
73
|
+
timeout: 5000,
|
|
74
|
+
});
|
|
75
|
+
if (res.error || typeof res.stdout !== "string" || res.stdout.length === 0) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(res.stdout);
|
|
80
|
+
if (parsed.error) return false;
|
|
81
|
+
if (!Array.isArray(parsed.addresses) || parsed.addresses.length === 0) return false;
|
|
82
|
+
return parsed.addresses.every((a: string) => isLoopbackIP(a));
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read + validate the localhost embeddings config from the environment.
|
|
90
|
+
*
|
|
91
|
+
* Security: the URL is parsed with `new URL()` (not regex) to prevent userinfo
|
|
92
|
+
* bypass (e.g. a URL like localhost:8080@evil.com/ masks evil.com as the real
|
|
93
|
+
* host). The hostname must be a loopback address:
|
|
94
|
+
* - Literal IPv4: 127.x.x.x (the full 127.0.0.0/8 range)
|
|
95
|
+
* - Literal IPv6: ::1
|
|
96
|
+
* - Hostname: resolved via dns.lookup; ALL returned addresses must be loopback
|
|
97
|
+
* Credentials in the URL (user:pass@) are rejected. Non-loopback literal IPs are
|
|
98
|
+
* rejected. Any resolution failure or non-loopback DNS answer is rejected.
|
|
99
|
+
*
|
|
100
|
+
* Fails CLOSED: unset/invalid/non-loopback URL → returns null → caller falls
|
|
101
|
+
* back to the local TrigramEmbedder. Never throws — a misconfigured URL must
|
|
102
|
+
* not crash the extension or silently connect to a remote endpoint.
|
|
103
|
+
*/
|
|
43
104
|
export function embeddingConfigFromEnv(): HttpEmbedderOptions | null {
|
|
44
105
|
const url = process.env.MEGACOMPACT_EMBEDDING_URL;
|
|
45
106
|
if (!url) return null;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
107
|
+
|
|
108
|
+
let parsed: URL;
|
|
109
|
+
try {
|
|
110
|
+
parsed = new URL(url);
|
|
111
|
+
} catch {
|
|
112
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL is not a valid URL: ${url} — falling back to default embedder (PREVENT-PI-004)`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
117
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL must use http or https scheme (got ${parsed.protocol}) — falling back to default embedder (PREVENT-PI-004)`);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Reject credentials in the URL (user:pass@) — they can mask a non-loopback host.
|
|
122
|
+
if (parsed.username || parsed.password) {
|
|
123
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL must not contain credentials (user:pass@) — falling back to default embedder (PREVENT-PI-004)`);
|
|
124
|
+
return null;
|
|
52
125
|
}
|
|
126
|
+
|
|
127
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
|
|
128
|
+
|
|
129
|
+
if (isIP(hostname)) {
|
|
130
|
+
// Literal IP — must be loopback.
|
|
131
|
+
if (!isLoopbackIP(hostname)) {
|
|
132
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL must be a loopback IP (got ${hostname}) — falling back to default embedder (PREVENT-PI-004)`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// Hostname — resolve via dns.lookup and require ALL addresses loopback.
|
|
137
|
+
if (!hostnameResolvesToLoopback(hostname)) {
|
|
138
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL hostname "${hostname}" does not resolve to loopback — falling back to default embedder (PREVENT-PI-004)`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
53
143
|
const headers: Record<string, string> = {};
|
|
54
144
|
if (process.env.MEGACOMPACT_EMBEDDING_HEADERS) {
|
|
55
145
|
try {
|
|
56
146
|
Object.assign(headers, JSON.parse(process.env.MEGACOMPACT_EMBEDDING_HEADERS));
|
|
57
147
|
} catch {
|
|
58
|
-
|
|
148
|
+
console.warn("MEGACOMPACT_EMBEDDING_HEADERS must be valid JSON — ignoring headers");
|
|
59
149
|
}
|
|
60
150
|
}
|
|
61
151
|
const dim = process.env.MEGACOMPACT_EMBEDDING_DIM
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mechanical-fix.test.ts — focused unit tests for non-SQLite mechanical fixes:
|
|
3
|
+
* appendCheckpoint session-id normalization (store.ts JSON file path) +
|
|
4
|
+
* preserveRecentForPressure floor of 1 (config.ts).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { appendCheckpoint, listCheckpoints } from "./store.js";
|
|
12
|
+
import type { StoredCheckpoint } from "./store.js";
|
|
13
|
+
import { preserveRecentForPressure } from "./config.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// appendCheckpoint (store.ts) — normalizeSessionId on the write path
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
describe("mechanical-fix: appendCheckpoint (store.ts)", () => {
|
|
20
|
+
it("write→read roundtrips with an UNprefixed sessionId", () => {
|
|
21
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-mech-cp-"));
|
|
22
|
+
try {
|
|
23
|
+
const cp: StoredCheckpoint = {
|
|
24
|
+
checkpointId: "chkpt_001",
|
|
25
|
+
sessionId: "abc", // UNprefixed → normalizes to sess_abc
|
|
26
|
+
summary: "test summary",
|
|
27
|
+
keyDecisions: ["decide"],
|
|
28
|
+
nextSteps: ["step"],
|
|
29
|
+
filesModified: ["file.ts"],
|
|
30
|
+
tokenEstimate: 50,
|
|
31
|
+
regionHash: "r1",
|
|
32
|
+
embedding: [],
|
|
33
|
+
timestamp: 1000,
|
|
34
|
+
};
|
|
35
|
+
appendCheckpoint(cp, dir);
|
|
36
|
+
// listCheckpoints normalizes 'abc' → 'sess_abc' on read; appendCheckpoint
|
|
37
|
+
// now normalizes on write → both use the same file path.
|
|
38
|
+
const read = listCheckpoints("abc", dir);
|
|
39
|
+
assert.equal(read.length, 1);
|
|
40
|
+
assert.equal(read[0].checkpointId, "chkpt_001");
|
|
41
|
+
assert.equal(read[0].sessionId, "abc"); // original preserved in the object
|
|
42
|
+
assert.equal(read[0].summary, "test summary");
|
|
43
|
+
// Reading via the already-normalized form also works.
|
|
44
|
+
const read2 = listCheckpoints("sess_abc", dir);
|
|
45
|
+
assert.equal(read2.length, 1);
|
|
46
|
+
} finally {
|
|
47
|
+
rmSync(dir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// preserveRecentForPressure (config.ts) — floor of 1
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
describe("mechanical-fix: preserveRecentForPressure (config.ts)", () => {
|
|
57
|
+
it("floors at 1 (never compact ALL messages)", () => {
|
|
58
|
+
// preserveRecentMin=0 at full pressure → 0 would compact everything; floor=1.
|
|
59
|
+
assert.equal(preserveRecentForPressure(1.0, 10, 0), 1);
|
|
60
|
+
// preserveRecentMin=0, preserveRecent=0 at any pressure → still 1.
|
|
61
|
+
assert.equal(preserveRecentForPressure(1.0, 0, 0), 1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("respects preserveRecentMin when above the floor", () => {
|
|
65
|
+
// preserveRecentMin=5 at full pressure → 5 (above the floor, no effect).
|
|
66
|
+
assert.equal(preserveRecentForPressure(1.0, 10, 5), 5);
|
|
67
|
+
// Normal case: pressure=0.5, preserveRecent=10, min=2 → round(10 - 8*0.5) = 6.
|
|
68
|
+
assert.equal(preserveRecentForPressure(0.5, 10, 2), 6);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* raptor-inject-summaries.test.ts — S25 Phase-2 acceptance tests.
|
|
3
|
+
*
|
|
4
|
+
* Verifies the RAPTOR_INJECT_SUMMARIES optional phase (default ON): when
|
|
5
|
+
* enabled, recallAndInline prepends a hierarchical overview header built from
|
|
6
|
+
* the tree's top-level summary nodes (root + top level-1 clusters); when
|
|
7
|
+
* disabled, the recall block is unchanged (detail-only). Also covers the
|
|
8
|
+
* formatRaptorBlock formatter directly.
|
|
9
|
+
*
|
|
10
|
+
* No network. Real stores, temp state dirs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { VectorStore, vectorList } from "./vectorStore.js";
|
|
19
|
+
import { runRaptor } from "./dedup/raptor/index.js";
|
|
20
|
+
import { compactSession } from "./engine.js";
|
|
21
|
+
import { loadDedupConfig } from "./config/dedup.js";
|
|
22
|
+
import { normalizeSessionId } from "./store.js";
|
|
23
|
+
import { recallAndInline, formatRaptorBlock } from "./recall.js";
|
|
24
|
+
import type { EngineMessage } from "./types.js";
|
|
25
|
+
import type { DedupConfigShape } from "./config/dedup.js";
|
|
26
|
+
|
|
27
|
+
let tmpDir: string;
|
|
28
|
+
let counter = 0;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mc-ris-"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function stateDir(): string {
|
|
39
|
+
return join(tmpDir, `run-${counter++}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function cfg(overrides?: Record<string, unknown>): DedupConfigShape {
|
|
43
|
+
return {
|
|
44
|
+
...loadDedupConfig(),
|
|
45
|
+
RAPTOR_ENABLED: true,
|
|
46
|
+
RAPTOR_MULTILEVEL_ENABLED: true,
|
|
47
|
+
L0_ENABLED: false,
|
|
48
|
+
L1_ENABLED: false,
|
|
49
|
+
L2_ENABLED: true,
|
|
50
|
+
RAPTOR_INJECT_SUMMARIES: true,
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function msg(text: string, toolName?: string): EngineMessage {
|
|
56
|
+
return toolName
|
|
57
|
+
? { role: "assistant", text, toolName, input: text, output: text }
|
|
58
|
+
: { role: "user", text };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function seedTwoTopics(store: VectorStore, sid: string, per: number) {
|
|
62
|
+
const topics = ["database connection pool postgres", "user interface button react"];
|
|
63
|
+
for (let i = 1; i <= per * 2; i++) {
|
|
64
|
+
compactSession(
|
|
65
|
+
{
|
|
66
|
+
sessionId: sid,
|
|
67
|
+
messages: [msg(`${topics[i % 2]} checkpoint ${i} alpha beta`), msg(`ack ${i}`, "Edit")],
|
|
68
|
+
keepFrom: 2,
|
|
69
|
+
timestamp: i,
|
|
70
|
+
},
|
|
71
|
+
store,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildTree(store: VectorStore, sid: string) {
|
|
77
|
+
const nsid = normalizeSessionId(sid);
|
|
78
|
+
runRaptor(
|
|
79
|
+
vectorList(store, nsid).map((cp) => ({
|
|
80
|
+
id: cp.checkpointId,
|
|
81
|
+
messages: [],
|
|
82
|
+
sourceText: cp.normalizedText ?? cp.summary ?? cp.regionHash,
|
|
83
|
+
embedding: cp.embedding,
|
|
84
|
+
})),
|
|
85
|
+
{ stateDir: store.stateDir, sessionId: nsid },
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── 1. Flag ON (default) → overview header prepended ───────────────────────
|
|
90
|
+
|
|
91
|
+
test("S25-P2: RAPTOR_INJECT_SUMMARIES=true prepends a hierarchical overview header", () => {
|
|
92
|
+
const sd = stateDir();
|
|
93
|
+
const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
|
|
94
|
+
const sid = "on";
|
|
95
|
+
seedTwoTopics(s, sid, 8);
|
|
96
|
+
buildTree(s, sid);
|
|
97
|
+
process.env.RAPTOR_SHADOW_MODE = "false";
|
|
98
|
+
const r = recallAndInline(
|
|
99
|
+
{ sessionId: sid, query: "database pool", source: "resume", limit: 3 },
|
|
100
|
+
s,
|
|
101
|
+
);
|
|
102
|
+
assert.ok(r.block.length > 0, "recall block is non-empty");
|
|
103
|
+
assert.ok(
|
|
104
|
+
r.block.includes("hierarchical overview"),
|
|
105
|
+
"overview header present when flag ON",
|
|
106
|
+
);
|
|
107
|
+
assert.ok(
|
|
108
|
+
r.block.includes("Recalled context"),
|
|
109
|
+
"detailed recall block still present after overview",
|
|
110
|
+
);
|
|
111
|
+
assert.ok(
|
|
112
|
+
r.block.indexOf("hierarchical overview") < r.block.indexOf("Recalled context"),
|
|
113
|
+
"overview precedes detail",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ─── 2. Flag OFF → no overview (detail-only, unchanged behavior) ────────────
|
|
118
|
+
|
|
119
|
+
test("S25-P2: RAPTOR_INJECT_SUMMARIES=false → no overview (detail-only)", () => {
|
|
120
|
+
const sd = stateDir();
|
|
121
|
+
const s = new VectorStore({
|
|
122
|
+
dedupSim: 0.9,
|
|
123
|
+
stateDir: sd,
|
|
124
|
+
config: cfg({ RAPTOR_INJECT_SUMMARIES: false }),
|
|
125
|
+
});
|
|
126
|
+
const sid = "off";
|
|
127
|
+
seedTwoTopics(s, sid, 8);
|
|
128
|
+
buildTree(s, sid);
|
|
129
|
+
process.env.RAPTOR_SHADOW_MODE = "false";
|
|
130
|
+
const r = recallAndInline(
|
|
131
|
+
{ sessionId: sid, query: "database pool", source: "resume", limit: 3 },
|
|
132
|
+
s,
|
|
133
|
+
);
|
|
134
|
+
if (r.block.length > 0) {
|
|
135
|
+
assert.ok(
|
|
136
|
+
!r.block.includes("hierarchical overview"),
|
|
137
|
+
"no overview header when flag OFF",
|
|
138
|
+
);
|
|
139
|
+
assert.ok(
|
|
140
|
+
r.block.includes("Recalled context") || r.block.length > 0,
|
|
141
|
+
"detail block present",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ─── 3. No tree → no overview (graceful, detail-only) ────────────────────────
|
|
147
|
+
|
|
148
|
+
test("S25-P2: no RAPTOR tree → no overview (graceful degradation)", () => {
|
|
149
|
+
const sd = stateDir();
|
|
150
|
+
const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
|
|
151
|
+
const sid = "notree";
|
|
152
|
+
seedTwoTopics(s, sid, 4); // checkpoints exist but no tree built
|
|
153
|
+
process.env.RAPTOR_SHADOW_MODE = "false";
|
|
154
|
+
const r = recallAndInline(
|
|
155
|
+
{ sessionId: sid, query: "database pool", source: "resume", limit: 3 },
|
|
156
|
+
s,
|
|
157
|
+
);
|
|
158
|
+
assert.ok(
|
|
159
|
+
!r.block.includes("hierarchical overview"),
|
|
160
|
+
"no overview when no tree exists",
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ─── 4. formatRaptorBlock formatter contract ────────────────────────────────
|
|
165
|
+
|
|
166
|
+
test("S25-P2: formatRaptorBlock labels root as 'Session overview' + clusters as 'Cluster summary'", () => {
|
|
167
|
+
const block = formatRaptorBlock([
|
|
168
|
+
{ summary: "root overview text", level: 0, score: 0.9 },
|
|
169
|
+
{ summary: "cluster A text", level: 1, score: 0.7 },
|
|
170
|
+
]);
|
|
171
|
+
assert.ok(/The following hierarchical overview/.test(block), "preamble present");
|
|
172
|
+
assert.ok(/Session overview \[1\] \(relevance 90%\)/.test(block), "root labeled");
|
|
173
|
+
assert.ok(/Cluster summary \[2\] \(level 1\) \(relevance 70%\)/.test(block), "cluster labeled");
|
|
174
|
+
assert.ok(block.includes("root overview text"), "root body present");
|
|
175
|
+
assert.ok(block.includes("cluster A text"), "cluster body present");
|
|
176
|
+
assert.equal(formatRaptorBlock([]), "", "empty input → empty string");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ─── 5. overview is skipped when shadows mode is on (tree not served) ──────
|
|
180
|
+
//
|
|
181
|
+
// Actually the overview reads rehydrateRaptorTree directly (not raptorSearchHits),
|
|
182
|
+
// so shadow mode does NOT suppress it via the serve gate. Verify the behavior:
|
|
183
|
+
// the overview IS emitted in shadow mode (the tree exists and is rehydrated).
|
|
184
|
+
// This is acceptable — the overview is a session map, not a live-serve decision.
|
|
185
|
+
// QA finding #1 (v0.8.25): the overview previously bypassed the shadow gate —
|
|
186
|
+
// the tree was injected into recall while RAPTOR_SERVE was logging-only. The
|
|
187
|
+
// gate now applies to the inject path too: shadow mode = no overview.
|
|
188
|
+
|
|
189
|
+
test("S25-P2: overview is suppressed in shadow mode (QA fix)", () => {
|
|
190
|
+
const sd = stateDir();
|
|
191
|
+
const s = new VectorStore({ dedupSim: 0.9, stateDir: sd, config: cfg() });
|
|
192
|
+
const sid = "shadow";
|
|
193
|
+
seedTwoTopics(s, sid, 8);
|
|
194
|
+
buildTree(s, sid);
|
|
195
|
+
const orig = process.env.RAPTOR_SHADOW_MODE;
|
|
196
|
+
process.env.RAPTOR_SHADOW_MODE = "true";
|
|
197
|
+
try {
|
|
198
|
+
const r = recallAndInline(
|
|
199
|
+
{ sessionId: sid, query: "database pool", source: "resume", limit: 3 },
|
|
200
|
+
s,
|
|
201
|
+
);
|
|
202
|
+
// Shadow mode = build + persist + log only; nothing RAPTOR-derived is
|
|
203
|
+
// injected into recall, including the overview header.
|
|
204
|
+
assert.ok(
|
|
205
|
+
!r.block.includes("hierarchical overview"),
|
|
206
|
+
"overview suppressed in shadow mode",
|
|
207
|
+
);
|
|
208
|
+
} finally {
|
|
209
|
+
if (orig === undefined) delete process.env.RAPTOR_SHADOW_MODE;
|
|
210
|
+
else process.env.RAPTOR_SHADOW_MODE = orig;
|
|
211
|
+
}
|
|
212
|
+
});
|