@rekal/mem 0.0.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/db-BMh1OP4b.mjs +294 -0
- package/dist/doc-DnYN4jAU.mjs +116 -0
- package/dist/embed-rUMZxqed.mjs +100 -0
- package/dist/fs-DMp26Byo.mjs +32 -0
- package/dist/glob.d.mts +27 -0
- package/dist/glob.mjs +132 -0
- package/dist/index.d.mts +1465 -0
- package/dist/index.mjs +351 -0
- package/dist/llama-CT3dc9Cn.mjs +75 -0
- package/dist/models-DFQSgBNr.mjs +77 -0
- package/dist/openai-j2_2GM4J.mjs +76 -0
- package/dist/progress-B1JdNapX.mjs +263 -0
- package/dist/query-VFSpErTB.mjs +125 -0
- package/dist/runtime.node-DlQPaGrV.mjs +35 -0
- package/dist/search-BllHWtZF.mjs +166 -0
- package/dist/store-DE7S35SS.mjs +137 -0
- package/dist/transformers-CJ3QA2PK.mjs +55 -0
- package/dist/uri-CehXVDGB.mjs +28 -0
- package/dist/util-DNyrmcA3.mjs +11 -0
- package/dist/vfs-CNQbkhsf.mjs +222 -0
- package/foo.ts +3 -0
- package/foo2.ts +20 -0
- package/package.json +61 -0
- package/src/context.ts +77 -0
- package/src/db.ts +464 -0
- package/src/doc.ts +163 -0
- package/src/embed/base.ts +122 -0
- package/src/embed/index.ts +67 -0
- package/src/embed/llama.ts +111 -0
- package/src/embed/models.ts +104 -0
- package/src/embed/openai.ts +95 -0
- package/src/embed/transformers.ts +81 -0
- package/src/frecency.ts +58 -0
- package/src/fs.ts +36 -0
- package/src/glob.ts +163 -0
- package/src/index.ts +15 -0
- package/src/log.ts +60 -0
- package/src/md.ts +204 -0
- package/src/progress.ts +121 -0
- package/src/query.ts +131 -0
- package/src/runtime.bun.ts +33 -0
- package/src/runtime.node.ts +47 -0
- package/src/search.ts +230 -0
- package/src/snippet.ts +248 -0
- package/src/sqlite.ts +1 -0
- package/src/store.ts +180 -0
- package/src/uri.ts +28 -0
- package/src/util.ts +21 -0
- package/src/vfs.ts +257 -0
- package/test/doc.test.ts +61 -0
- package/test/fixtures/ignore-test/keep.md +0 -0
- package/test/fixtures/ignore-test/skip.log +0 -0
- package/test/fixtures/ignore-test/sub/keep.md +0 -0
- package/test/fixtures/store/agent/index.md +9 -0
- package/test/fixtures/store/agent/lessons.md +21 -0
- package/test/fixtures/store/agent/soul.md +28 -0
- package/test/fixtures/store/agent/tools.md +25 -0
- package/test/fixtures/store/concepts/frecency.md +30 -0
- package/test/fixtures/store/concepts/index.md +9 -0
- package/test/fixtures/store/concepts/memory-coherence.md +33 -0
- package/test/fixtures/store/concepts/rag.md +27 -0
- package/test/fixtures/store/index.md +9 -0
- package/test/fixtures/store/projects/index.md +9 -0
- package/test/fixtures/store/projects/rekall-inc/architecture.md +41 -0
- package/test/fixtures/store/projects/rekall-inc/decisions/index.md +9 -0
- package/test/fixtures/store/projects/rekall-inc/decisions/no-military.md +20 -0
- package/test/fixtures/store/projects/rekall-inc/index.md +28 -0
- package/test/fixtures/store/user/family.md +13 -0
- package/test/fixtures/store/user/index.md +9 -0
- package/test/fixtures/store/user/preferences.md +29 -0
- package/test/fixtures/store/user/profile.md +29 -0
- package/test/fs.test.ts +15 -0
- package/test/glob.test.ts +190 -0
- package/test/md.test.ts +177 -0
- package/test/query.test.ts +105 -0
- package/test/uri.test.ts +46 -0
- package/test/util.test.ts +62 -0
- package/test/vfs.test.ts +164 -0
- package/tsconfig.json +3 -0
- package/tsdown.config.ts +8 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// oxlint-disable sort-keys
|
|
2
|
+
import { describe, expect, test } from "vitest"
|
|
3
|
+
import { toFts, tokenize } from "../src/query.ts"
|
|
4
|
+
|
|
5
|
+
describe("tokenize", () => {
|
|
6
|
+
const cases: Record<string, ReturnType<typeof tokenize>> = {
|
|
7
|
+
"children family": [
|
|
8
|
+
{ type: "term", value: "children" },
|
|
9
|
+
{ type: "term", value: "family" },
|
|
10
|
+
],
|
|
11
|
+
'"my children"': [{ type: "term", value: "my children" }],
|
|
12
|
+
"'my children'": [{ type: "term", value: "my children" }],
|
|
13
|
+
"-sports": [{ type: "term", value: "sports", neg: true }],
|
|
14
|
+
"children | kids": [
|
|
15
|
+
{ type: "term", value: "children" },
|
|
16
|
+
{ type: "op", value: "OR" },
|
|
17
|
+
{ type: "term", value: "kids" },
|
|
18
|
+
],
|
|
19
|
+
"(children | kids) family": [
|
|
20
|
+
{ type: "paren", value: "(" },
|
|
21
|
+
{ type: "term", value: "children" },
|
|
22
|
+
{ type: "op", value: "OR" },
|
|
23
|
+
{ type: "term", value: "kids" },
|
|
24
|
+
{ type: "paren", value: ")" },
|
|
25
|
+
{ type: "term", value: "family" },
|
|
26
|
+
],
|
|
27
|
+
'"my children': [{ type: "term", value: "my children" }],
|
|
28
|
+
'""': [],
|
|
29
|
+
"what's up": [
|
|
30
|
+
{ type: "term", value: "what's" },
|
|
31
|
+
{ type: "term", value: "up" },
|
|
32
|
+
],
|
|
33
|
+
"child*": [{ type: "term", value: "child*" }],
|
|
34
|
+
"entities:Lars": [{ type: "term", value: "Lars", field: "entities" }],
|
|
35
|
+
"tags:family children": [
|
|
36
|
+
{ type: "term", value: "family", field: "tags" },
|
|
37
|
+
{ type: "term", value: "children" },
|
|
38
|
+
],
|
|
39
|
+
"-tags:sports": [{ type: "term", value: "sports", field: "tags", neg: true }],
|
|
40
|
+
"+children": [{ type: "term", value: "children", req: true }],
|
|
41
|
+
"+tags:family": [{ type: "term", value: "family", field: "tags", req: true }],
|
|
42
|
+
"unknown:foo": [{ type: "term", value: "unknown:foo" }],
|
|
43
|
+
'children -sports "my family" | (kids age)': [
|
|
44
|
+
{ type: "term", value: "children" },
|
|
45
|
+
{ type: "term", value: "sports", neg: true },
|
|
46
|
+
{ type: "term", value: "my family" },
|
|
47
|
+
{ type: "op", value: "OR" },
|
|
48
|
+
{ type: "paren", value: "(" },
|
|
49
|
+
{ type: "term", value: "kids" },
|
|
50
|
+
{ type: "term", value: "age" },
|
|
51
|
+
{ type: "paren", value: ")" },
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const [input, expected] of Object.entries(cases)) {
|
|
56
|
+
test(input, () => expect(tokenize(input)).toEqual(expected))
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("toFts", () => {
|
|
61
|
+
const or: Record<string, string> = {
|
|
62
|
+
"children family": '"children" OR "family"',
|
|
63
|
+
"children | kids": '"children" OR "kids"',
|
|
64
|
+
"children -sports": '"children" OR NOT "sports"',
|
|
65
|
+
'"my children"': '"my children"',
|
|
66
|
+
"'my children'": '"my children"',
|
|
67
|
+
"(children | kids) family": '("children" OR "kids") OR "family"',
|
|
68
|
+
"what's children?": '"what\'s" OR "children"',
|
|
69
|
+
"": "",
|
|
70
|
+
kids: '"kids"',
|
|
71
|
+
"child*": '"child"*',
|
|
72
|
+
"child* family": '"child"* OR "family"',
|
|
73
|
+
"child* famil*": '"child"* OR "famil"*',
|
|
74
|
+
"-sport*": 'NOT "sport"*',
|
|
75
|
+
"entities:Lars": 'entities : "Lars"',
|
|
76
|
+
"tags:family children": 'tags : "family" OR "children"',
|
|
77
|
+
"-tags:sports": 'NOT tags : "sports"',
|
|
78
|
+
"entities:Lars tags:family": 'entities : "Lars" OR tags : "family"',
|
|
79
|
+
"entities:Lars* tags:fam*": 'entities : "Lars"* OR tags : "fam"*',
|
|
80
|
+
"unknown:foo": '"unknown:foo"',
|
|
81
|
+
"folke +children": '"children" AND ("folke" OR "children")',
|
|
82
|
+
"+children folke": '"children" AND ("children" OR "folke")',
|
|
83
|
+
"folke +children +family": '"children" AND "family" AND ("folke" OR "children" OR "family")',
|
|
84
|
+
"+tags:family children": 'tags : "family" AND (tags : "family" OR "children")',
|
|
85
|
+
"+children": '"children"',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const and: Record<string, string> = {
|
|
89
|
+
"children family": '"children" AND "family"',
|
|
90
|
+
"children | kids": '"children" OR "kids"',
|
|
91
|
+
"children -sports": '"children" AND NOT "sports"',
|
|
92
|
+
"(children | kids) family": '("children" OR "kids") AND "family"',
|
|
93
|
+
"(children kids) family": '("children" AND "kids") AND "family"',
|
|
94
|
+
"child* family": '"child"* AND "family"',
|
|
95
|
+
"what's my name?": '"what\'s" AND "my" AND "name"',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const [input, expected] of Object.entries(or)) {
|
|
99
|
+
test(`OR: ${input}`, () => expect(toFts(input)).toBe(expected))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const [input, expected] of Object.entries(and)) {
|
|
103
|
+
test(`AND: ${input}`, () => expect(toFts(input, "AND")).toBe(expected))
|
|
104
|
+
}
|
|
105
|
+
})
|
package/test/uri.test.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// oxlint-disable sort-keys
|
|
2
|
+
import { describe, expect, test } from "vitest"
|
|
3
|
+
import { normUri, parentUri } from "../src/uri.ts"
|
|
4
|
+
|
|
5
|
+
describe("normUri", () => {
|
|
6
|
+
const cases: { input?: string; dir?: boolean; expected: string }[] = [
|
|
7
|
+
{ expected: "rekal://" },
|
|
8
|
+
{ input: "", expected: "rekal://" },
|
|
9
|
+
{ input: " ", expected: "rekal://" },
|
|
10
|
+
{ input: "foo", expected: "rekal://foo" },
|
|
11
|
+
{ input: "foo/bar", expected: "rekal://foo/bar" },
|
|
12
|
+
{ input: "foo/bar/index.md", expected: "rekal://foo/bar/" },
|
|
13
|
+
{ input: "rekal://notes", expected: "rekal://notes" },
|
|
14
|
+
{ input: "rekal://notes/", expected: "rekal://notes/" },
|
|
15
|
+
{ input: "rekall://test", expected: "rekal://test" },
|
|
16
|
+
{ input: "a//b///c", expected: "rekal://a/b/c" },
|
|
17
|
+
{ input: "///a/b", expected: "rekal://a/b" },
|
|
18
|
+
{ input: "a\\b\\c", expected: "rekal://a/b/c" },
|
|
19
|
+
{ input: "foo", dir: true, expected: "rekal://foo/" },
|
|
20
|
+
{ input: "foo/bar", dir: true, expected: "rekal://foo/bar/" },
|
|
21
|
+
{ input: "foo/bar/", dir: true, expected: "rekal://foo/bar/" },
|
|
22
|
+
{ input: "foo", dir: false, expected: "rekal://foo" },
|
|
23
|
+
{ input: "foo/bar/", dir: false, expected: "rekal://foo/bar" },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
for (const { input, dir, expected } of cases) {
|
|
27
|
+
test(`normUri(${JSON.stringify(input)}, ${dir}) => ${expected}`, () =>
|
|
28
|
+
expect(normUri(input, dir)).toBe(expected))
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("parentUri", () => {
|
|
33
|
+
const cases: { input: string; expected: string | undefined }[] = [
|
|
34
|
+
{ input: "rekal://", expected: undefined },
|
|
35
|
+
{ input: "rekal://foo", expected: "rekal://" },
|
|
36
|
+
{ input: "rekal://foo/", expected: "rekal://" },
|
|
37
|
+
{ input: "rekal://foo\\bar", expected: "rekal://foo/" },
|
|
38
|
+
{ input: "rekal://foo/bar/", expected: "rekal://foo/" },
|
|
39
|
+
{ input: "rekal://foo/bar/baz.md", expected: "rekal://foo/bar/" },
|
|
40
|
+
{ input: "rekal://a/b/c/d", expected: "rekal://a/b/c/" },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
for (const { input, expected } of cases) {
|
|
44
|
+
test(`parentUri(${input}) => ${expected}`, () => expect(parentUri(input)).toBe(expected))
|
|
45
|
+
}
|
|
46
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest"
|
|
2
|
+
import { parseFrontmatter } from "../src/md.ts"
|
|
3
|
+
|
|
4
|
+
describe("parseFrontmatter", () => {
|
|
5
|
+
test("parses yaml frontmatter", () => {
|
|
6
|
+
const { frontmatter, body } = parseFrontmatter(`---
|
|
7
|
+
summary: "hello world"
|
|
8
|
+
tags: [a, b]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Title
|
|
12
|
+
|
|
13
|
+
Body text.
|
|
14
|
+
`)
|
|
15
|
+
expect(frontmatter).toEqual({ summary: "hello world", tags: ["a", "b"] })
|
|
16
|
+
expect(body).toContain("# Title")
|
|
17
|
+
expect(body).toContain("Body text.")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("returns empty data when no frontmatter", () => {
|
|
21
|
+
const { frontmatter, body } = parseFrontmatter("Just plain text.")
|
|
22
|
+
expect(frontmatter).toEqual({})
|
|
23
|
+
expect(body).toBe("Just plain text.")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test("handles missing closing delimiter gracefully", () => {
|
|
27
|
+
const { frontmatter, body } = parseFrontmatter(`---
|
|
28
|
+
summary: "oops"
|
|
29
|
+
No closing delimiter here.
|
|
30
|
+
`)
|
|
31
|
+
// Should not crash — returns empty frontmatter, full content as body
|
|
32
|
+
expect(frontmatter).toEqual({})
|
|
33
|
+
expect(body).toContain("summary")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("handles empty frontmatter", () => {
|
|
37
|
+
const { frontmatter, body } = parseFrontmatter(`---
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
Content after empty frontmatter.
|
|
41
|
+
`)
|
|
42
|
+
expect(frontmatter).toEqual({})
|
|
43
|
+
expect(body).toContain("Content after empty frontmatter.")
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("handles complex yaml values", () => {
|
|
47
|
+
const { frontmatter } = parseFrontmatter(`---
|
|
48
|
+
entities: [Folke, "Lars Lemaitre", snacks.nvim]
|
|
49
|
+
tags:
|
|
50
|
+
- user
|
|
51
|
+
- family
|
|
52
|
+
nested:
|
|
53
|
+
key: value
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
Body.
|
|
57
|
+
`)
|
|
58
|
+
expect(frontmatter.entities).toEqual(["Folke", "Lars Lemaitre", "snacks.nvim"])
|
|
59
|
+
expect(frontmatter.tags).toEqual(["user", "family"])
|
|
60
|
+
expect(frontmatter.nested).toEqual({ key: "value" })
|
|
61
|
+
})
|
|
62
|
+
})
|
package/test/vfs.test.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { VfsEntry } from "../src/vfs.ts"
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import { join } from "pathe"
|
|
5
|
+
import { describe, expect, test } from "vitest"
|
|
6
|
+
import { Context } from "../src/context.ts"
|
|
7
|
+
import { Vfs } from "../src/vfs.ts"
|
|
8
|
+
|
|
9
|
+
const FIXTURES = join(fileURLToPath(import.meta.url), "..", "fixtures/store")
|
|
10
|
+
|
|
11
|
+
function createVfs() {
|
|
12
|
+
const ctx = new Context({})
|
|
13
|
+
return new Vfs(ctx)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function collect(vfs: Vfs, uri: string, depth?: number) {
|
|
17
|
+
const results: VfsEntry[] = []
|
|
18
|
+
for await (const item of vfs.find({ depth, uri })) {
|
|
19
|
+
results.push(item)
|
|
20
|
+
}
|
|
21
|
+
return results
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("Vfs addFolder", () => {
|
|
25
|
+
test("adds a folder to the VFS", () => {
|
|
26
|
+
const vfs = createVfs()
|
|
27
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://notes" })
|
|
28
|
+
expect(vfs.folders).toHaveLength(1)
|
|
29
|
+
expect(vfs.folders[0].uri).toBe("rekal://notes/")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("normalizes trailing slash on URI", () => {
|
|
33
|
+
const vfs = createVfs()
|
|
34
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://notes" })
|
|
35
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://docs/" })
|
|
36
|
+
expect(vfs.folders[0].uri).toBe("rekal://notes/")
|
|
37
|
+
expect(vfs.folders[1].uri).toBe("rekal://docs/")
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe("Vfs find", () => {
|
|
42
|
+
test("finds files in a single mount", async () => {
|
|
43
|
+
const vfs = createVfs()
|
|
44
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://store" })
|
|
45
|
+
const results = await collect(vfs, "rekal://store")
|
|
46
|
+
expect(results.length).toBeGreaterThan(0)
|
|
47
|
+
|
|
48
|
+
const uris = results.map((r) => r.uri)
|
|
49
|
+
expect(uris.some((u) => u.includes("family.md"))).toBe(true)
|
|
50
|
+
expect(uris.some((u) => u.includes("user/"))).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("depth=1 shows only immediate children", async () => {
|
|
54
|
+
const vfs = createVfs()
|
|
55
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://store" })
|
|
56
|
+
const results = await collect(vfs, "rekal://store", 1)
|
|
57
|
+
const uris = results.map((r) => r.uri)
|
|
58
|
+
|
|
59
|
+
// Should see top-level dirs (index.md is filtered as a dir node)
|
|
60
|
+
expect(uris.some((u) => u === "rekal://store/user/")).toBe(true)
|
|
61
|
+
expect(uris.some((u) => u === "rekal://store/projects/")).toBe(true)
|
|
62
|
+
|
|
63
|
+
// Should NOT see nested files
|
|
64
|
+
expect(uris.some((u) => u.includes("family.md"))).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test("nested mount appears as virtual directory", async () => {
|
|
68
|
+
const vfs = createVfs()
|
|
69
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://notes" })
|
|
70
|
+
vfs.addFolder({ path: join(FIXTURES, "concepts"), uri: "rekal://notes/extra" })
|
|
71
|
+
const results = await collect(vfs, "rekal://notes", 1)
|
|
72
|
+
const uris = results.map((r) => r.uri)
|
|
73
|
+
|
|
74
|
+
// The nested mount should appear as a directory
|
|
75
|
+
expect(uris).toContain("rekal://notes/extra/")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("nested mount has correct path", async () => {
|
|
79
|
+
const vfs = createVfs()
|
|
80
|
+
const toolsDir = join(FIXTURES, "concepts")
|
|
81
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://notes" })
|
|
82
|
+
vfs.addFolder({ path: toolsDir, uri: "rekal://notes/extra" })
|
|
83
|
+
const results = await collect(vfs, "rekal://notes", 1)
|
|
84
|
+
|
|
85
|
+
const extra = results.find((r) => r.uri === "rekal://notes/extra/")
|
|
86
|
+
expect(extra).toBeDefined()
|
|
87
|
+
// Should have a path from the explicit mount
|
|
88
|
+
expect(extra!.path.startsWith(toolsDir)).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("nested mount contents are found at depth=2", async () => {
|
|
92
|
+
const vfs = createVfs()
|
|
93
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://notes" })
|
|
94
|
+
vfs.addFolder({ path: join(FIXTURES, "concepts"), uri: "rekal://notes/extra" })
|
|
95
|
+
const results = await collect(vfs, "rekal://notes", 2)
|
|
96
|
+
const uris = results.map((r) => r.uri)
|
|
97
|
+
|
|
98
|
+
// Should find files inside the nested mount
|
|
99
|
+
expect(uris.some((u) => u.includes("extra/") && u.endsWith(".md"))).toBe(true)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("find below a mount resolves correctly", async () => {
|
|
103
|
+
const vfs = createVfs()
|
|
104
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://store" })
|
|
105
|
+
const results = await collect(vfs, "rekal://store/user")
|
|
106
|
+
const uris = results.map((r) => r.uri)
|
|
107
|
+
|
|
108
|
+
expect(uris.some((u) => u.includes("family.md"))).toBe(true)
|
|
109
|
+
expect(uris.some((u) => u.includes("preferences.md"))).toBe(true)
|
|
110
|
+
// Should NOT include files from other top-level dirs
|
|
111
|
+
expect(uris.some((u) => u.includes("concepts/"))).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("multiple paths for same URI prefix", async () => {
|
|
115
|
+
const vfs = createVfs()
|
|
116
|
+
vfs.addFolder({ path: join(FIXTURES, "user"), uri: "rekal://merged" })
|
|
117
|
+
vfs.addFolder({ path: join(FIXTURES, "concepts"), uri: "rekal://merged" })
|
|
118
|
+
const results = await collect(vfs, "rekal://merged")
|
|
119
|
+
const uris = results.map((r) => r.uri)
|
|
120
|
+
|
|
121
|
+
// Should find files from both directories
|
|
122
|
+
expect(uris.some((u) => u.includes("family.md"))).toBe(true) // from user
|
|
123
|
+
expect(uris.some((u) => u.includes("frecency.md"))).toBe(true) // from concepts
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("deduplicates URIs from overlapping mounts", async () => {
|
|
127
|
+
const vfs = createVfs()
|
|
128
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://notes" })
|
|
129
|
+
// Mount the user subdir again explicitly
|
|
130
|
+
vfs.addFolder({ path: join(FIXTURES, "user"), uri: "rekal://notes/user" })
|
|
131
|
+
const results = await collect(vfs, "rekal://notes")
|
|
132
|
+
const uris = results.map((r) => r.uri)
|
|
133
|
+
|
|
134
|
+
// family.md should appear only once
|
|
135
|
+
const familyResults = uris.filter((u) => u === "rekal://notes/user/family.md")
|
|
136
|
+
expect(familyResults).toHaveLength(1)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test("overlapping mount yields results from both sources", async () => {
|
|
140
|
+
const vfs = createVfs()
|
|
141
|
+
const userDir = join(FIXTURES, "user")
|
|
142
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://notes" })
|
|
143
|
+
vfs.addFolder({ path: userDir, uri: "rekal://notes/user" })
|
|
144
|
+
const results = await collect(vfs, "rekal://notes")
|
|
145
|
+
const uris = results.map((r) => r.uri)
|
|
146
|
+
|
|
147
|
+
// Should find the user/ directory and files from both mounts
|
|
148
|
+
expect(uris.some((u) => u === "rekal://notes/user/")).toBe(true)
|
|
149
|
+
expect(uris.some((u) => u.includes("family.md"))).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe("Vfs ls", () => {
|
|
154
|
+
test("ls is equivalent to find with depth=1", async () => {
|
|
155
|
+
const vfs = createVfs()
|
|
156
|
+
vfs.addFolder({ path: FIXTURES, uri: "rekal://store" })
|
|
157
|
+
const lsResults = await collect(vfs, "rekal://store", 1)
|
|
158
|
+
const findResults: VfsEntry[] = []
|
|
159
|
+
for await (const item of vfs.ls({ uri: "rekal://store" })) {
|
|
160
|
+
findResults.push(item)
|
|
161
|
+
}
|
|
162
|
+
expect(lsResults.map((r) => r.uri).toSorted()).toEqual(findResults.map((r) => r.uri).toSorted())
|
|
163
|
+
})
|
|
164
|
+
})
|
package/tsconfig.json
ADDED