@ai2070/memex 0.9.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/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +35 -0
- package/API.md +1078 -0
- package/LICENSE +190 -0
- package/README.md +574 -0
- package/package.json +30 -0
- package/src/bulk.ts +128 -0
- package/src/envelope.ts +52 -0
- package/src/errors.ts +27 -0
- package/src/graph.ts +15 -0
- package/src/helpers.ts +51 -0
- package/src/index.ts +142 -0
- package/src/integrity.ts +378 -0
- package/src/intent.ts +311 -0
- package/src/query.ts +357 -0
- package/src/reducer.ts +177 -0
- package/src/replay.ts +32 -0
- package/src/retrieval.ts +306 -0
- package/src/serialization.ts +34 -0
- package/src/stats.ts +62 -0
- package/src/task.ts +373 -0
- package/src/transplant.ts +488 -0
- package/src/types.ts +248 -0
- package/tests/bugfix-and-coverage.test.ts +958 -0
- package/tests/bugfix-holes.test.ts +856 -0
- package/tests/bulk.test.ts +256 -0
- package/tests/edge-cases-v2.test.ts +355 -0
- package/tests/edge-cases.test.ts +661 -0
- package/tests/envelope.test.ts +92 -0
- package/tests/graph.test.ts +41 -0
- package/tests/helpers.test.ts +120 -0
- package/tests/integrity.test.ts +371 -0
- package/tests/intent.test.ts +276 -0
- package/tests/query-advanced.test.ts +252 -0
- package/tests/query.test.ts +623 -0
- package/tests/reducer.test.ts +342 -0
- package/tests/replay.test.ts +145 -0
- package/tests/retrieval.test.ts +691 -0
- package/tests/serialization.test.ts +118 -0
- package/tests/setup.test.ts +7 -0
- package/tests/stats.test.ts +163 -0
- package/tests/task.test.ts +322 -0
- package/tests/transplant.test.ts +385 -0
- package/tests/types.test.ts +231 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getItems,
|
|
4
|
+
getEdges,
|
|
5
|
+
getItemById,
|
|
6
|
+
getEdgeById,
|
|
7
|
+
getRelatedItems,
|
|
8
|
+
getParents,
|
|
9
|
+
getChildren,
|
|
10
|
+
getScoredItems,
|
|
11
|
+
} from "../src/query.js";
|
|
12
|
+
import type { MemoryItem, Edge, GraphState } from "../src/types.js";
|
|
13
|
+
|
|
14
|
+
// -- test fixtures --
|
|
15
|
+
|
|
16
|
+
const items: MemoryItem[] = [
|
|
17
|
+
{
|
|
18
|
+
id: "m1",
|
|
19
|
+
scope: "user:laz/general",
|
|
20
|
+
kind: "assertion",
|
|
21
|
+
content: { key: "theme", value: "dark" },
|
|
22
|
+
author: "user:laz",
|
|
23
|
+
source_kind: "user_explicit",
|
|
24
|
+
authority: 0.95,
|
|
25
|
+
importance: 0.8,
|
|
26
|
+
conviction: 0.9,
|
|
27
|
+
meta: { agent_id: "agent:x", tags: { primary: "preference", env: "prod" } },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "m2",
|
|
31
|
+
scope: "user:laz/general",
|
|
32
|
+
kind: "observation",
|
|
33
|
+
content: { key: "login_count", value: 42 },
|
|
34
|
+
author: "agent:reasoner",
|
|
35
|
+
source_kind: "observed",
|
|
36
|
+
authority: 0.7,
|
|
37
|
+
importance: 0.5,
|
|
38
|
+
conviction: 0.6,
|
|
39
|
+
meta: { agent_id: "agent:y", tags: { primary: "metric", env: "prod" } },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "m3",
|
|
43
|
+
scope: "project:cyberdeck",
|
|
44
|
+
kind: "derivation",
|
|
45
|
+
content: { key: "active_user", value: true },
|
|
46
|
+
author: "system:rule",
|
|
47
|
+
source_kind: "derived_deterministic",
|
|
48
|
+
parents: ["m1", "m2"],
|
|
49
|
+
authority: 0.6,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "m4",
|
|
53
|
+
scope: "user:laz/general",
|
|
54
|
+
kind: "hypothesis",
|
|
55
|
+
content: { key: "will_churn", value: false },
|
|
56
|
+
author: "agent:reasoner",
|
|
57
|
+
source_kind: "agent_inferred",
|
|
58
|
+
parents: ["m2"],
|
|
59
|
+
authority: 0.3,
|
|
60
|
+
importance: 0.9,
|
|
61
|
+
conviction: 0.4,
|
|
62
|
+
meta: {
|
|
63
|
+
agent_id: "agent:x",
|
|
64
|
+
tags: { primary: "prediction", env: "staging" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "m5",
|
|
69
|
+
scope: "project:cyberdeck",
|
|
70
|
+
kind: "simulation",
|
|
71
|
+
content: { key: "scenario", value: "outage" },
|
|
72
|
+
author: "agent:reasoner",
|
|
73
|
+
source_kind: "simulated",
|
|
74
|
+
authority: 0.2,
|
|
75
|
+
importance: 0.4,
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const edges: Edge[] = [
|
|
80
|
+
{
|
|
81
|
+
edge_id: "e1",
|
|
82
|
+
from: "m1",
|
|
83
|
+
to: "m2",
|
|
84
|
+
kind: "SUPPORTS",
|
|
85
|
+
author: "system:rule",
|
|
86
|
+
source_kind: "derived_deterministic",
|
|
87
|
+
authority: 0.8,
|
|
88
|
+
active: true,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
edge_id: "e2",
|
|
92
|
+
from: "m3",
|
|
93
|
+
to: "m4",
|
|
94
|
+
kind: "DERIVED_FROM",
|
|
95
|
+
author: "system:rule",
|
|
96
|
+
source_kind: "derived_deterministic",
|
|
97
|
+
authority: 0.9,
|
|
98
|
+
active: true,
|
|
99
|
+
weight: 0.7,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
edge_id: "e3",
|
|
103
|
+
from: "m2",
|
|
104
|
+
to: "m3",
|
|
105
|
+
kind: "ABOUT",
|
|
106
|
+
author: "agent:reasoner",
|
|
107
|
+
source_kind: "agent_inferred",
|
|
108
|
+
authority: 0.5,
|
|
109
|
+
active: false,
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
function buildState(): GraphState {
|
|
114
|
+
const state: GraphState = { items: new Map(), edges: new Map() };
|
|
115
|
+
for (const i of items) state.items.set(i.id, i);
|
|
116
|
+
for (const e of edges) state.edges.set(e.edge_id, e);
|
|
117
|
+
return state;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// -- getItems basic filters --
|
|
121
|
+
|
|
122
|
+
describe("getItems", () => {
|
|
123
|
+
const state = buildState();
|
|
124
|
+
|
|
125
|
+
it("returns all items with no filter", () => {
|
|
126
|
+
expect(getItems(state)).toHaveLength(5);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("filters by scope", () => {
|
|
130
|
+
const result = getItems(state, { scope: "project:cyberdeck" });
|
|
131
|
+
expect(result).toHaveLength(2);
|
|
132
|
+
expect(result.every((i) => i.scope === "project:cyberdeck")).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("filters by kind", () => {
|
|
136
|
+
const result = getItems(state, { kind: "observation" });
|
|
137
|
+
expect(result).toHaveLength(1);
|
|
138
|
+
expect(result[0].id).toBe("m2");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("filters by source_kind", () => {
|
|
142
|
+
const result = getItems(state, { source_kind: "observed" });
|
|
143
|
+
expect(result).toHaveLength(1);
|
|
144
|
+
expect(result[0].id).toBe("m2");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("filters by author", () => {
|
|
148
|
+
const result = getItems(state, { author: "agent:reasoner" });
|
|
149
|
+
expect(result).toHaveLength(3);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("excludes items matching not filter", () => {
|
|
153
|
+
const result = getItems(state, {
|
|
154
|
+
not: { or: [{ kind: "hypothesis" }, { kind: "simulation" }] },
|
|
155
|
+
});
|
|
156
|
+
expect(result).toHaveLength(3);
|
|
157
|
+
expect(
|
|
158
|
+
result.every((i) => i.kind !== "hypothesis" && i.kind !== "simulation"),
|
|
159
|
+
).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("excludes by single kind with not", () => {
|
|
163
|
+
const result = getItems(state, { not: { kind: "simulation" } });
|
|
164
|
+
expect(result).toHaveLength(4);
|
|
165
|
+
expect(result.every((i) => i.kind !== "simulation")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("excludes by non-kind fields with not", () => {
|
|
169
|
+
const result = getItems(state, { not: { author: "agent:reasoner" } });
|
|
170
|
+
expect(result).toHaveLength(2);
|
|
171
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m3"]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("combines not with other filters", () => {
|
|
175
|
+
const result = getItems(state, {
|
|
176
|
+
scope: "user:laz/general",
|
|
177
|
+
not: { range: { authority: { max: 0.5 } } },
|
|
178
|
+
});
|
|
179
|
+
expect(result).toHaveLength(2);
|
|
180
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m2"]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("filters by meta", () => {
|
|
184
|
+
const result = getItems(state, { meta: { agent_id: "agent:x" } });
|
|
185
|
+
expect(result).toHaveLength(2);
|
|
186
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m4"]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("combines filters with AND logic", () => {
|
|
190
|
+
const result = getItems(state, {
|
|
191
|
+
scope: "user:laz/general",
|
|
192
|
+
range: { authority: { min: 0.5 } },
|
|
193
|
+
});
|
|
194
|
+
expect(result).toHaveLength(2);
|
|
195
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m2"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// -- range filters --
|
|
199
|
+
|
|
200
|
+
it("filters by authority min", () => {
|
|
201
|
+
const result = getItems(state, { range: { authority: { min: 0.6 } } });
|
|
202
|
+
expect(result).toHaveLength(3);
|
|
203
|
+
expect(result.every((i) => i.authority >= 0.6)).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("filters by authority max", () => {
|
|
207
|
+
const result = getItems(state, { range: { authority: { max: 0.3 } } });
|
|
208
|
+
expect(result).toHaveLength(2);
|
|
209
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m4", "m5"]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("filters by authority range (min + max)", () => {
|
|
213
|
+
const result = getItems(state, {
|
|
214
|
+
range: { authority: { min: 0.3, max: 0.7 } },
|
|
215
|
+
});
|
|
216
|
+
expect(result).toHaveLength(3);
|
|
217
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m2", "m3", "m4"]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("filters by importance min (excludes undefined)", () => {
|
|
221
|
+
const result = getItems(state, { range: { importance: { min: 0.7 } } });
|
|
222
|
+
expect(result).toHaveLength(2);
|
|
223
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m4"]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("filters by conviction range", () => {
|
|
227
|
+
const result = getItems(state, {
|
|
228
|
+
range: { conviction: { min: 0.5, max: 0.8 } },
|
|
229
|
+
});
|
|
230
|
+
expect(result).toHaveLength(1);
|
|
231
|
+
expect(result[0].id).toBe("m2");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("filters by multiple ranges simultaneously", () => {
|
|
235
|
+
const result = getItems(state, {
|
|
236
|
+
range: { authority: { min: 0.5 }, importance: { min: 0.5 } },
|
|
237
|
+
});
|
|
238
|
+
expect(result).toHaveLength(2);
|
|
239
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m2"]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// -- nested meta (dot-path) --
|
|
243
|
+
|
|
244
|
+
it("filters by nested meta with dot-path", () => {
|
|
245
|
+
const result = getItems(state, { meta: { "tags.primary": "preference" } });
|
|
246
|
+
expect(result).toHaveLength(1);
|
|
247
|
+
expect(result[0].id).toBe("m1");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("filters by multiple nested meta paths", () => {
|
|
251
|
+
const result = getItems(state, {
|
|
252
|
+
meta: { "tags.env": "prod", agent_id: "agent:x" },
|
|
253
|
+
});
|
|
254
|
+
expect(result).toHaveLength(1);
|
|
255
|
+
expect(result[0].id).toBe("m1");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("returns empty when nested meta path does not exist", () => {
|
|
259
|
+
const result = getItems(state, { meta: { "tags.nonexistent": "foo" } });
|
|
260
|
+
expect(result).toHaveLength(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("handles items with no meta gracefully for nested paths", () => {
|
|
264
|
+
const result = getItems(state, { meta: { "tags.primary": "metric" } });
|
|
265
|
+
expect(result).toHaveLength(1);
|
|
266
|
+
expect(result[0].id).toBe("m2");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// -- meta_has (existence) --
|
|
270
|
+
|
|
271
|
+
it("filters by meta_has — field must exist", () => {
|
|
272
|
+
const result = getItems(state, { meta_has: ["tags"] });
|
|
273
|
+
expect(result).toHaveLength(3); // m1, m2, m4 have tags
|
|
274
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m2", "m4"]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("filters by meta_has with dot-path", () => {
|
|
278
|
+
const result = getItems(state, { meta_has: ["tags.primary"] });
|
|
279
|
+
expect(result).toHaveLength(3);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("meta_has excludes items without meta", () => {
|
|
283
|
+
const result = getItems(state, { meta_has: ["agent_id"] });
|
|
284
|
+
expect(result).toHaveLength(3); // m3 and m5 have no meta
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("meta_has with multiple paths requires all", () => {
|
|
288
|
+
const result = getItems(state, { meta_has: ["agent_id", "tags.env"] });
|
|
289
|
+
expect(result).toHaveLength(3);
|
|
290
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m2", "m4"]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("meta_has returns empty when path does not exist on any item", () => {
|
|
294
|
+
const result = getItems(state, { meta_has: ["nonexistent.deep.path"] });
|
|
295
|
+
expect(result).toHaveLength(0);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("combines meta_has with not for 'field exists but not this value'", () => {
|
|
299
|
+
const result = getItems(state, {
|
|
300
|
+
meta_has: ["agent_id"],
|
|
301
|
+
not: { meta: { agent_id: "agent:x" } },
|
|
302
|
+
});
|
|
303
|
+
expect(result).toHaveLength(1);
|
|
304
|
+
expect(result[0].id).toBe("m2");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// -- OR queries --
|
|
308
|
+
|
|
309
|
+
it("matches any sub-filter in or array", () => {
|
|
310
|
+
const result = getItems(state, {
|
|
311
|
+
or: [{ kind: "observation" }, { kind: "assertion" }],
|
|
312
|
+
});
|
|
313
|
+
expect(result).toHaveLength(2);
|
|
314
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m2"]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("AND-combines top-level fields with or", () => {
|
|
318
|
+
const result = getItems(state, {
|
|
319
|
+
scope: "user:laz/general",
|
|
320
|
+
or: [{ kind: "observation" }, { kind: "hypothesis" }],
|
|
321
|
+
});
|
|
322
|
+
expect(result).toHaveLength(2);
|
|
323
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m2", "m4"]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("or with no matches returns empty", () => {
|
|
327
|
+
const result = getItems(state, {
|
|
328
|
+
or: [{ kind: "policy" }, { kind: "trait" }],
|
|
329
|
+
});
|
|
330
|
+
expect(result).toHaveLength(0);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("nested or filters work recursively", () => {
|
|
334
|
+
const result = getItems(state, {
|
|
335
|
+
or: [
|
|
336
|
+
{ kind: "simulation" },
|
|
337
|
+
{ kind: "derivation", scope: "project:cyberdeck" },
|
|
338
|
+
],
|
|
339
|
+
});
|
|
340
|
+
expect(result).toHaveLength(2);
|
|
341
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m3", "m5"]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("or combined with meta dot-path", () => {
|
|
345
|
+
const result = getItems(state, {
|
|
346
|
+
or: [
|
|
347
|
+
{ meta: { "tags.primary": "preference" } },
|
|
348
|
+
{ meta: { "tags.primary": "prediction" } },
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
expect(result).toHaveLength(2);
|
|
352
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m4"]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("empty or array matches all (no or constraint)", () => {
|
|
356
|
+
const result = getItems(state, { or: [] });
|
|
357
|
+
expect(result).toHaveLength(5);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// -- sorting & pagination --
|
|
362
|
+
|
|
363
|
+
describe("getItems with QueryOptions", () => {
|
|
364
|
+
const state = buildState();
|
|
365
|
+
|
|
366
|
+
it("sorts by authority ascending", () => {
|
|
367
|
+
const result = getItems(
|
|
368
|
+
state,
|
|
369
|
+
{},
|
|
370
|
+
{ sort: { field: "authority", order: "asc" } },
|
|
371
|
+
);
|
|
372
|
+
const authorities = result.map((i) => i.authority);
|
|
373
|
+
expect(authorities).toEqual([...authorities].sort((a, b) => a - b));
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("sorts by authority descending", () => {
|
|
377
|
+
const result = getItems(
|
|
378
|
+
state,
|
|
379
|
+
{},
|
|
380
|
+
{ sort: { field: "authority", order: "desc" } },
|
|
381
|
+
);
|
|
382
|
+
const authorities = result.map((i) => i.authority);
|
|
383
|
+
expect(authorities).toEqual([...authorities].sort((a, b) => b - a));
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("sorts by importance descending (undefined treated as 0)", () => {
|
|
387
|
+
const result = getItems(
|
|
388
|
+
state,
|
|
389
|
+
{},
|
|
390
|
+
{ sort: { field: "importance", order: "desc" } },
|
|
391
|
+
);
|
|
392
|
+
expect(result[0].id).toBe("m4"); // importance 0.9
|
|
393
|
+
expect(result[1].id).toBe("m1"); // importance 0.8
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("sorts by conviction ascending", () => {
|
|
397
|
+
const result = getItems(
|
|
398
|
+
state,
|
|
399
|
+
{},
|
|
400
|
+
{ sort: { field: "conviction", order: "asc" } },
|
|
401
|
+
);
|
|
402
|
+
const convictions = result.map((i) => i.conviction ?? 0);
|
|
403
|
+
expect(convictions).toEqual([...convictions].sort((a, b) => a - b));
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("limits results", () => {
|
|
407
|
+
const result = getItems(
|
|
408
|
+
state,
|
|
409
|
+
{},
|
|
410
|
+
{ sort: { field: "authority", order: "desc" }, limit: 2 },
|
|
411
|
+
);
|
|
412
|
+
expect(result).toHaveLength(2);
|
|
413
|
+
expect(result[0].authority).toBe(0.95);
|
|
414
|
+
expect(result[1].authority).toBe(0.7);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("offsets results", () => {
|
|
418
|
+
const result = getItems(
|
|
419
|
+
state,
|
|
420
|
+
{},
|
|
421
|
+
{ sort: { field: "authority", order: "desc" }, offset: 3 },
|
|
422
|
+
);
|
|
423
|
+
expect(result).toHaveLength(2);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("combines offset and limit", () => {
|
|
427
|
+
const result = getItems(
|
|
428
|
+
state,
|
|
429
|
+
{},
|
|
430
|
+
{ sort: { field: "authority", order: "desc" }, offset: 1, limit: 2 },
|
|
431
|
+
);
|
|
432
|
+
expect(result).toHaveLength(2);
|
|
433
|
+
expect(result[0].authority).toBe(0.7);
|
|
434
|
+
expect(result[1].authority).toBe(0.6);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("combines filter with sort and limit", () => {
|
|
438
|
+
const result = getItems(
|
|
439
|
+
state,
|
|
440
|
+
{ scope: "user:laz/general" },
|
|
441
|
+
{ sort: { field: "authority", order: "desc" }, limit: 2 },
|
|
442
|
+
);
|
|
443
|
+
expect(result).toHaveLength(2);
|
|
444
|
+
expect(result[0].id).toBe("m1");
|
|
445
|
+
expect(result[1].id).toBe("m2");
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// -- getEdges --
|
|
450
|
+
|
|
451
|
+
describe("getEdges", () => {
|
|
452
|
+
const state = buildState();
|
|
453
|
+
|
|
454
|
+
it("returns only active edges by default", () => {
|
|
455
|
+
const result = getEdges(state);
|
|
456
|
+
expect(result).toHaveLength(2);
|
|
457
|
+
expect(result.every((e) => e.active)).toBe(true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("returns all edges with active_only: false", () => {
|
|
461
|
+
const result = getEdges(state, { active_only: false });
|
|
462
|
+
expect(result).toHaveLength(3);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("filters by from", () => {
|
|
466
|
+
const result = getEdges(state, { from: "m1" });
|
|
467
|
+
expect(result).toHaveLength(1);
|
|
468
|
+
expect(result[0].edge_id).toBe("e1");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("filters by kind", () => {
|
|
472
|
+
const result = getEdges(state, { kind: "DERIVED_FROM" });
|
|
473
|
+
expect(result).toHaveLength(1);
|
|
474
|
+
expect(result[0].edge_id).toBe("e2");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("filters by min_weight", () => {
|
|
478
|
+
const result = getEdges(state, { min_weight: 0.5 });
|
|
479
|
+
expect(result).toHaveLength(1);
|
|
480
|
+
expect(result[0].edge_id).toBe("e2");
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// -- getItemById / getEdgeById --
|
|
485
|
+
|
|
486
|
+
describe("getItemById", () => {
|
|
487
|
+
const state = buildState();
|
|
488
|
+
|
|
489
|
+
it("returns the item", () => {
|
|
490
|
+
expect(getItemById(state, "m1")?.id).toBe("m1");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("returns undefined for non-existent id", () => {
|
|
494
|
+
expect(getItemById(state, "nope")).toBeUndefined();
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
describe("getEdgeById", () => {
|
|
499
|
+
const state = buildState();
|
|
500
|
+
|
|
501
|
+
it("returns the edge", () => {
|
|
502
|
+
expect(getEdgeById(state, "e1")?.edge_id).toBe("e1");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("returns undefined for non-existent id", () => {
|
|
506
|
+
expect(getEdgeById(state, "nope")).toBeUndefined();
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// -- getRelatedItems --
|
|
511
|
+
|
|
512
|
+
describe("getRelatedItems", () => {
|
|
513
|
+
const state = buildState();
|
|
514
|
+
|
|
515
|
+
it("returns items connected in both directions", () => {
|
|
516
|
+
const result = getRelatedItems(state, "m2");
|
|
517
|
+
expect(result).toHaveLength(1);
|
|
518
|
+
expect(result[0].id).toBe("m1");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("returns items in 'from' direction only", () => {
|
|
522
|
+
const result = getRelatedItems(state, "m1", "from");
|
|
523
|
+
expect(result).toHaveLength(1);
|
|
524
|
+
expect(result[0].id).toBe("m2");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("returns items in 'to' direction only", () => {
|
|
528
|
+
const result = getRelatedItems(state, "m2", "to");
|
|
529
|
+
expect(result).toHaveLength(1);
|
|
530
|
+
expect(result[0].id).toBe("m1");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("returns empty for unconnected item", () => {
|
|
534
|
+
expect(getRelatedItems(state, "m5")).toHaveLength(0);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// -- parents & children --
|
|
539
|
+
|
|
540
|
+
describe("parents and children", () => {
|
|
541
|
+
const state = buildState();
|
|
542
|
+
|
|
543
|
+
it("getParents returns parent items", () => {
|
|
544
|
+
const result = getParents(state, "m3");
|
|
545
|
+
expect(result).toHaveLength(2);
|
|
546
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m2"]);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("getParents returns single parent", () => {
|
|
550
|
+
const result = getParents(state, "m4");
|
|
551
|
+
expect(result).toHaveLength(1);
|
|
552
|
+
expect(result[0].id).toBe("m2");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("getParents returns empty for root items", () => {
|
|
556
|
+
expect(getParents(state, "m1")).toHaveLength(0);
|
|
557
|
+
expect(getParents(state, "m2")).toHaveLength(0);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("getParents returns empty for non-existent item", () => {
|
|
561
|
+
expect(getParents(state, "nope")).toHaveLength(0);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("getChildren returns items derived from a parent", () => {
|
|
565
|
+
const result = getChildren(state, "m2");
|
|
566
|
+
expect(result).toHaveLength(2);
|
|
567
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m3", "m4"]);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("getChildren returns single child", () => {
|
|
571
|
+
const result = getChildren(state, "m1");
|
|
572
|
+
expect(result).toHaveLength(1);
|
|
573
|
+
expect(result[0].id).toBe("m3");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("getChildren returns empty for leaf items", () => {
|
|
577
|
+
expect(getChildren(state, "m5")).toHaveLength(0);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// -- filter by parents --
|
|
582
|
+
|
|
583
|
+
describe("getItems parent filters", () => {
|
|
584
|
+
const state = buildState();
|
|
585
|
+
|
|
586
|
+
it("has_parent filters items with a specific parent", () => {
|
|
587
|
+
const result = getItems(state, { has_parent: "m2" });
|
|
588
|
+
expect(result).toHaveLength(2);
|
|
589
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m3", "m4"]);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("has_parent returns empty when no items have that parent", () => {
|
|
593
|
+
const result = getItems(state, { has_parent: "m5" });
|
|
594
|
+
expect(result).toHaveLength(0);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("is_root: true returns items without parents", () => {
|
|
598
|
+
const result = getItems(state, { is_root: true });
|
|
599
|
+
expect(result).toHaveLength(3);
|
|
600
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m1", "m2", "m5"]);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("is_root: false returns items with parents", () => {
|
|
604
|
+
const result = getItems(state, { is_root: false });
|
|
605
|
+
expect(result).toHaveLength(2);
|
|
606
|
+
expect(result.map((i) => i.id).sort()).toEqual(["m3", "m4"]);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("combines has_parent with other filters", () => {
|
|
610
|
+
const result = getItems(state, { has_parent: "m2", kind: "hypothesis" });
|
|
611
|
+
expect(result).toHaveLength(1);
|
|
612
|
+
expect(result[0].id).toBe("m4");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("not with has_parent excludes items derived from a specific parent", () => {
|
|
616
|
+
const result = getItems(state, {
|
|
617
|
+
is_root: false,
|
|
618
|
+
not: { has_parent: "m1" },
|
|
619
|
+
});
|
|
620
|
+
expect(result).toHaveLength(1);
|
|
621
|
+
expect(result[0].id).toBe("m4");
|
|
622
|
+
});
|
|
623
|
+
});
|