@desplega.ai/agent-swarm 1.157.0 → 1.158.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/dist/{acp-adapter-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
- package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
- package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
- package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
- package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
- package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
- package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
- package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
- package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
- package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
- package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
- package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
- package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
- package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
- package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
- package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
- package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
- package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
- package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
- package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
- package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
- package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
- package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
- package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
- package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
- package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
- package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
- package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
- package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
- package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
- package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
- package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
- package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
- package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
- package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
- package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
- package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
- package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
- package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
- package/dist/cli-t1ze64h3.js +811 -0
- package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
- package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
- package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
- package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
- package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
- package/dist/cli.js +29 -15
- package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
- package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
- package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
- package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
- package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
- package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
- package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
- package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
- package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
- package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
- package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
- package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
- package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
- package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
- package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
- package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
- package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
- package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
- package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
- package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
- package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
- package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
- package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
- package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
- package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
- package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
- package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
- package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
- package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
- package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
- package/openapi.json +1626 -629
- package/package.json +2 -1
- package/src/apps/catalog.generated.json +229 -0
- package/src/apps/page-validator.ts +71 -16
- package/src/apps/row-store.ts +66 -11
- package/src/apps/sync.ts +59 -17
- package/src/artifact-sdk/browser-sdk.ts +436 -2
- package/src/be/approval-sweeps.ts +109 -0
- package/src/be/boot-seeds.ts +40 -0
- package/src/be/chunking.ts +1 -1
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/approvals.ts +78 -0
- package/src/be/db/tasks/read.ts +197 -39
- package/src/be/db/tasks/write.ts +41 -9
- package/src/be/db-client.ts +107 -48
- package/src/be/db.ts +219 -76
- package/src/be/events.ts +33 -1
- package/src/be/memory/graph-expansion.ts +10 -3
- package/src/be/memory/providers/sqlite-store.ts +55 -13
- package/src/be/memory/raters/retrieval.ts +5 -2
- package/src/be/memory/reranker.ts +26 -2
- package/src/be/memory/types.ts +2 -0
- package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
- package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
- package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
- package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
- package/src/be/models-catalog.ts +2 -0
- package/src/be/modelsdev-cache.json +1857 -0
- package/src/be/modelsdev-cache.ts +14 -1
- package/src/be/pricing-normalize.ts +2 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +3 -1
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/steering.ts +53 -3
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/be/task-citations.ts +29 -0
- package/src/cli.tsx +14 -1
- package/src/commands/onboard/compose-generator.ts +8 -3
- package/src/commands/onboard/container-engine.ts +220 -0
- package/src/commands/onboard/manifest.ts +1 -0
- package/src/commands/onboard/steps/health-check.tsx +4 -1
- package/src/commands/onboard/steps/prereq-check.tsx +50 -54
- package/src/commands/onboard/steps/start.tsx +31 -16
- package/src/commands/onboard/types.ts +7 -2
- package/src/commands/onboard.tsx +39 -4
- package/src/commands/profile-sync.ts +79 -26
- package/src/commands/runner.ts +8 -1
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +263 -103
- package/src/hooks/hook.ts +21 -12
- package/src/http/agents.ts +35 -0
- package/src/http/approval-requests.ts +136 -64
- package/src/http/apps.ts +14 -3
- package/src/http/config.ts +15 -1
- package/src/http/events.ts +62 -2
- package/src/http/index.ts +6 -4
- package/src/http/memory.ts +21 -7
- package/src/http/schedules.ts +1 -1
- package/src/http/session-data.ts +9 -4
- package/src/http/sessions.ts +25 -18
- package/src/http/stats.ts +19 -7
- package/src/http/status.ts +11 -1
- package/src/http/tasks.ts +40 -8
- package/src/http/webhooks.ts +67 -46
- package/src/http/workflows.ts +15 -20
- package/src/linear/outbound.ts +37 -15
- package/src/otel-impl.ts +151 -1
- package/src/otel.ts +14 -3
- package/src/prompts/memories.ts +19 -2
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/providers/modelsdev-reasoning.json +551 -0
- package/src/rbac/legacy-policy.ts +16 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/schedule-task.ts +3 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +12 -7
- package/src/slack/app.ts +35 -4
- package/src/slack/connection-state.ts +17 -0
- package/src/tasks/context-key.ts +1 -0
- package/src/tasks/worker-follow-up.ts +6 -2
- package/src/tests/agent-task-activity.test.ts +74 -0
- package/src/tests/agents-list-model-display.test.ts +6 -1
- package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
- package/src/tests/approval-requests.test.ts +395 -0
- package/src/tests/approval-sweeps-config.test.ts +47 -0
- package/src/tests/approval-sweeps.test.ts +508 -0
- package/src/tests/apps-kibo-blocks.test.ts +77 -0
- package/src/tests/apps-spike.test.ts +56 -1
- package/src/tests/apps-sync-engine.test.ts +20 -9
- package/src/tests/boot-seeds.test.ts +170 -0
- package/src/tests/cancel-approval-request-tool.test.ts +115 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/config-key-filter.test.ts +80 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/db-client-busy-retry.test.ts +96 -0
- package/src/tests/db-client.test.ts +60 -1
- package/src/tests/db-task-read-contract.test.ts +10 -0
- package/src/tests/db-task-write-contract.test.ts +9 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/events-http.test.ts +103 -0
- package/src/tests/extensions-catalog.test.ts +22 -17
- package/src/tests/extensions-pre-task.test.ts +107 -0
- package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
- package/src/tests/follow-up-config-inheritance.test.ts +260 -0
- package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
- package/src/tests/heartbeat-reboot-live.test.ts +95 -0
- package/src/tests/heartbeat-stall-cas.test.ts +80 -0
- package/src/tests/heartbeat.test.ts +99 -3
- package/src/tests/hook-mcp-config.test.ts +29 -0
- package/src/tests/http-api-integration.test.ts +16 -2
- package/src/tests/kv-storage.test.ts +62 -0
- package/src/tests/linear-outbound-sync.test.ts +120 -5
- package/src/tests/list-endpoint-slimming.test.ts +91 -0
- package/src/tests/memory-graph-organic-route.test.ts +155 -0
- package/src/tests/memory-http-recall-gating.test.ts +14 -4
- package/src/tests/memory-hybrid.test.ts +161 -1
- package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
- package/src/tests/memory.test.ts +22 -0
- package/src/tests/onboard-container-engine.test.ts +240 -0
- package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
- package/src/tests/pricing-routes.test.ts +14 -0
- package/src/tests/profile-sync-warning.test.ts +8 -4
- package/src/tests/promote-draft-task-route.test.ts +19 -0
- package/src/tests/provider-registration.test.ts +296 -42
- package/src/tests/rbac-engine.test.ts +30 -0
- package/src/tests/reasoning-effort.test.ts +8 -0
- package/src/tests/script-connections.test.ts +7 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/sessions-detail-batch.test.ts +250 -0
- package/src/tests/sessions.test.ts +118 -0
- package/src/tests/slack-config.test.ts +22 -0
- package/src/tests/sonnet-5-5-registry.test.ts +44 -0
- package/src/tests/status.test.ts +36 -1
- package/src/tests/swarm-diff.test.ts +316 -10
- package/src/tests/task-completion-idempotency.test.ts +2 -1
- package/src/tests/tasks-requested-by-filter.test.ts +3 -1
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/ui-list-slim-projections.test.ts +158 -0
- package/src/tests/webhooks-task-blocked.test.ts +127 -0
- package/src/tests/workflow-approval-recovery.test.ts +203 -0
- package/src/tests/workflow-hitl-routing.test.ts +22 -1
- package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
- package/src/tests/workflow-http-v2.test.ts +99 -8
- package/src/tests/workflow-retry-validation.test.ts +56 -0
- package/src/tests/workflow-tla-races.test.ts +389 -0
- package/src/tests/workflow-validation-port-routing.test.ts +113 -1
- package/src/tools/cancel-approval-request.ts +57 -0
- package/src/tools/memory-search.ts +1 -0
- package/src/tools/request-human-input.ts +4 -2
- package/src/tools/send-task.ts +4 -0
- package/src/tools/templates.ts +19 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/workflows/list-workflow-runs.ts +1 -0
- package/src/types.ts +38 -0
- package/src/utils/context-window.ts +1 -0
- package/src/workflows/approval-cancel.ts +152 -0
- package/src/workflows/approval-notifications.ts +84 -0
- package/src/workflows/checkpoint.ts +10 -5
- package/src/workflows/definition.ts +21 -0
- package/src/workflows/engine.ts +152 -21
- package/src/workflows/executors/human-in-the-loop.ts +1 -1
- package/src/workflows/recovery.ts +100 -45
- package/src/workflows/resume.ts +49 -18
- package/src/workflows/retry-poller.ts +71 -21
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/github-sender-allowlist/README.md +18 -0
- package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
- package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
- package/templates/skills/apps/content.md +20 -1
- package/templates/skills/code-quality/content.md +26 -1
- package/templates/skills/pages/content.md +16 -4
- package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
- package/dist/cli-2daksqps.js +0 -378
- package/dist/rbac-roles-3gxmnb20.js +0 -37
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
|
|
4
|
-
import {
|
|
4
|
+
import { expandCandidatesWithGraph } from "../be/memory/graph-expansion";
|
|
5
|
+
import {
|
|
6
|
+
computeRrfScore,
|
|
7
|
+
fusedSimilarity,
|
|
8
|
+
SqliteMemoryStore,
|
|
9
|
+
} from "../be/memory/providers/sqlite-store";
|
|
10
|
+
import { rerank } from "../be/memory/reranker";
|
|
11
|
+
import { memoryRelevance, renderMemoriesPrompt, SIMILARITY_THRESHOLD } from "../prompts/memories";
|
|
5
12
|
|
|
6
13
|
const TEST_DB_PATH = "./test-memory-hybrid.sqlite";
|
|
7
14
|
const agentId = "aaaa0000-0000-4000-8000-000000000101";
|
|
@@ -213,3 +220,156 @@ describe("memory hybrid search", () => {
|
|
|
213
220
|
expect(freshIndex).toBeLessThan(staleIndex);
|
|
214
221
|
});
|
|
215
222
|
});
|
|
223
|
+
|
|
224
|
+
describe("fusedSimilarity", () => {
|
|
225
|
+
test("keeps the vec arm's [0,1] cosine scale", () => {
|
|
226
|
+
const both = computeRrfScore(0, 1.0) * 2;
|
|
227
|
+
expect(fusedSimilarity(0.8, both)).toBeCloseTo(0.8, 6);
|
|
228
|
+
expect(fusedSimilarity(0.8, computeRrfScore(0, 1.0))).toBeCloseTo(0.6, 6);
|
|
229
|
+
expect(fusedSimilarity(0.8, computeRrfScore(99, 1.0))).toBeGreaterThan(0.4);
|
|
230
|
+
expect(fusedSimilarity(1, both)).toBeLessThanOrEqual(1);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Score-scale regression: every arm must land on one [0,1] scale before
|
|
235
|
+
// rerank, and the injection gate must read that scale, not the boosted
|
|
236
|
+
// composite. Before the fix a hybrid hit scored ~0.033 (raw RRF) while a graph
|
|
237
|
+
// neighbour scored parentCosine × 0.7, so neighbours outranked the hit that
|
|
238
|
+
// surfaced them and an unrelated vec-only query cleared the 0.4 composite gate.
|
|
239
|
+
describe("memory search score scale", () => {
|
|
240
|
+
const scaleAgentId = "aaaa0000-0000-4000-8000-000000000102";
|
|
241
|
+
let store: SqliteMemoryStore;
|
|
242
|
+
let prevHybridFlag: string | undefined;
|
|
243
|
+
let exactId: string;
|
|
244
|
+
let siblingId: string;
|
|
245
|
+
let neighborId: string;
|
|
246
|
+
|
|
247
|
+
/** Unit vector: `cos` on axis 2 (the query axis), the remainder on `axis`. */
|
|
248
|
+
function mix(cos: number, axis: number): Float32Array {
|
|
249
|
+
const embedding = new Float32Array(512);
|
|
250
|
+
embedding[2] = cos;
|
|
251
|
+
embedding[axis] = Math.sqrt(1 - cos * cos);
|
|
252
|
+
return embedding;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const queryAxis = mix(1, 3);
|
|
256
|
+
|
|
257
|
+
async function searchRanked(queryEmbedding: Float32Array, queryText: string) {
|
|
258
|
+
const candidates = await store.search(queryEmbedding, scaleAgentId, {
|
|
259
|
+
scope: "agent",
|
|
260
|
+
limit: 15,
|
|
261
|
+
queryText,
|
|
262
|
+
});
|
|
263
|
+
const expanded = await expandCandidatesWithGraph(candidates, scaleAgentId, { scope: "agent" });
|
|
264
|
+
return rerank(expanded, { limit: 5 });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
beforeAll(async () => {
|
|
268
|
+
prevHybridFlag = process.env.MEMORY_HYBRID_SEARCH;
|
|
269
|
+
process.env.MEMORY_HYBRID_SEARCH = "1";
|
|
270
|
+
await createAgent({
|
|
271
|
+
id: scaleAgentId,
|
|
272
|
+
name: "Scale Test Agent",
|
|
273
|
+
isLead: false,
|
|
274
|
+
status: "idle",
|
|
275
|
+
});
|
|
276
|
+
store = new SqliteMemoryStore();
|
|
277
|
+
|
|
278
|
+
const exact = await store.store({
|
|
279
|
+
agentId: scaleAgentId,
|
|
280
|
+
scope: "agent",
|
|
281
|
+
name: "blockerdigestexactname",
|
|
282
|
+
content: "Daily blocker digest for one specific day.",
|
|
283
|
+
source: "manual",
|
|
284
|
+
});
|
|
285
|
+
const sibling = await store.store({
|
|
286
|
+
agentId: scaleAgentId,
|
|
287
|
+
scope: "agent",
|
|
288
|
+
name: "digest for another day",
|
|
289
|
+
content: "Daily blocker digest for a different day.",
|
|
290
|
+
source: "manual",
|
|
291
|
+
});
|
|
292
|
+
const neighbor = await store.store({
|
|
293
|
+
agentId: scaleAgentId,
|
|
294
|
+
scope: "agent",
|
|
295
|
+
name: "linked follow-up",
|
|
296
|
+
content: "Follow-up linked from the digest.",
|
|
297
|
+
source: "manual",
|
|
298
|
+
});
|
|
299
|
+
exactId = exact.id;
|
|
300
|
+
siblingId = sibling.id;
|
|
301
|
+
neighborId = neighbor.id;
|
|
302
|
+
// The sibling is semantically CLOSER than the exact hit (as dated siblings
|
|
303
|
+
// are in production); only the keyword arm can put the exact name first.
|
|
304
|
+
await store.updateEmbedding(exactId, mix(0.7, 4), "test");
|
|
305
|
+
await store.updateEmbedding(siblingId, mix(0.72, 5), "test");
|
|
306
|
+
await store.updateEmbedding(neighborId, mix(0.2, 6), "test");
|
|
307
|
+
const now = new Date().toISOString();
|
|
308
|
+
await getDbClient().run(
|
|
309
|
+
`INSERT INTO memory_link
|
|
310
|
+
(id, from_memory_id, linkType, targetKind, targetId, strength, resolver, sourceText, metadata, createdAt, updatedAt)
|
|
311
|
+
VALUES (?, ?, 'wikilink', 'memory', ?, 1.0, 'wikilink', 'follow-up', NULL, ?, ?)`,
|
|
312
|
+
[crypto.randomUUID(), exactId, neighborId, now, now],
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
afterAll(() => {
|
|
317
|
+
if (prevHybridFlag === undefined) delete process.env.MEMORY_HYBRID_SEARCH;
|
|
318
|
+
else process.env.MEMORY_HYBRID_SEARCH = prevHybridFlag;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("hybrid arm emits a [0,1] score instead of raw RRF", async () => {
|
|
322
|
+
const candidates = await store.search(queryAxis, scaleAgentId, {
|
|
323
|
+
scope: "agent",
|
|
324
|
+
limit: 10,
|
|
325
|
+
queryText: "blockerdigestexactname",
|
|
326
|
+
});
|
|
327
|
+
const exact = candidates.find((c) => c.id === exactId);
|
|
328
|
+
expect(exact?.retrievalSource).toBe("hybrid");
|
|
329
|
+
expect(exact!.similarity).toBeGreaterThan(0.6);
|
|
330
|
+
expect(exact!.similarity).toBeLessThanOrEqual(0.7);
|
|
331
|
+
expect(exact!.rawSimilarity).toBe(exact!.similarity);
|
|
332
|
+
expect(exact!.recencyDecayApplied).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("exact-name direct hit outranks its graph neighbour and the closer sibling", async () => {
|
|
336
|
+
const ranked = await searchRanked(queryAxis, "blockerdigestexactname");
|
|
337
|
+
const ids = ranked.map((r) => r.id);
|
|
338
|
+
expect(ids[0]).toBe(exactId);
|
|
339
|
+
const neighbor = ranked.find((r) => r.id === neighborId);
|
|
340
|
+
expect(neighbor?.retrievalSource).toBe("graph");
|
|
341
|
+
expect(ids.indexOf(exactId)).toBeLessThan(ids.indexOf(neighborId));
|
|
342
|
+
expect(ids.indexOf(exactId)).toBeLessThan(ids.indexOf(siblingId));
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("a hybrid direct hit clears the injection threshold; its graph neighbour does not", async () => {
|
|
346
|
+
const ranked = await searchRanked(queryAxis, "blockerdigestexactname");
|
|
347
|
+
const exact = ranked.find((r) => r.id === exactId)!;
|
|
348
|
+
const neighbor = ranked.find((r) => r.id === neighborId)!;
|
|
349
|
+
expect(memoryRelevance(exact)).toBeGreaterThan(SIMILARITY_THRESHOLD);
|
|
350
|
+
expect(memoryRelevance(neighbor)).toBeLessThan(SIMILARITY_THRESHOLD);
|
|
351
|
+
const prompt = renderMemoriesPrompt(ranked);
|
|
352
|
+
expect(prompt).toContain(exactId);
|
|
353
|
+
expect(prompt).not.toContain(neighborId);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("a vec-only direct hit (no keyword match) clears the injection threshold", async () => {
|
|
357
|
+
const ranked = await searchRanked(mix(0.99, 5), "paraphrasednokeywordmatch");
|
|
358
|
+
const sibling = ranked.find((r) => r.id === siblingId)!;
|
|
359
|
+
expect(sibling.retrievalSource).toBe("vec");
|
|
360
|
+
expect(memoryRelevance(sibling)).toBeGreaterThan(SIMILARITY_THRESHOLD);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("negative control: a nonexistent-name query injects nothing", async () => {
|
|
364
|
+
// Best cosine ~0.33, the production level for an unrelated query. The old
|
|
365
|
+
// path returned it raw and rerank's 1.5x manual boost lifted it to ~0.5,
|
|
366
|
+
// over the old 0.4 composite gate.
|
|
367
|
+
const unrelated = new Float32Array(512);
|
|
368
|
+
unrelated[2] = 0.46;
|
|
369
|
+
unrelated[7] = Math.sqrt(1 - 0.46 ** 2);
|
|
370
|
+
const ranked = await searchRanked(unrelated, "blockerdigest-2031-01-01-nonexistent");
|
|
371
|
+
expect(ranked.length).toBeGreaterThan(0);
|
|
372
|
+
for (const r of ranked) expect(memoryRelevance(r)).toBeLessThan(SIMILARITY_THRESHOLD);
|
|
373
|
+
expect(renderMemoriesPrompt(ranked)).toBeNull();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
|
|
7
|
+
import * as realMemoryModule from "../be/memory";
|
|
8
|
+
import { expandCandidatesWithGraph } from "../be/memory/graph-expansion";
|
|
9
|
+
import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
|
|
10
|
+
import { rerank } from "../be/memory/reranker";
|
|
11
|
+
import { memoryRelevance, renderMemoriesPrompt, SIMILARITY_THRESHOLD } from "../prompts/memories";
|
|
12
|
+
|
|
13
|
+
// Crowd-out regression (PR #1689 review): graph neighbours get their own
|
|
14
|
+
// access and source boosts, so five boosted neighbours of one direct hit
|
|
15
|
+
// out-scored it on the composite, filled all five slots of the runner's
|
|
16
|
+
// `limit: 5` recall, and all failed the relevance gate. The rendered prompt
|
|
17
|
+
// was null while an eligible direct hit existed. Everything below the query
|
|
18
|
+
// embedding is real: SQLite store, hybrid fusion, graph expansion, reranker,
|
|
19
|
+
// the HTTP handler's selection, and the runner's prompt renderer.
|
|
20
|
+
|
|
21
|
+
// Capture the real exports BEFORE mock.module patches the registry entry.
|
|
22
|
+
const realMemoryExports = {
|
|
23
|
+
getEmbeddingProvider: realMemoryModule.getEmbeddingProvider,
|
|
24
|
+
getMemoryStore: realMemoryModule.getMemoryStore,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const TEST_DB_PATH = "./test-memory-prompt-recall-crowdout.sqlite";
|
|
28
|
+
const agentId = randomUUID();
|
|
29
|
+
const sourceTaskId = randomUUID();
|
|
30
|
+
const PARENT_NAME = "crowdoutparentexactname";
|
|
31
|
+
const NEIGHBOR_COUNT = 5;
|
|
32
|
+
|
|
33
|
+
/** Unit vector: `cos` on axis 2 (the query axis), the remainder on `axis`. */
|
|
34
|
+
function mix(cos: number, axis: number): Float32Array {
|
|
35
|
+
const embedding = new Float32Array(512);
|
|
36
|
+
embedding[2] = cos;
|
|
37
|
+
embedding[axis] = Math.sqrt(1 - cos * cos);
|
|
38
|
+
return embedding;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const queryEmbedding = mix(1, 3);
|
|
42
|
+
|
|
43
|
+
mock.module("../be/memory", () => ({
|
|
44
|
+
getEmbeddingProvider: () => ({
|
|
45
|
+
name: "test",
|
|
46
|
+
dimensions: 512,
|
|
47
|
+
embed: async () => queryEmbedding,
|
|
48
|
+
embedBatch: async (texts: string[]) => texts.map(() => queryEmbedding),
|
|
49
|
+
}),
|
|
50
|
+
getMemoryStore: () => new SqliteMemoryStore(),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const { handleMemory } = await import("../http/memory");
|
|
54
|
+
|
|
55
|
+
let parentId: string;
|
|
56
|
+
const neighborIds: string[] = [];
|
|
57
|
+
let prevHybridFlag: string | undefined;
|
|
58
|
+
|
|
59
|
+
async function promptRecall(
|
|
60
|
+
callerAgentId: string = agentId,
|
|
61
|
+
query: string = PARENT_NAME,
|
|
62
|
+
): Promise<{
|
|
63
|
+
results: Array<{
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
content: string;
|
|
67
|
+
similarity: number;
|
|
68
|
+
rawSimilarity?: number;
|
|
69
|
+
retrievalSource?: string;
|
|
70
|
+
}>;
|
|
71
|
+
}> {
|
|
72
|
+
// Byte-for-byte the runner's pre-task recall request (fetchRelevantMemories).
|
|
73
|
+
const body = { query, limit: 5, intent: "pre-task memory recall" };
|
|
74
|
+
const req = Readable.from([Buffer.from(JSON.stringify(body))]) as IncomingMessage;
|
|
75
|
+
req.method = "POST";
|
|
76
|
+
req.url = "/api/memory/search";
|
|
77
|
+
req.headers = {
|
|
78
|
+
"x-source-task-id": sourceTaskId,
|
|
79
|
+
"x-context-key": "task:crowdout",
|
|
80
|
+
"x-memory-consumption": "prompt",
|
|
81
|
+
};
|
|
82
|
+
let payload: any = null;
|
|
83
|
+
const res = {
|
|
84
|
+
writeHead() {
|
|
85
|
+
return this;
|
|
86
|
+
},
|
|
87
|
+
end(chunk?: unknown) {
|
|
88
|
+
payload = typeof chunk === "string" ? JSON.parse(chunk) : chunk;
|
|
89
|
+
return this;
|
|
90
|
+
},
|
|
91
|
+
} as unknown as ServerResponse;
|
|
92
|
+
const handled = await handleMemory(req, res, ["api", "memory", "search"], callerAgentId);
|
|
93
|
+
expect(handled).toBe(true);
|
|
94
|
+
return payload;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
beforeAll(async () => {
|
|
98
|
+
prevHybridFlag = process.env.MEMORY_HYBRID_SEARCH;
|
|
99
|
+
process.env.MEMORY_HYBRID_SEARCH = "1";
|
|
100
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
101
|
+
try {
|
|
102
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
initDb(TEST_DB_PATH);
|
|
106
|
+
await createAgent({ id: agentId, name: "Crowd-out Agent", isLead: false, status: "idle" });
|
|
107
|
+
const nowIso = new Date().toISOString();
|
|
108
|
+
await getDbClient().run(
|
|
109
|
+
`INSERT INTO agent_tasks (id, agentId, task, status, source, createdAt, lastUpdatedAt)
|
|
110
|
+
VALUES (?, ?, ?, 'in_progress', 'mcp', ?, ?)`,
|
|
111
|
+
[sourceTaskId, agentId, "crowd-out recall task", nowIso, nowIso],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const store = new SqliteMemoryStore();
|
|
115
|
+
// Fresh manual parent, cosine 0.7, matched by both arms, never accessed.
|
|
116
|
+
const parent = await store.store({
|
|
117
|
+
agentId,
|
|
118
|
+
scope: "agent",
|
|
119
|
+
name: PARENT_NAME,
|
|
120
|
+
content: "The direct hit this recall is looking for.",
|
|
121
|
+
source: "manual",
|
|
122
|
+
});
|
|
123
|
+
parentId = parent.id;
|
|
124
|
+
await store.updateEmbedding(parentId, mix(0.7, 4), "test");
|
|
125
|
+
|
|
126
|
+
// Five fresh manual neighbours, each linked at strength 1 and independently
|
|
127
|
+
// boosted by five recent accesses. Their own cosine is low; only the graph
|
|
128
|
+
// arm can surface them.
|
|
129
|
+
for (let i = 0; i < NEIGHBOR_COUNT; i++) {
|
|
130
|
+
const neighbor = await store.store({
|
|
131
|
+
agentId,
|
|
132
|
+
scope: "agent",
|
|
133
|
+
name: `linked note ${i}`,
|
|
134
|
+
content: `Linked follow-up note number ${i}.`,
|
|
135
|
+
source: "manual",
|
|
136
|
+
});
|
|
137
|
+
neighborIds.push(neighbor.id);
|
|
138
|
+
await store.updateEmbedding(neighbor.id, mix(0.1, 10 + i), "test");
|
|
139
|
+
await getDbClient().run(
|
|
140
|
+
`INSERT INTO memory_link
|
|
141
|
+
(id, from_memory_id, linkType, targetKind, targetId, strength, resolver, sourceText, metadata, createdAt, updatedAt)
|
|
142
|
+
VALUES (?, ?, 'wikilink', 'memory', ?, 1.0, 'wikilink', ?, NULL, ?, ?)`,
|
|
143
|
+
[randomUUID(), parentId, neighbor.id, `note ${i}`, nowIso, nowIso],
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
afterAll(async () => {
|
|
149
|
+
// bun's mock.module is process-global and never auto-restored.
|
|
150
|
+
mock.module("../be/memory", () => realMemoryExports);
|
|
151
|
+
if (prevHybridFlag === undefined) delete process.env.MEMORY_HYBRID_SEARCH;
|
|
152
|
+
else process.env.MEMORY_HYBRID_SEARCH = prevHybridFlag;
|
|
153
|
+
closeDb();
|
|
154
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
155
|
+
try {
|
|
156
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("pre-task recall with independently boosted graph neighbours", () => {
|
|
162
|
+
// Set boosts before each test: prompt recall records accesses on the rows it
|
|
163
|
+
// injects, so a previous test would otherwise shift the fixture.
|
|
164
|
+
async function boostFixture(): Promise<void> {
|
|
165
|
+
const nowIso = new Date().toISOString();
|
|
166
|
+
await getDbClient().run(
|
|
167
|
+
"UPDATE agent_memory SET accessCount = 0, accessedAt = ? WHERE id = ?",
|
|
168
|
+
[nowIso, parentId],
|
|
169
|
+
);
|
|
170
|
+
for (const id of neighborIds) {
|
|
171
|
+
await getDbClient().run(
|
|
172
|
+
"UPDATE agent_memory SET accessCount = 5, accessedAt = ? WHERE id = ?",
|
|
173
|
+
[nowIso, id],
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
test("fixture reproduces the review: the neighbours alone out-score the eligible parent", async () => {
|
|
179
|
+
await boostFixture();
|
|
180
|
+
const candidates = await new SqliteMemoryStore().search(queryEmbedding, agentId, {
|
|
181
|
+
scope: "all",
|
|
182
|
+
limit: 15,
|
|
183
|
+
queryText: PARENT_NAME,
|
|
184
|
+
});
|
|
185
|
+
const expanded = await expandCandidatesWithGraph(candidates, agentId, { scope: "all" });
|
|
186
|
+
const scored = expanded.map((c) => ({ c, score: rerank([c], { limit: 1 })[0]! }));
|
|
187
|
+
const parent = scored.find((s) => s.c.id === parentId)!;
|
|
188
|
+
const neighbors = scored.filter((s) => neighborIds.includes(s.c.id));
|
|
189
|
+
|
|
190
|
+
expect(parent.c.retrievalSource).toBe("hybrid");
|
|
191
|
+
expect(memoryRelevance(parent.score)).toBeGreaterThan(SIMILARITY_THRESHOLD);
|
|
192
|
+
expect(neighbors).toHaveLength(NEIGHBOR_COUNT);
|
|
193
|
+
for (const n of neighbors) {
|
|
194
|
+
expect(n.c.retrievalSource).toBe("graph");
|
|
195
|
+
expect(memoryRelevance(n.score)).toBeLessThan(SIMILARITY_THRESHOLD);
|
|
196
|
+
// Scored in isolation, each neighbour's boosted composite beats the parent's.
|
|
197
|
+
expect(n.score.compositeScore!).toBeGreaterThan(parent.score.compositeScore!);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("a parent outranks its own boosted graph neighbours in the reranked top five", async () => {
|
|
202
|
+
await boostFixture();
|
|
203
|
+
const candidates = await new SqliteMemoryStore().search(queryEmbedding, agentId, {
|
|
204
|
+
scope: "all",
|
|
205
|
+
limit: 15,
|
|
206
|
+
queryText: PARENT_NAME,
|
|
207
|
+
});
|
|
208
|
+
const expanded = await expandCandidatesWithGraph(candidates, agentId, { scope: "all" });
|
|
209
|
+
const ranked = rerank(expanded, { limit: 5 });
|
|
210
|
+
expect(ranked[0]!.id).toBe(parentId);
|
|
211
|
+
const parentScore = ranked[0]!.compositeScore!;
|
|
212
|
+
for (const r of ranked.slice(1)) {
|
|
213
|
+
if (neighborIds.includes(r.id)) expect(r.compositeScore!).toBeLessThanOrEqual(parentScore);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("the runner's five-slot prompt recall injects the parent instead of rendering null", async () => {
|
|
218
|
+
await boostFixture();
|
|
219
|
+
const { results } = await promptRecall();
|
|
220
|
+
expect(results.length).toBeGreaterThan(0);
|
|
221
|
+
expect(results.length).toBeLessThanOrEqual(5);
|
|
222
|
+
expect(results[0]!.id).toBe(parentId);
|
|
223
|
+
|
|
224
|
+
const prompt = renderMemoriesPrompt(results);
|
|
225
|
+
expect(prompt).not.toBeNull();
|
|
226
|
+
expect(prompt).toContain(parentId);
|
|
227
|
+
for (const id of neighborIds) expect(prompt).not.toContain(id);
|
|
228
|
+
|
|
229
|
+
const recorded = await getDbClient().query<{ memoryId: string; relevance: number | null }>(
|
|
230
|
+
"SELECT memoryId, relevance FROM memory_retrieval WHERE taskId = ?",
|
|
231
|
+
[sourceTaskId],
|
|
232
|
+
);
|
|
233
|
+
expect(recorded.map((r) => r.memoryId)).toContain(parentId);
|
|
234
|
+
const access = await getDbClient().get<{ accessCount: number }>(
|
|
235
|
+
"SELECT accessCount FROM agent_memory WHERE id = ?",
|
|
236
|
+
[parentId],
|
|
237
|
+
);
|
|
238
|
+
expect(access!.accessCount).toBe(1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("boosted low-relevance direct rows do not take the slots of an eligible hit", async () => {
|
|
242
|
+
// No graph here: an eligible task_completion hit (0.7x source quality)
|
|
243
|
+
// against five manual rows that match only on the vector arm and carry
|
|
244
|
+
// five recent accesses each. The composite puts all five above the hit;
|
|
245
|
+
// the prompt selection must still give the hit a slot.
|
|
246
|
+
const otherAgentId = randomUUID();
|
|
247
|
+
const hitName = "crowdoutsecondexactname";
|
|
248
|
+
await createAgent({
|
|
249
|
+
id: otherAgentId,
|
|
250
|
+
name: "Crowd-out Agent 2",
|
|
251
|
+
isLead: false,
|
|
252
|
+
status: "idle",
|
|
253
|
+
});
|
|
254
|
+
const store = new SqliteMemoryStore();
|
|
255
|
+
const hit = await store.store({
|
|
256
|
+
agentId: otherAgentId,
|
|
257
|
+
scope: "agent",
|
|
258
|
+
name: hitName,
|
|
259
|
+
content: "An eligible hit from a completed task.",
|
|
260
|
+
source: "task_completion",
|
|
261
|
+
});
|
|
262
|
+
await store.updateEmbedding(hit.id, mix(0.75, 4), "test");
|
|
263
|
+
const boostedIds: string[] = [];
|
|
264
|
+
const nowIso = new Date().toISOString();
|
|
265
|
+
for (let i = 0; i < NEIGHBOR_COUNT; i++) {
|
|
266
|
+
const row = await store.store({
|
|
267
|
+
agentId: otherAgentId,
|
|
268
|
+
scope: "agent",
|
|
269
|
+
name: `unrelated manual ${i}`,
|
|
270
|
+
content: `Loosely related manual note ${i}.`,
|
|
271
|
+
source: "manual",
|
|
272
|
+
});
|
|
273
|
+
boostedIds.push(row.id);
|
|
274
|
+
await store.updateEmbedding(row.id, mix(0.5, 20 + i), "test");
|
|
275
|
+
await getDbClient().run(
|
|
276
|
+
"UPDATE agent_memory SET accessCount = 5, accessedAt = ? WHERE id = ?",
|
|
277
|
+
[nowIso, row.id],
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const candidates = await store.search(queryEmbedding, otherAgentId, {
|
|
282
|
+
scope: "all",
|
|
283
|
+
limit: 15,
|
|
284
|
+
queryText: hitName,
|
|
285
|
+
});
|
|
286
|
+
const byComposite = rerank(candidates, { limit: 5 });
|
|
287
|
+
expect(byComposite.map((r) => r.id)).not.toContain(hit.id);
|
|
288
|
+
|
|
289
|
+
const { results } = await promptRecall(otherAgentId, hitName);
|
|
290
|
+
expect(results).toHaveLength(5);
|
|
291
|
+
expect(results[0]!.id).toBe(hit.id);
|
|
292
|
+
expect(memoryRelevance(results[0]!)).toBeGreaterThan(SIMILARITY_THRESHOLD);
|
|
293
|
+
const prompt = renderMemoriesPrompt(results);
|
|
294
|
+
expect(prompt).toContain(hit.id);
|
|
295
|
+
for (const id of boostedIds) expect(prompt).not.toContain(id);
|
|
296
|
+
});
|
|
297
|
+
});
|
package/src/tests/memory.test.ts
CHANGED
|
@@ -192,6 +192,28 @@ describe("Memory System", () => {
|
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
+
test("hard-splits a long unbroken run and terminates", () => {
|
|
196
|
+
// No separator (space, newline, ". ") in 2600 chars, as in base64 or minified JSON.
|
|
197
|
+
const run = Array.from({ length: 2600 }, (_, i) => String.fromCharCode(97 + (i % 26))).join(
|
|
198
|
+
"",
|
|
199
|
+
);
|
|
200
|
+
const chunks = chunkContent(run);
|
|
201
|
+
expect(chunks.length).toBe(2);
|
|
202
|
+
for (const chunk of chunks) {
|
|
203
|
+
expect(run).toContain(chunk.content);
|
|
204
|
+
expect(chunk.content.length).toBeLessThanOrEqual(2000);
|
|
205
|
+
}
|
|
206
|
+
expect(run.startsWith(chunks[0]!.content)).toBe(true);
|
|
207
|
+
expect(run.endsWith(chunks[1]!.content)).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("terminates when an unbroken run follows normal text", () => {
|
|
211
|
+
const chunks = chunkContent(`${"word ".repeat(600)}\n\n${"x".repeat(2500)}`);
|
|
212
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
213
|
+
const xChars = chunks.reduce((n, c) => n + (c.content.match(/x/g)?.length ?? 0), 0);
|
|
214
|
+
expect(xChars).toBeGreaterThanOrEqual(2500);
|
|
215
|
+
});
|
|
216
|
+
|
|
195
217
|
test("chunk indices are sequential", () => {
|
|
196
218
|
const longText = "A ".repeat(3000);
|
|
197
219
|
const chunks = chunkContent(longText);
|