@m16khb/llm-wiki 0.1.0 → 0.1.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.
@@ -0,0 +1,415 @@
1
+ package sessionctx
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+
9
+ "github.com/m16khb/llm-wiki/internal/wiki"
10
+ )
11
+
12
+ func TestBuildContextDerivesProjectTermsAndSearchesWiki(t *testing.T) {
13
+ vaultRoot := t.TempDir()
14
+ writeFile(t, vaultRoot, "20-wiki/concepts/llm-wiki-hooks.md", `---
15
+ title: LLM Wiki Hooks
16
+ type: concept
17
+ status: active
18
+ created: 2026-05-23
19
+ updated: 2026-05-23
20
+ tags: [llm-wiki, hooks]
21
+ domain: agent-memory
22
+ ---
23
+
24
+ # LLM Wiki Hooks
25
+
26
+ Codex and Claude Code use portable hook commands for llm-wiki context.
27
+ `)
28
+ projectRoot := filepath.Join(t.TempDir(), "llm-wiki")
29
+ writeFile(t, projectRoot, "go.mod", "module github.com/m16khb/llm-wiki\n")
30
+ writeFile(t, projectRoot, "README.md", "# LLM Wiki\n")
31
+
32
+ v, err := wiki.New(vaultRoot)
33
+ if err != nil {
34
+ t.Fatalf("wiki.New() error = %v", err)
35
+ }
36
+ result, err := BuildContext(v, Options{ProjectPath: projectRoot, Limit: 5})
37
+ if err != nil {
38
+ t.Fatalf("BuildContext() error = %v", err)
39
+ }
40
+ if result.Project.Name != "llm-wiki" || !contains(result.Project.Terms, "llm-wiki") {
41
+ t.Fatalf("unexpected project info: %+v", result.Project)
42
+ }
43
+ if len(result.Results) != 1 || result.Results[0].Slug != "llm-wiki-hooks" {
44
+ t.Fatalf("expected hook search hit, got %+v", result.Results)
45
+ }
46
+ text := FormatAdditionalContext(result)
47
+ if !strings.Contains(text, "llm-wiki session context") || !strings.Contains(text, "[[llm-wiki-hooks]]") {
48
+ t.Fatalf("unexpected context text:\n%s", text)
49
+ }
50
+ }
51
+
52
+ func TestBuildContextAddsTechResultsForGoAndSwaggerProject(t *testing.T) {
53
+ vaultRoot := t.TempDir()
54
+ writeFile(t, vaultRoot, "20-wiki/concepts/llm-wiki-hooks.md", `---
55
+ title: LLM Wiki Hooks
56
+ type: concept
57
+ status: active
58
+ created: 2026-05-23
59
+ updated: 2026-05-23
60
+ tags: [llm-wiki, hooks]
61
+ domain: agent-memory
62
+ ---
63
+
64
+ # LLM Wiki Hooks
65
+
66
+ Project-specific context for llm-wiki hook behavior.
67
+ `)
68
+ writeFile(t, vaultRoot, "20-wiki/concepts/go-conventions.md", `---
69
+ title: Go Conventions
70
+ type: concept
71
+ status: active
72
+ created: 2026-05-23
73
+ updated: 2026-05-23
74
+ tags: [go, golang]
75
+ domain: programming
76
+ ---
77
+
78
+ # Go Conventions
79
+
80
+ Go and Golang style rules for agents working in Go modules.
81
+ `)
82
+ writeFile(t, vaultRoot, "20-wiki/concepts/swagger-rules.md", `---
83
+ title: Swagger Rules
84
+ type: concept
85
+ status: active
86
+ created: 2026-05-23
87
+ updated: 2026-05-23
88
+ tags: [swagger, openapi, swaggo]
89
+ domain: api
90
+ ---
91
+
92
+ # Swagger Rules
93
+
94
+ Swagger and OpenAPI documentation rules for API projects.
95
+ `)
96
+ writeFile(t, vaultRoot, "20-wiki/concepts/mcp-conventions.md", `---
97
+ title: MCP Conventions
98
+ type: concept
99
+ status: active
100
+ created: 2026-05-23
101
+ updated: 2026-05-23
102
+ tags: [mcp]
103
+ domain: protocol
104
+ ---
105
+
106
+ # MCP Conventions
107
+
108
+ Model Context Protocol rules for local tools.
109
+ `)
110
+ writeFile(t, vaultRoot, "20-wiki/concepts/sqlite-conventions.md", `---
111
+ title: SQLite Conventions
112
+ type: concept
113
+ status: active
114
+ created: 2026-05-23
115
+ updated: 2026-05-23
116
+ tags: [sqlite]
117
+ domain: database
118
+ ---
119
+
120
+ # SQLite Conventions
121
+
122
+ SQLite WAL queue rules.
123
+ `)
124
+ writeFile(t, vaultRoot, "20-wiki/concepts/llm-wiki-tech-map.md", `---
125
+ title: LLM Wiki Tech Map
126
+ type: concept
127
+ status: active
128
+ created: 2026-05-23
129
+ updated: 2026-05-23
130
+ tags: [llm-wiki, go, golang, mcp, sqlite, swagger, openapi, swaggo]
131
+ domain: agent-memory
132
+ ---
133
+
134
+ # LLM Wiki Tech Map
135
+
136
+ Project-specific map for llm-wiki Go, MCP, SQLite, and Swagger retrieval.
137
+ `)
138
+ writeFile(t, vaultRoot, "20-wiki/concepts/gin-conventions.md", `---
139
+ title: Gin Conventions
140
+ type: concept
141
+ status: active
142
+ created: 2026-05-23
143
+ updated: 2026-05-23
144
+ tags: [go, gin]
145
+ domain: web
146
+ ---
147
+
148
+ # Gin Conventions
149
+
150
+ Go Gin framework rules. This should only appear when Gin is detected.
151
+ `)
152
+ writeFile(t, vaultRoot, "20-wiki/concepts/repository-references/other-api/other-api-go-noise.md", `---
153
+ title: Other API Go Noise
154
+ type: concept
155
+ status: active
156
+ created: 2026-05-23
157
+ updated: 2026-05-23
158
+ tags: [go]
159
+ domain: programming
160
+ ---
161
+
162
+ # Other API Go Noise
163
+
164
+ go go go go go go go go go go go go go go go go go go go go
165
+ golang golang golang golang golang golang golang golang golang golang
166
+ `)
167
+ projectRoot := filepath.Join(t.TempDir(), "llm-wiki")
168
+ writeFile(t, projectRoot, "go.mod", `module github.com/m16khb/llm-wiki
169
+
170
+ require (
171
+ github.com/modelcontextprotocol/go-sdk v1.6.0
172
+ github.com/swaggo/gin-swagger v1.6.0
173
+ modernc.org/sqlite v1.50.1
174
+ )
175
+ `)
176
+ writeFile(t, projectRoot, "README.md", "# LLM Wiki\n")
177
+
178
+ v, err := wiki.New(vaultRoot)
179
+ if err != nil {
180
+ t.Fatalf("wiki.New() error = %v", err)
181
+ }
182
+ result, err := BuildContext(v, Options{ProjectPath: projectRoot, Limit: 5})
183
+ if err != nil {
184
+ t.Fatalf("BuildContext() error = %v", err)
185
+ }
186
+ text := FormatAdditionalContext(result)
187
+ for _, want := range []string{"Detected tech terms:", "go", "golang", "mcp", "sqlite", "swagger", "openapi", "swaggo", "Suggested reads:", "[[go-conventions]]", "[[swagger-rules]]", "[[mcp-conventions]]", "[[sqlite-conventions]]"} {
188
+ if !strings.Contains(text, want) {
189
+ t.Fatalf("expected formatted context to contain %q:\n%s", want, text)
190
+ }
191
+ }
192
+ if len(result.TechResults) == 0 || result.TechResults[0].Slug == "other-api-go-noise" {
193
+ t.Fatalf("expected unrelated repository-reference tech hit to be demoted, got %+v", result.TechResults)
194
+ }
195
+ if result.TechResults[0].Slug != "llm-wiki-tech-map" {
196
+ t.Fatalf("expected project-specific tech map first, got %+v", result.TechResults)
197
+ }
198
+ for _, hit := range result.TechResults {
199
+ if hit.Slug == "gin-conventions" {
200
+ t.Fatalf("did not expect Gin-specific page without Gin dependency, got %+v", result.TechResults)
201
+ }
202
+ if hit.Slug == "other-api-go-noise" {
203
+ t.Fatalf("did not expect unrelated repository-reference page, got %+v", result.TechResults)
204
+ }
205
+ }
206
+ }
207
+
208
+ func TestBuildContextAddsPackageWrapperTechResults(t *testing.T) {
209
+ vaultRoot := t.TempDir()
210
+ writeFile(t, vaultRoot, "20-wiki/concepts/npm-wrapper.md", `---
211
+ title: npm Wrapper
212
+ type: concept
213
+ status: active
214
+ created: 2026-05-23
215
+ updated: 2026-05-23
216
+ tags: [node, npm, npx]
217
+ domain: packaging
218
+ ---
219
+
220
+ # npm Wrapper
221
+
222
+ Node, npm, and npx wrapper conventions.
223
+ `)
224
+ projectRoot := filepath.Join(t.TempDir(), "wrapped-cli")
225
+ writeFile(t, projectRoot, "package.json", `{
226
+ "name": "@example/wrapped-cli",
227
+ "bin": {
228
+ "wrapped-cli": "npm/bin/wrapped-cli.js"
229
+ }
230
+ }
231
+ `)
232
+
233
+ v, err := wiki.New(vaultRoot)
234
+ if err != nil {
235
+ t.Fatalf("wiki.New() error = %v", err)
236
+ }
237
+ result, err := BuildContext(v, Options{ProjectPath: projectRoot, Limit: 5})
238
+ if err != nil {
239
+ t.Fatalf("BuildContext() error = %v", err)
240
+ }
241
+ text := FormatAdditionalContext(result)
242
+ for _, want := range []string{"Detected tech terms:", "node", "npm", "npx", "Suggested reads:", "[[npm-wrapper]]"} {
243
+ if !strings.Contains(text, want) {
244
+ t.Fatalf("expected formatted context to contain %q:\n%s", want, text)
245
+ }
246
+ }
247
+ }
248
+
249
+ func TestBuildContextAddsSwaggerTechResultsForOpenAPIFile(t *testing.T) {
250
+ vaultRoot := t.TempDir()
251
+ writeFile(t, vaultRoot, "20-wiki/concepts/swagger-rules.md", `---
252
+ title: Swagger Rules
253
+ type: concept
254
+ status: active
255
+ created: 2026-05-23
256
+ updated: 2026-05-23
257
+ tags: [swagger, openapi]
258
+ domain: api
259
+ ---
260
+
261
+ # Swagger Rules
262
+
263
+ OpenAPI and Swagger documentation conventions.
264
+ `)
265
+ projectRoot := filepath.Join(t.TempDir(), "api-project")
266
+ writeFile(t, projectRoot, "openapi.yaml", "openapi: 3.0.0\ninfo:\n title: API\n")
267
+
268
+ v, err := wiki.New(vaultRoot)
269
+ if err != nil {
270
+ t.Fatalf("wiki.New() error = %v", err)
271
+ }
272
+ result, err := BuildContext(v, Options{ProjectPath: projectRoot, Limit: 5})
273
+ if err != nil {
274
+ t.Fatalf("BuildContext() error = %v", err)
275
+ }
276
+ text := FormatAdditionalContext(result)
277
+ for _, want := range []string{"Detected tech terms:", "swagger", "openapi", "Suggested reads:", "[[swagger-rules]]"} {
278
+ if !strings.Contains(text, want) {
279
+ t.Fatalf("expected formatted context to contain %q:\n%s", want, text)
280
+ }
281
+ }
282
+ }
283
+
284
+ func TestFormatAdditionalContextRendersConciseReadList(t *testing.T) {
285
+ ctx := Context{
286
+ Project: Project{
287
+ Path: "/workspace/llm-wiki",
288
+ Name: "@m16khb/llm-wiki",
289
+ Terms: []string{"llm-wiki", "wiki"},
290
+ TechTerms: []string{"go", "mcp"},
291
+ },
292
+ Query: "go hook context",
293
+ Results: []wiki.SearchResult{
294
+ {
295
+ Slug: "index",
296
+ Path: "00-meta/index.md",
297
+ Title: "Wiki Index",
298
+ Type: "index",
299
+ Score: 99,
300
+ Snippet: "# Wiki Index\n\nHuge catalog noise should not appear in hook output.",
301
+ },
302
+ {
303
+ Slug: "llm-wiki-hooks",
304
+ Path: "20-wiki/concepts/llm-wiki-hooks.md",
305
+ Title: "LLM Wiki Hooks",
306
+ Type: "concept",
307
+ Score: 80,
308
+ Snippet: "# LLM Wiki Hooks\n\nLong raw snippets make hook output noisy.",
309
+ },
310
+ },
311
+ TechResults: []wiki.SearchResult{
312
+ {
313
+ Slug: "go-conventions",
314
+ Path: "20-wiki/concepts/go-conventions.md",
315
+ Title: "Go Conventions",
316
+ Type: "concept",
317
+ Score: 30,
318
+ Snippet: "# Go Conventions\n\nDetailed page body should be read explicitly, not injected.",
319
+ },
320
+ {
321
+ Slug: "api-swagger-openapi-conventions",
322
+ Path: "20-wiki/concepts/api-swagger-openapi-conventions.md",
323
+ Title: "Swagger / OpenAPI Conventions",
324
+ Type: "concept",
325
+ Tags: []string{"go", "swagger", "openapi"},
326
+ Score: 28,
327
+ Snippet: "# Swagger / OpenAPI Conventions\n\nOnly include when requested or detected.",
328
+ },
329
+ },
330
+ }
331
+
332
+ text := FormatAdditionalContext(ctx)
333
+ for _, want := range []string{
334
+ "Suggested reads:",
335
+ "- [[llm-wiki-hooks]] `20-wiki/concepts/llm-wiki-hooks.md` — LLM Wiki Hooks",
336
+ "- [[go-conventions]] `20-wiki/concepts/go-conventions.md` — Go Conventions",
337
+ "Hook context is search-only; read listed pages before relying on details.",
338
+ } {
339
+ if !strings.Contains(text, want) {
340
+ t.Fatalf("expected concise context to contain %q:\n%s", want, text)
341
+ }
342
+ }
343
+ for _, notWant := range []string{
344
+ "Relevant wiki pages:",
345
+ "Relevant tech wiki pages:",
346
+ "[[index]]",
347
+ "api-swagger-openapi-conventions",
348
+ "Huge catalog noise",
349
+ "Long raw snippets",
350
+ "Detailed page body",
351
+ "Only include when requested",
352
+ } {
353
+ if strings.Contains(text, notWant) {
354
+ t.Fatalf("did not expect noisy hook output %q:\n%s", notWant, text)
355
+ }
356
+ }
357
+ }
358
+
359
+ func TestFormatAdditionalContextUsesQueryTermsWithoutTechAllowlist(t *testing.T) {
360
+ ctx := Context{
361
+ Project: Project{
362
+ Path: "/workspace/polyglot",
363
+ Name: "polyglot",
364
+ Terms: []string{"polyglot"},
365
+ },
366
+ Query: "k8s rust c++",
367
+ Results: []wiki.SearchResult{
368
+ {Slug: "k8s-patterns", Path: "20-wiki/concepts/k8s-patterns.md", Title: "K8s Patterns", Type: "concept", Score: 30},
369
+ {Slug: "rust-async-stack", Path: "20-wiki/concepts/rust-async-stack.md", Title: "Rust Async Stack", Type: "concept", Score: 30},
370
+ {Slug: "cpp-guide", Path: "20-wiki/concepts/cpp-guide.md", Title: "C++ Guide", Type: "concept", Score: 30},
371
+ {Slug: "unrelated-api", Path: "20-wiki/concepts/unrelated-api.md", Title: "Unrelated API", Type: "concept", Tags: []string{"k8s"}, Score: 30},
372
+ },
373
+ }
374
+
375
+ text := FormatAdditionalContext(ctx)
376
+ for _, want := range []string{"[[k8s-patterns]]", "[[rust-async-stack]]", "[[cpp-guide]]"} {
377
+ if !strings.Contains(text, want) {
378
+ t.Fatalf("expected query-matched page %q without a hard-coded tech allowlist:\n%s", want, text)
379
+ }
380
+ }
381
+ if strings.Contains(text, "[[unrelated-api]]") {
382
+ t.Fatalf("did not expect a page to match only through tags:\n%s", text)
383
+ }
384
+ }
385
+
386
+ func TestExtractCaptureBlock(t *testing.T) {
387
+ msg := "done\n\n```llm-wiki-capture\ntitle: Hook decision\ntags: hooks, codex\n---\nPersist this decision.\n```\n"
388
+ block, ok := ExtractCaptureBlock(msg)
389
+ if !ok {
390
+ t.Fatalf("expected capture block")
391
+ }
392
+ if block.Title != "Hook decision" || block.Body != "Persist this decision." || len(block.Tags) != 2 {
393
+ t.Fatalf("unexpected block: %+v", block)
394
+ }
395
+ }
396
+
397
+ func contains(values []string, want string) bool {
398
+ for _, v := range values {
399
+ if v == want {
400
+ return true
401
+ }
402
+ }
403
+ return false
404
+ }
405
+
406
+ func writeFile(t *testing.T, root, rel, content string) {
407
+ t.Helper()
408
+ abs := filepath.Join(root, filepath.FromSlash(rel))
409
+ if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
410
+ t.Fatalf("mkdir %s: %v", rel, err)
411
+ }
412
+ if err := os.WriteFile(abs, []byte(content), 0o644); err != nil {
413
+ t.Fatalf("write %s: %v", rel, err)
414
+ }
415
+ }