@rekal/mem 0.0.0 → 0.0.2

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.
Files changed (79) hide show
  1. package/dist/{db-BMh1OP4b.mjs → db-CHpq7OOi.mjs} +46 -15
  2. package/dist/db-CHpq7OOi.mjs.map +1 -0
  3. package/dist/doc-DnYN4jAU.mjs +2 -0
  4. package/dist/doc-DnYN4jAU.mjs.map +1 -0
  5. package/dist/{embed-rUMZxqed.mjs → embed-CZI5Dz1q.mjs} +3 -1
  6. package/dist/embed-CZI5Dz1q.mjs.map +1 -0
  7. package/dist/frecency-CiaqPIOy.mjs +30 -0
  8. package/dist/frecency-CiaqPIOy.mjs.map +1 -0
  9. package/dist/fs-DMp26Byo.mjs +2 -0
  10. package/dist/fs-DMp26Byo.mjs.map +1 -0
  11. package/dist/glob.d.mts +2 -1
  12. package/dist/glob.d.mts.map +1 -0
  13. package/dist/glob.mjs +2 -0
  14. package/dist/glob.mjs.map +1 -0
  15. package/dist/index.d.mts +21 -11
  16. package/dist/index.d.mts.map +1 -0
  17. package/dist/index.mjs +7 -5
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/{llama-CT3dc9Cn.mjs → llama-CpNV7Lh9.mjs} +3 -1
  20. package/dist/llama-CpNV7Lh9.mjs.map +1 -0
  21. package/dist/{models-DFQSgBNr.mjs → models-Bo6czhQe.mjs} +5 -3
  22. package/dist/models-Bo6czhQe.mjs.map +1 -0
  23. package/dist/{openai-j2_2GM4J.mjs → openai-ALl6_YhI.mjs} +3 -1
  24. package/dist/openai-ALl6_YhI.mjs.map +1 -0
  25. package/dist/progress-B1JdNapX.mjs +2 -0
  26. package/dist/progress-B1JdNapX.mjs.map +1 -0
  27. package/dist/query-VFSpErTB.mjs +2 -0
  28. package/dist/query-VFSpErTB.mjs.map +1 -0
  29. package/dist/runtime.node-DlQPaGrV.mjs +2 -0
  30. package/dist/runtime.node-DlQPaGrV.mjs.map +1 -0
  31. package/dist/{search-BllHWtZF.mjs → search-DsVjB-9f.mjs} +2 -0
  32. package/dist/search-DsVjB-9f.mjs.map +1 -0
  33. package/dist/{store-DE7S35SS.mjs → store-I5nVEYxK.mjs} +10 -6
  34. package/dist/store-I5nVEYxK.mjs.map +1 -0
  35. package/dist/{transformers-CJ3QA2PK.mjs → transformers-Df56Nq9G.mjs} +3 -1
  36. package/dist/transformers-Df56Nq9G.mjs.map +1 -0
  37. package/dist/uri-CehXVDGB.mjs +2 -0
  38. package/dist/uri-CehXVDGB.mjs.map +1 -0
  39. package/dist/util-DNyrmcA3.mjs +2 -0
  40. package/dist/util-DNyrmcA3.mjs.map +1 -0
  41. package/dist/{vfs-CNQbkhsf.mjs → vfs-QUP1rnSI.mjs} +2 -0
  42. package/dist/vfs-QUP1rnSI.mjs.map +1 -0
  43. package/package.json +25 -25
  44. package/src/db.ts +73 -23
  45. package/src/frecency.ts +29 -46
  46. package/src/store.ts +13 -7
  47. package/foo.ts +0 -3
  48. package/foo2.ts +0 -20
  49. package/test/doc.test.ts +0 -61
  50. package/test/fixtures/ignore-test/keep.md +0 -0
  51. package/test/fixtures/ignore-test/skip.log +0 -0
  52. package/test/fixtures/ignore-test/sub/keep.md +0 -0
  53. package/test/fixtures/store/agent/index.md +0 -9
  54. package/test/fixtures/store/agent/lessons.md +0 -21
  55. package/test/fixtures/store/agent/soul.md +0 -28
  56. package/test/fixtures/store/agent/tools.md +0 -25
  57. package/test/fixtures/store/concepts/frecency.md +0 -30
  58. package/test/fixtures/store/concepts/index.md +0 -9
  59. package/test/fixtures/store/concepts/memory-coherence.md +0 -33
  60. package/test/fixtures/store/concepts/rag.md +0 -27
  61. package/test/fixtures/store/index.md +0 -9
  62. package/test/fixtures/store/projects/index.md +0 -9
  63. package/test/fixtures/store/projects/rekall-inc/architecture.md +0 -41
  64. package/test/fixtures/store/projects/rekall-inc/decisions/index.md +0 -9
  65. package/test/fixtures/store/projects/rekall-inc/decisions/no-military.md +0 -20
  66. package/test/fixtures/store/projects/rekall-inc/index.md +0 -28
  67. package/test/fixtures/store/user/family.md +0 -13
  68. package/test/fixtures/store/user/index.md +0 -9
  69. package/test/fixtures/store/user/preferences.md +0 -29
  70. package/test/fixtures/store/user/profile.md +0 -29
  71. package/test/fs.test.ts +0 -15
  72. package/test/glob.test.ts +0 -190
  73. package/test/md.test.ts +0 -177
  74. package/test/query.test.ts +0 -105
  75. package/test/uri.test.ts +0 -46
  76. package/test/util.test.ts +0 -62
  77. package/test/vfs.test.ts +0 -164
  78. package/tsconfig.json +0 -3
  79. package/tsdown.config.ts +0 -8
package/test/glob.test.ts DELETED
@@ -1,190 +0,0 @@
1
- import { fileURLToPath } from "node:url"
2
- import { join } from "pathe"
3
- import { describe, expect, test } from "vitest"
4
- import { glob } from "../src/glob.ts"
5
-
6
- const FIXTURES = join(fileURLToPath(import.meta.url), "..", "fixtures/store")
7
-
8
- async function collect(opts: Partial<Parameters<typeof glob>[0]> = {}) {
9
- const results: string[] = []
10
- for await (const path of glob({ cwd: FIXTURES, ...opts })) {
11
- results.push(path)
12
- }
13
- return results
14
- }
15
-
16
- describe("glob", () => {
17
- test("yields files and directories", async () => {
18
- const results = await collect()
19
- expect(results.length).toBeGreaterThan(0)
20
- expect(results.some((r) => r.endsWith(".md"))).toBe(true)
21
- expect(results.some((r) => r.endsWith("/"))).toBe(true)
22
- })
23
-
24
- test("finds known fixture files", async () => {
25
- const results = await collect()
26
- expect(results).toContain("index.md")
27
- expect(results).toContain("user/")
28
- expect(results).toContain("user/family.md")
29
- expect(results).toContain("user/index.md")
30
- expect(results).toContain("projects/rekall-inc/decisions/no-military.md")
31
- })
32
-
33
- test("results are sorted by name", async () => {
34
- const results = await collect()
35
- const topLevelFiles = results.filter((r) => !r.includes("/") && r.endsWith(".md"))
36
- const sorted = [...topLevelFiles].toSorted()
37
- expect(topLevelFiles).toEqual(sorted)
38
- })
39
-
40
- test("respects depth", async () => {
41
- const results = await collect({ depth: 2 })
42
- expect(results).toContain("index.md")
43
- expect(results).toContain("user/")
44
- expect(results.some((r) => r.startsWith("user/") && r.endsWith(".md"))).toBe(true)
45
- expect(results.some((r) => r.includes("decisions/"))).toBe(false)
46
- })
47
-
48
- test("depth 1 only shows root contents", async () => {
49
- const results = await collect({ depth: 1 })
50
- expect(results).toContain("index.md")
51
- expect(results).toContain("user/")
52
- expect(results.every((r) => !r.includes("/") || r.endsWith("/"))).toBe(true)
53
- })
54
-
55
- test("skips hidden files and dirs by default", async () => {
56
- const results = await collect()
57
- expect(results.every((r) => !r.startsWith("."))).toBe(true)
58
- expect(results.every((r) => !r.includes("/."))).toBe(true)
59
- expect(results.some((r) => r.includes(".rekal"))).toBe(false)
60
- })
61
-
62
- test("includes hidden files when hidden is true", async () => {
63
- const results = await collect({ depth: 1, hidden: true })
64
- expect(results.length).toBeGreaterThan(0)
65
- })
66
-
67
- test("type=files excludes directories from output", async () => {
68
- const results = await collect({ type: "file" })
69
- expect(results.every((r) => !r.endsWith("/"))).toBe(true)
70
- expect(results.some((r) => r.includes("/"))).toBe(true)
71
- })
72
- })
73
-
74
- describe("glob patterns", () => {
75
- test("filters to matching files only", async () => {
76
- const results = await collect({ glob: "**/*.md" })
77
- const files = results.filter((r) => !r.endsWith("/"))
78
- expect(files.length).toBeGreaterThan(0)
79
- expect(files.every((r) => r.endsWith(".md"))).toBe(true)
80
- })
81
-
82
- test("excludes non-matching files", async () => {
83
- const results = await collect({ glob: "**/*.md" })
84
- expect(results.some((r) => r.endsWith(".yaml"))).toBe(false)
85
- })
86
-
87
- test("still includes directories for traversal", async () => {
88
- const results = await collect({ glob: "**/*.md" })
89
- expect(results.some((r) => r.endsWith("/"))).toBe(true)
90
- })
91
-
92
- test("supports multiple glob patterns", async () => {
93
- const results = await collect({ glob: ["**/family.md", "**/profile.md"] })
94
- expect(results.some((r) => r.endsWith("family.md"))).toBe(true)
95
- expect(results.some((r) => r.endsWith("profile.md"))).toBe(true)
96
- })
97
-
98
- test("narrow glob reduces results", async () => {
99
- const allMd = await collect({ glob: "**/*.md" })
100
- const userOnly = await collect({ glob: "user/*.md" })
101
- expect(userOnly.length).toBeLessThan(allMd.length)
102
- const files = userOnly.filter((r) => !r.endsWith("/"))
103
- expect(files.every((r) => r.startsWith("user/"))).toBe(true)
104
- })
105
- })
106
-
107
- describe("glob empty directories", () => {
108
- test("excludes empty directories by default", async () => {
109
- const results = await collect()
110
- expect(results.some((r) => r === "empty-dir/")).toBe(false)
111
- })
112
-
113
- test("includes empty directories when empty is true", async () => {
114
- const results = await collect({ empty: true })
115
- expect(results.some((r) => r === "empty-dir/")).toBe(true)
116
- })
117
- })
118
-
119
- describe("glob onVisit", () => {
120
- test("calls onVisit for entries", async () => {
121
- const visited: string[] = []
122
- await collect({ onVisit: (rel) => visited.push(rel) })
123
- expect(visited.length).toBeGreaterThan(0)
124
- })
125
- })
126
-
127
- describe("glob onError", () => {
128
- test("calls onError for inaccessible directories", async () => {
129
- const errors: { path: string; error: Error }[] = []
130
- await collect({
131
- cwd: "/nonexistent-path-that-does-not-exist",
132
- onError: (path, error) => errors.push({ error, path }),
133
- })
134
- expect(errors.length).toBeGreaterThan(0)
135
- })
136
- })
137
-
138
- describe("glob sort", () => {
139
- test("type sort puts directories before files", async () => {
140
- const results = await collect({ depth: 1, sort: "type" })
141
- const firstFile = results.findIndex((r) => !r.endsWith("/"))
142
- const lastDir = results.findLastIndex((r) => r.endsWith("/"))
143
- if (firstFile !== -1 && lastDir !== -1) {
144
- expect(lastDir).toBeLessThan(firstFile)
145
- }
146
- })
147
-
148
- test("name sort is alphabetical", async () => {
149
- const results = await collect({ depth: 1, sort: "name" })
150
- const sorted = [...results].toSorted()
151
- expect(results).toEqual(sorted)
152
- })
153
- })
154
-
155
- describe("glob ignore", () => {
156
- test("respects exclude rules", async () => {
157
- const results = await collect({ exclude: ["user/"] })
158
- expect(results.some((r) => r.startsWith("user/"))).toBe(false)
159
- expect(results.some((r) => r === "user/")).toBe(false)
160
- expect(results.some((r) => r.startsWith("projects/"))).toBe(true)
161
- })
162
-
163
- test("ignore=false skips all ignore file processing", async () => {
164
- const withIgnore = await collect()
165
- const withoutIgnore = await collect({ hidden: true, ignore: false })
166
- expect(withoutIgnore.length).toBeGreaterThanOrEqual(withIgnore.length)
167
- })
168
- })
169
-
170
- describe("glob nested ignore files", () => {
171
- const IGNORE_FIXTURES = join(fileURLToPath(import.meta.url), "..", "fixtures/ignore-test")
172
-
173
- async function collectIgnore(opts: Partial<Parameters<typeof glob>[0]> = {}) {
174
- const results: string[] = []
175
- for await (const path of glob({ cwd: IGNORE_FIXTURES, ...opts })) {
176
- results.push(path)
177
- }
178
- return results
179
- }
180
-
181
- test("nested gitignore with / prefix only applies relative to its directory", async () => {
182
- // sub/.gitignore contains "/skip.log"
183
- // This should ignore sub/skip.log but NOT skip.log at the root
184
- const results = await collectIgnore({ hidden: false })
185
- expect(results).toContain("skip.log") // root skip.log should NOT be ignored
186
- expect(results).not.toContain("sub/skip.log") // sub/skip.log SHOULD be ignored
187
- expect(results).toContain("keep.md")
188
- expect(results).toContain("sub/keep.md")
189
- })
190
- })
package/test/md.test.ts DELETED
@@ -1,177 +0,0 @@
1
- import { describe, expect, test } from "vitest"
2
- import { chunkMarkdown, parseSections } from "../src/md.ts"
3
-
4
- describe("parse", () => {
5
- test("parses sections by heading", () => {
6
- const sections = parseSections(`# Title
7
-
8
- Intro paragraph.
9
-
10
- ## Section One
11
-
12
- Content one.
13
-
14
- ## Section Two
15
-
16
- Content two.
17
- `)
18
- expect(sections).toHaveLength(3)
19
- expect(sections[0].headingText).toBe("# Title")
20
- expect(sections[0].level).toBe(1)
21
- expect(sections[1].headingText).toBe("## Section One")
22
- expect(sections[1].level).toBe(2)
23
- expect(sections[2].headingText).toBe("## Section Two")
24
- expect(sections[2].level).toBe(2)
25
- })
26
-
27
- test("handles content before first heading", () => {
28
- const sections = parseSections(`Some preamble text.
29
-
30
- # First Heading
31
-
32
- Content.
33
- `)
34
- expect(sections).toHaveLength(2)
35
- expect(sections[0].headingText).toBe("")
36
- expect(sections[0].level).toBe(0)
37
- expect(sections[0].content.join("\n")).toContain("preamble")
38
- })
39
-
40
- test("ignores headings inside code blocks", () => {
41
- const sections = parseSections(`# Real Heading
42
-
43
- \`\`\`markdown
44
- # Not a heading
45
- ## Also not a heading
46
- \`\`\`
47
-
48
- More content.
49
- `)
50
- expect(sections).toHaveLength(1)
51
- expect(sections[0].headingText).toBe("# Real Heading")
52
- expect(sections[0].content.join("\n")).toContain("# Not a heading")
53
- })
54
-
55
- test("handles tilde code fences", () => {
56
- const sections = parseSections(`# Title
57
-
58
- ~~~python
59
- # comment not a heading
60
- ~~~
61
-
62
- ## Next Section
63
-
64
- Content.
65
- `)
66
- expect(sections).toHaveLength(2)
67
- expect(sections[0].headingText).toBe("# Title")
68
- expect(sections[1].headingText).toBe("## Next Section")
69
- })
70
-
71
- test("discards empty sections", () => {
72
- // A section with a heading but no content lines (immediately followed by another heading)
73
- const sections = parseSections(`# First
74
- ## Second
75
-
76
- Content here.
77
- `)
78
- // "First" has "## Second" as its only content line before being replaced,
79
- // but parse() keeps sections that have the heading line itself as content
80
- expect(sections).toHaveLength(2)
81
- expect(sections[0].headingText).toBe("# First")
82
- expect(sections[1].headingText).toBe("## Second")
83
- })
84
-
85
- test("handles nested heading levels", () => {
86
- const sections = parseSections(`# H1
87
-
88
- ## H2
89
-
90
- ### H3
91
-
92
- Content.
93
-
94
- ## Another H2
95
-
96
- More content.
97
- `)
98
- expect(sections.map((s) => s.headingText)).toEqual(["# H1", "## H2", "### H3", "## Another H2"])
99
- expect(sections.map((s) => s.level)).toEqual([1, 2, 3, 2])
100
- })
101
- })
102
-
103
- describe("chunk", () => {
104
- // Simple mock tokenizer that counts words as tokens
105
- const mockTokenizer = {
106
- toks: (text: string) => text.split(/\s+/).filter(Boolean).length,
107
- } as Parameters<typeof chunkMarkdown>[1]
108
-
109
- test("small document produces single chunk", () => {
110
- const result = chunkMarkdown("# Title\n\nShort content.", mockTokenizer, 100)
111
- expect(result).toHaveLength(1)
112
- expect(result[0]).toContain("Title")
113
- expect(result[0]).toContain("Short content.")
114
- })
115
-
116
- test("splits at section boundaries", () => {
117
- const md = `# Title
118
-
119
- First section with enough words to take some space in the chunk budget.
120
-
121
- ## Second Section
122
-
123
- Second section content that should go into a new chunk because of size limits.
124
- `
125
- const result = chunkMarkdown(md, mockTokenizer, 15)
126
- expect(result.length).toBeGreaterThan(1)
127
- })
128
-
129
- test("includes parent headings in child chunks", () => {
130
- const md = `# Parent
131
-
132
- ## Child
133
-
134
- Child content that is long enough to be its own chunk separate from the parent section.
135
- `
136
- const result = chunkMarkdown(md, mockTokenizer, 15)
137
- // The child chunk should include "# Parent" for context
138
- const childChunk = result.find((c) => c.includes("Child content"))
139
- expect(childChunk).toContain("# Parent")
140
- })
141
-
142
- test("handles deeply nested headings", () => {
143
- const md = `# Level 1
144
-
145
- ## Level 2
146
-
147
- ### Level 3
148
-
149
- Deep content that needs all its parent headings for context.
150
- `
151
- const result = chunkMarkdown(md, mockTokenizer, 20)
152
- const deepChunk = result.find((c) => c.includes("Deep content"))
153
- expect(deepChunk).toBeDefined()
154
- if (deepChunk) {
155
- expect(deepChunk).toContain("# Level 1")
156
- expect(deepChunk).toContain("## Level 2")
157
- }
158
- })
159
-
160
- test("does not duplicate headings when packing siblings", () => {
161
- const md = `# Parent
162
-
163
- ## Child A
164
-
165
- Short A.
166
-
167
- ## Child B
168
-
169
- Short B.
170
- `
171
- const result = chunkMarkdown(md, mockTokenizer, 100)
172
- // Both children fit in one chunk, parent heading should appear only once
173
- expect(result).toHaveLength(1)
174
- const count = result[0].split("# Parent").length - 1
175
- expect(count).toBe(1)
176
- })
177
- })
@@ -1,105 +0,0 @@
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 DELETED
@@ -1,46 +0,0 @@
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
- })
package/test/util.test.ts DELETED
@@ -1,62 +0,0 @@
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
- })