@comfanion/workflow 4.38.3-dev.2 → 4.38.4-dev.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/package.json +1 -1
- package/src/build-info.json +4 -5
- package/src/opencode/config.yaml +0 -69
- package/src/opencode/gitignore +2 -0
- package/src/opencode/opencode.json +3 -5
- package/src/opencode/vectorizer.yaml +45 -0
- package/src/opencode/plugins/README.md +0 -182
- package/src/opencode/plugins/__tests__/custom-compaction.test.ts +0 -829
- package/src/opencode/plugins/__tests__/file-indexer.test.ts +0 -425
- package/src/opencode/plugins/__tests__/helpers/mock-ctx.ts +0 -171
- package/src/opencode/plugins/__tests__/leak-stress.test.ts +0 -315
- package/src/opencode/plugins/__tests__/usethis-todo.test.ts +0 -205
- package/src/opencode/plugins/__tests__/version-check.test.ts +0 -223
- package/src/opencode/plugins/custom-compaction.ts +0 -1080
- package/src/opencode/plugins/file-indexer.ts +0 -516
- package/src/opencode/plugins/usethis-todo-publish.ts +0 -44
- package/src/opencode/plugins/usethis-todo-ui.ts +0 -37
- package/src/opencode/plugins/version-check.ts +0 -230
- package/src/opencode/tools/codeindex.ts +0 -264
- package/src/opencode/tools/search.ts +0 -149
- package/src/opencode/tools/usethis_todo.ts +0 -538
- package/src/vectorizer/index.js +0 -573
- package/src/vectorizer/package.json +0 -16
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { join } from "path"
|
|
3
|
-
import { writeFile } from "fs/promises"
|
|
4
|
-
import { FileIndexerPlugin } from "../file-indexer"
|
|
5
|
-
import {
|
|
6
|
-
createMockCtx,
|
|
7
|
-
createTempDir,
|
|
8
|
-
cleanupTempDir,
|
|
9
|
-
FIXTURE_CONFIG_YAML,
|
|
10
|
-
} from "./helpers/mock-ctx"
|
|
11
|
-
import path from "path"
|
|
12
|
-
|
|
13
|
-
// =============================================================================
|
|
14
|
-
// LOCAL REPLICAS of internal functions (not exported from plugin)
|
|
15
|
-
// These mirror the source logic so we can unit-test the algorithms directly.
|
|
16
|
-
// =============================================================================
|
|
17
|
-
|
|
18
|
-
const DEFAULT_CONFIG = {
|
|
19
|
-
enabled: true,
|
|
20
|
-
auto_index: true,
|
|
21
|
-
debounce_ms: 1000,
|
|
22
|
-
indexes: {
|
|
23
|
-
code: { enabled: true, extensions: ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.scala', '.clj'] },
|
|
24
|
-
docs: { enabled: true, extensions: ['.md', '.mdx', '.txt', '.rst', '.adoc'] },
|
|
25
|
-
config: { enabled: false, extensions: ['.yaml', '.yml', '.json', '.toml', '.ini', '.xml'] },
|
|
26
|
-
},
|
|
27
|
-
exclude: ['node_modules', 'vendor', 'dist', 'build', 'out', '__pycache__'],
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface VectorizerConfig {
|
|
31
|
-
enabled: boolean
|
|
32
|
-
auto_index: boolean
|
|
33
|
-
debounce_ms: number
|
|
34
|
-
indexes: Record<string, { enabled: boolean; extensions: string[] }>
|
|
35
|
-
exclude: string[]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getIndexForFile(filePath: string, config: VectorizerConfig): string | null {
|
|
39
|
-
const ext = path.extname(filePath).toLowerCase()
|
|
40
|
-
for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
|
|
41
|
-
if (indexConfig.enabled && indexConfig.extensions.includes(ext)) {
|
|
42
|
-
return indexName
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return null
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isExcluded(relativePath: string, config: VectorizerConfig): boolean {
|
|
49
|
-
return config.exclude.some(pattern => relativePath.startsWith(pattern))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function estimateTime(fileCount: number): number {
|
|
53
|
-
const modelLoadTime = 30
|
|
54
|
-
const perFileTime = 0.5
|
|
55
|
-
const totalSeconds = modelLoadTime + (fileCount * perFileTime)
|
|
56
|
-
return Math.ceil(totalSeconds / 60)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function formatDuration(seconds: number): string {
|
|
60
|
-
if (seconds < 60) return `${Math.round(seconds)}s`
|
|
61
|
-
const mins = Math.floor(seconds / 60)
|
|
62
|
-
const secs = Math.round(seconds % 60)
|
|
63
|
-
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// =============================================================================
|
|
67
|
-
// UNIT TESTS: getIndexForFile (replica)
|
|
68
|
-
// =============================================================================
|
|
69
|
-
describe("getIndexForFile", () => {
|
|
70
|
-
const config = DEFAULT_CONFIG as VectorizerConfig
|
|
71
|
-
|
|
72
|
-
it("returns 'code' for TypeScript files", () => {
|
|
73
|
-
expect(getIndexForFile("src/app.ts", config)).toBe("code")
|
|
74
|
-
expect(getIndexForFile("src/app.tsx", config)).toBe("code")
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it("returns 'code' for JavaScript files", () => {
|
|
78
|
-
expect(getIndexForFile("src/index.js", config)).toBe("code")
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it("returns null for .mjs/.cjs (not in default extensions)", () => {
|
|
82
|
-
expect(getIndexForFile("src/util.mjs", config)).toBeNull()
|
|
83
|
-
expect(getIndexForFile("src/util.cjs", config)).toBeNull()
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it("returns 'code' for various language files", () => {
|
|
87
|
-
expect(getIndexForFile("main.py", config)).toBe("code")
|
|
88
|
-
expect(getIndexForFile("main.go", config)).toBe("code")
|
|
89
|
-
expect(getIndexForFile("main.rs", config)).toBe("code")
|
|
90
|
-
expect(getIndexForFile("Main.java", config)).toBe("code")
|
|
91
|
-
expect(getIndexForFile("Main.kt", config)).toBe("code")
|
|
92
|
-
expect(getIndexForFile("App.swift", config)).toBe("code")
|
|
93
|
-
expect(getIndexForFile("main.c", config)).toBe("code")
|
|
94
|
-
expect(getIndexForFile("main.cpp", config)).toBe("code")
|
|
95
|
-
expect(getIndexForFile("main.h", config)).toBe("code")
|
|
96
|
-
expect(getIndexForFile("main.hpp", config)).toBe("code")
|
|
97
|
-
expect(getIndexForFile("Program.cs", config)).toBe("code")
|
|
98
|
-
expect(getIndexForFile("app.rb", config)).toBe("code")
|
|
99
|
-
expect(getIndexForFile("index.php", config)).toBe("code")
|
|
100
|
-
expect(getIndexForFile("App.scala", config)).toBe("code")
|
|
101
|
-
expect(getIndexForFile("core.clj", config)).toBe("code")
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it("returns 'docs' for documentation files", () => {
|
|
105
|
-
expect(getIndexForFile("README.md", config)).toBe("docs")
|
|
106
|
-
expect(getIndexForFile("docs/api.mdx", config)).toBe("docs")
|
|
107
|
-
expect(getIndexForFile("notes.txt", config)).toBe("docs")
|
|
108
|
-
expect(getIndexForFile("guide.rst", config)).toBe("docs")
|
|
109
|
-
expect(getIndexForFile("manual.adoc", config)).toBe("docs")
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it("returns null for config files (disabled by default)", () => {
|
|
113
|
-
expect(getIndexForFile("config.yaml", config)).toBeNull()
|
|
114
|
-
expect(getIndexForFile("package.json", config)).toBeNull()
|
|
115
|
-
expect(getIndexForFile("settings.toml", config)).toBeNull()
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it("returns null for unknown file extensions", () => {
|
|
119
|
-
expect(getIndexForFile("image.png", config)).toBeNull()
|
|
120
|
-
expect(getIndexForFile("archive.zip", config)).toBeNull()
|
|
121
|
-
expect(getIndexForFile("data.csv", config)).toBeNull()
|
|
122
|
-
expect(getIndexForFile("noextension", config)).toBeNull()
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it("returns config index when enabled", () => {
|
|
126
|
-
const enabledConfig: VectorizerConfig = {
|
|
127
|
-
...config,
|
|
128
|
-
indexes: {
|
|
129
|
-
...config.indexes,
|
|
130
|
-
config: { enabled: true, extensions: [".yaml", ".yml", ".json", ".toml", ".ini", ".xml"] },
|
|
131
|
-
},
|
|
132
|
-
}
|
|
133
|
-
expect(getIndexForFile("config.yaml", enabledConfig)).toBe("config")
|
|
134
|
-
expect(getIndexForFile("package.json", enabledConfig)).toBe("config")
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
// =============================================================================
|
|
139
|
-
// UNIT TESTS: isExcluded (replica)
|
|
140
|
-
// =============================================================================
|
|
141
|
-
describe("isExcluded", () => {
|
|
142
|
-
const config = DEFAULT_CONFIG as VectorizerConfig
|
|
143
|
-
|
|
144
|
-
it("excludes node_modules paths", () => {
|
|
145
|
-
expect(isExcluded("node_modules/lodash/index.js", config)).toBe(true)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it("excludes vendor paths", () => {
|
|
149
|
-
expect(isExcluded("vendor/package/lib.go", config)).toBe(true)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it("excludes dist paths", () => {
|
|
153
|
-
expect(isExcluded("dist/bundle.js", config)).toBe(true)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it("excludes build paths", () => {
|
|
157
|
-
expect(isExcluded("build/output.js", config)).toBe(true)
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it("excludes __pycache__ paths", () => {
|
|
161
|
-
expect(isExcluded("__pycache__/module.pyc", config)).toBe(true)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it("does not exclude regular source paths", () => {
|
|
165
|
-
expect(isExcluded("src/app.ts", config)).toBe(false)
|
|
166
|
-
expect(isExcluded("lib/utils.js", config)).toBe(false)
|
|
167
|
-
expect(isExcluded("components/Button.tsx", config)).toBe(false)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it("does not exclude nested non-excluded paths", () => {
|
|
171
|
-
expect(isExcluded("src/vendor-custom/lib.ts", config)).toBe(false)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it("handles empty exclude list", () => {
|
|
175
|
-
const emptyConfig: VectorizerConfig = { ...config, exclude: [] }
|
|
176
|
-
expect(isExcluded("node_modules/test.js", emptyConfig)).toBe(false)
|
|
177
|
-
})
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
// =============================================================================
|
|
181
|
-
// UNIT TESTS: estimateTime (replica)
|
|
182
|
-
// =============================================================================
|
|
183
|
-
describe("estimateTime", () => {
|
|
184
|
-
it("returns at least 1 minute for small counts", () => {
|
|
185
|
-
expect(estimateTime(1)).toBeGreaterThanOrEqual(1)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it("scales with file count", () => {
|
|
189
|
-
const small = estimateTime(10)
|
|
190
|
-
const large = estimateTime(1000)
|
|
191
|
-
expect(large).toBeGreaterThan(small)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it("includes model load time", () => {
|
|
195
|
-
expect(estimateTime(0)).toBe(1)
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it("estimates correctly for 100 files", () => {
|
|
199
|
-
expect(estimateTime(100)).toBe(2)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it("estimates correctly for 1000 files", () => {
|
|
203
|
-
expect(estimateTime(1000)).toBe(9)
|
|
204
|
-
})
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
// =============================================================================
|
|
208
|
-
// UNIT TESTS: formatDuration (replica)
|
|
209
|
-
// =============================================================================
|
|
210
|
-
describe("formatDuration", () => {
|
|
211
|
-
it("formats seconds only", () => {
|
|
212
|
-
expect(formatDuration(30)).toBe("30s")
|
|
213
|
-
expect(formatDuration(59)).toBe("59s")
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it("formats minutes and seconds", () => {
|
|
217
|
-
expect(formatDuration(90)).toBe("1m 30s")
|
|
218
|
-
expect(formatDuration(125)).toBe("2m 5s")
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
it("formats exact minutes", () => {
|
|
222
|
-
expect(formatDuration(60)).toBe("1m")
|
|
223
|
-
expect(formatDuration(120)).toBe("2m")
|
|
224
|
-
expect(formatDuration(300)).toBe("5m")
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it("rounds seconds", () => {
|
|
228
|
-
expect(formatDuration(0.4)).toBe("0s")
|
|
229
|
-
expect(formatDuration(0.6)).toBe("1s")
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it("handles zero", () => {
|
|
233
|
-
expect(formatDuration(0)).toBe("0s")
|
|
234
|
-
})
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
// =============================================================================
|
|
238
|
-
// INTEGRATION: Plugin initialization & config parsing (via plugin interface)
|
|
239
|
-
// =============================================================================
|
|
240
|
-
describe("FileIndexerPlugin", () => {
|
|
241
|
-
let tempDir: string
|
|
242
|
-
|
|
243
|
-
beforeEach(async () => {
|
|
244
|
-
tempDir = await createTempDir()
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
afterEach(async () => {
|
|
248
|
-
await cleanupTempDir(tempDir)
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it("returns no-op event handler when disabled", async () => {
|
|
252
|
-
await writeFile(
|
|
253
|
-
join(tempDir, ".opencode", "config.yaml"),
|
|
254
|
-
`vectorizer:\n enabled: false\n auto_index: false\n`
|
|
255
|
-
)
|
|
256
|
-
const ctx = createMockCtx(tempDir)
|
|
257
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
258
|
-
|
|
259
|
-
expect(hooks).toBeDefined()
|
|
260
|
-
expect(hooks.event).toBeFunction()
|
|
261
|
-
|
|
262
|
-
// Should not throw
|
|
263
|
-
await hooks.event!({ event: { type: "file.edited" } as any })
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
it("returns no-op when auto_index is false", async () => {
|
|
267
|
-
await writeFile(
|
|
268
|
-
join(tempDir, ".opencode", "config.yaml"),
|
|
269
|
-
`vectorizer:\n enabled: true\n auto_index: false\n`
|
|
270
|
-
)
|
|
271
|
-
const ctx = createMockCtx(tempDir)
|
|
272
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
273
|
-
expect(hooks.event).toBeFunction()
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it("returns active event handler when enabled", async () => {
|
|
277
|
-
await writeFile(join(tempDir, ".opencode", "config.yaml"), FIXTURE_CONFIG_YAML)
|
|
278
|
-
const ctx = createMockCtx(tempDir)
|
|
279
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
280
|
-
|
|
281
|
-
expect(hooks).toBeDefined()
|
|
282
|
-
expect(hooks.event).toBeFunction()
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
it("event handler processes file.edited events without crashing", async () => {
|
|
286
|
-
await writeFile(join(tempDir, ".opencode", "config.yaml"), FIXTURE_CONFIG_YAML)
|
|
287
|
-
const ctx = createMockCtx(tempDir)
|
|
288
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
289
|
-
|
|
290
|
-
// Queue a file edit — should not throw
|
|
291
|
-
await hooks.event!({
|
|
292
|
-
event: {
|
|
293
|
-
type: "file.edited",
|
|
294
|
-
properties: { file: join(tempDir, "src", "app.ts") },
|
|
295
|
-
} as any,
|
|
296
|
-
})
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
it("event handler ignores non-file events", async () => {
|
|
300
|
-
await writeFile(join(tempDir, ".opencode", "config.yaml"), FIXTURE_CONFIG_YAML)
|
|
301
|
-
const ctx = createMockCtx(tempDir)
|
|
302
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
303
|
-
|
|
304
|
-
// These should be silently ignored
|
|
305
|
-
await hooks.event!({ event: { type: "session.idle" } as any })
|
|
306
|
-
await hooks.event!({ event: { type: "todo.updated" } as any })
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
it("event handler ignores events without file path", async () => {
|
|
310
|
-
await writeFile(join(tempDir, ".opencode", "config.yaml"), FIXTURE_CONFIG_YAML)
|
|
311
|
-
const ctx = createMockCtx(tempDir)
|
|
312
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
313
|
-
|
|
314
|
-
await hooks.event!({
|
|
315
|
-
event: { type: "file.edited", properties: {} } as any,
|
|
316
|
-
})
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
it("loads default config when no config.yaml exists", async () => {
|
|
320
|
-
// No config.yaml file at all
|
|
321
|
-
const ctx = createMockCtx(tempDir)
|
|
322
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
323
|
-
expect(hooks).toBeDefined()
|
|
324
|
-
expect(hooks.event).toBeFunction()
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
it("parses vectorizer section from config.yaml", async () => {
|
|
328
|
-
await writeFile(join(tempDir, ".opencode", "config.yaml"), FIXTURE_CONFIG_YAML)
|
|
329
|
-
const ctx = createMockCtx(tempDir)
|
|
330
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
331
|
-
// If config parsed correctly, plugin should return active event handler
|
|
332
|
-
expect(hooks).toBeDefined()
|
|
333
|
-
expect(hooks.event).toBeFunction()
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
it("parses disabled vectorizer and returns no-op", async () => {
|
|
337
|
-
await writeFile(
|
|
338
|
-
join(tempDir, ".opencode", "config.yaml"),
|
|
339
|
-
`vectorizer:\n enabled: false\n auto_index: false\n`
|
|
340
|
-
)
|
|
341
|
-
const ctx = createMockCtx(tempDir)
|
|
342
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
343
|
-
expect(hooks.event).toBeFunction()
|
|
344
|
-
// Event should be a no-op (disabled plugin)
|
|
345
|
-
await hooks.event!({
|
|
346
|
-
event: {
|
|
347
|
-
type: "file.edited",
|
|
348
|
-
properties: { file: join(tempDir, "src", "app.ts") },
|
|
349
|
-
} as any,
|
|
350
|
-
})
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it("getLanguage returns 'uk' for Ukrainian config (verified via plugin init)", async () => {
|
|
354
|
-
await writeFile(
|
|
355
|
-
join(tempDir, ".opencode", "config.yaml"),
|
|
356
|
-
`communication_language: Ukrainian\nvectorizer:\n enabled: true\n auto_index: true\n`
|
|
357
|
-
)
|
|
358
|
-
const ctx = createMockCtx(tempDir)
|
|
359
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
360
|
-
// Plugin loaded successfully with Ukrainian language detection
|
|
361
|
-
expect(hooks).toBeDefined()
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
it("getLanguage returns 'en' by default", async () => {
|
|
365
|
-
// No communication_language in config
|
|
366
|
-
await writeFile(
|
|
367
|
-
join(tempDir, ".opencode", "config.yaml"),
|
|
368
|
-
`vectorizer:\n enabled: true\n auto_index: true\n`
|
|
369
|
-
)
|
|
370
|
-
const ctx = createMockCtx(tempDir)
|
|
371
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
372
|
-
expect(hooks).toBeDefined()
|
|
373
|
-
})
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
// =============================================================================
|
|
377
|
-
// MEMORY SAFETY: Plugin instance leak detection
|
|
378
|
-
// =============================================================================
|
|
379
|
-
describe("memory safety: plugin instances", () => {
|
|
380
|
-
it("multiple plugin instantiations don't cause excessive memory growth", async () => {
|
|
381
|
-
const memBefore = process.memoryUsage().heapUsed
|
|
382
|
-
|
|
383
|
-
for (let i = 0; i < 20; i++) {
|
|
384
|
-
const dir = await createTempDir({
|
|
385
|
-
".opencode/config.yaml": `vectorizer:\n enabled: false\n auto_index: false\n`,
|
|
386
|
-
})
|
|
387
|
-
const ctx = createMockCtx(dir)
|
|
388
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
389
|
-
await hooks.event!({ event: { type: "session.idle" } as any })
|
|
390
|
-
await cleanupTempDir(dir)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if ((globalThis as any).gc) (globalThis as any).gc()
|
|
394
|
-
|
|
395
|
-
const memAfter = process.memoryUsage().heapUsed
|
|
396
|
-
const growthMB = (memAfter - memBefore) / 1024 / 1024
|
|
397
|
-
|
|
398
|
-
// 20 disabled-plugin instances shouldn't use >50MB
|
|
399
|
-
expect(growthMB).toBeLessThan(50)
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
it("repeated file.edited events don't crash (enabled plugin, no vectorizer)", async () => {
|
|
403
|
-
const dir = await createTempDir({
|
|
404
|
-
".opencode/config.yaml": FIXTURE_CONFIG_YAML,
|
|
405
|
-
})
|
|
406
|
-
const ctx = createMockCtx(dir)
|
|
407
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
408
|
-
|
|
409
|
-
// Send many events — plugin will queue them but vectorizer not installed
|
|
410
|
-
// so the timeout callback should clear pendingFiles (fix #2)
|
|
411
|
-
for (let i = 0; i < 100; i++) {
|
|
412
|
-
await hooks.event!({
|
|
413
|
-
event: {
|
|
414
|
-
type: "file.edited",
|
|
415
|
-
properties: { file: join(dir, `src/file-${i}.ts`) },
|
|
416
|
-
} as any,
|
|
417
|
-
})
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Wait for debounce timeout to fire and clear pendingFiles
|
|
421
|
-
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
422
|
-
|
|
423
|
-
await cleanupTempDir(dir)
|
|
424
|
-
})
|
|
425
|
-
})
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mock PluginInput context for testing plugins.
|
|
3
|
-
* Creates a temp directory with optional fixture files.
|
|
4
|
-
*/
|
|
5
|
-
import { mkdtemp, mkdir, writeFile, rm } from "fs/promises"
|
|
6
|
-
import { tmpdir } from "os"
|
|
7
|
-
import { join } from "path"
|
|
8
|
-
|
|
9
|
-
export interface MockCtx {
|
|
10
|
-
directory: string
|
|
11
|
-
worktree: string
|
|
12
|
-
client: {
|
|
13
|
-
tui: {
|
|
14
|
-
showToast: (...args: any[]) => Promise<void>
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
project: { name: string }
|
|
18
|
-
serverUrl: URL
|
|
19
|
-
$: any
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function createMockCtx(directory: string): MockCtx {
|
|
23
|
-
const calls: any[][] = []
|
|
24
|
-
const showToast = (...args: any[]) => {
|
|
25
|
-
calls.push(args)
|
|
26
|
-
return Promise.resolve()
|
|
27
|
-
}
|
|
28
|
-
// Attach calls for inspection
|
|
29
|
-
;(showToast as any).calls = calls
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
directory,
|
|
33
|
-
worktree: directory,
|
|
34
|
-
client: {
|
|
35
|
-
tui: { showToast },
|
|
36
|
-
},
|
|
37
|
-
project: { name: "test-project" },
|
|
38
|
-
serverUrl: new URL("http://localhost:3000"),
|
|
39
|
-
$: {},
|
|
40
|
-
} as any
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Create a temp directory with .opencode/ subdirectory
|
|
45
|
-
* and optional fixture files.
|
|
46
|
-
*/
|
|
47
|
-
export async function createTempDir(
|
|
48
|
-
fixtures?: Record<string, string>
|
|
49
|
-
): Promise<string> {
|
|
50
|
-
const dir = await mkdtemp(join(tmpdir(), "plugin-test-"))
|
|
51
|
-
await mkdir(join(dir, ".opencode"), { recursive: true })
|
|
52
|
-
await mkdir(join(dir, ".opencode", "state"), { recursive: true })
|
|
53
|
-
|
|
54
|
-
if (fixtures) {
|
|
55
|
-
for (const [relativePath, content] of Object.entries(fixtures)) {
|
|
56
|
-
const fullPath = join(dir, relativePath)
|
|
57
|
-
const parentDir = join(fullPath, "..")
|
|
58
|
-
await mkdir(parentDir, { recursive: true })
|
|
59
|
-
await writeFile(fullPath, content, "utf-8")
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return dir
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Remove temp directory
|
|
68
|
-
*/
|
|
69
|
-
export async function cleanupTempDir(dir: string): Promise<void> {
|
|
70
|
-
try {
|
|
71
|
-
await rm(dir, { recursive: true, force: true })
|
|
72
|
-
} catch {
|
|
73
|
-
// ignore cleanup errors
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Fixture: session-state.yaml for dev agent working on a story
|
|
79
|
-
*/
|
|
80
|
-
export const FIXTURE_SESSION_STATE = `command: /dev-epic
|
|
81
|
-
agent: dev
|
|
82
|
-
sprint:
|
|
83
|
-
number: 1
|
|
84
|
-
status: in-progress
|
|
85
|
-
epic:
|
|
86
|
-
id: AUTH-E01
|
|
87
|
-
title: Authentication Module
|
|
88
|
-
file: docs/sprint-artifacts/sprint-1/epic-01-auth.md
|
|
89
|
-
progress: 2/5 stories done
|
|
90
|
-
story:
|
|
91
|
-
id: AUTH-S01-03
|
|
92
|
-
title: JWT Token Refresh
|
|
93
|
-
file: docs/sprint-artifacts/sprint-1/stories/story-01-03-jwt-refresh.md
|
|
94
|
-
current_task: T2
|
|
95
|
-
completed_tasks: [T1]
|
|
96
|
-
pending_tasks: [T2, T3, T4]
|
|
97
|
-
next_action: Implement T2 - token refresh endpoint
|
|
98
|
-
key_decisions:
|
|
99
|
-
- Using RS256 for JWT signing
|
|
100
|
-
- Refresh tokens stored in Redis
|
|
101
|
-
`
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Fixture: story markdown file
|
|
105
|
-
*/
|
|
106
|
-
export const FIXTURE_STORY_MD = `# AUTH-S01-03: JWT Token Refresh
|
|
107
|
-
|
|
108
|
-
**Status:** in-progress
|
|
109
|
-
|
|
110
|
-
## Tasks
|
|
111
|
-
|
|
112
|
-
- [x] **T1**: Define refresh token interface
|
|
113
|
-
- [ ] **T2**: Implement refresh endpoint
|
|
114
|
-
- [ ] **T3**: Add token rotation logic
|
|
115
|
-
- [ ] **T4**: Write integration tests
|
|
116
|
-
|
|
117
|
-
## Acceptance Criteria
|
|
118
|
-
|
|
119
|
-
- [x] Refresh tokens are stored securely
|
|
120
|
-
- [ ] Token rotation works on refresh
|
|
121
|
-
- [ ] Expired tokens are rejected
|
|
122
|
-
`
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Fixture: epic state YAML
|
|
126
|
-
*/
|
|
127
|
-
export const FIXTURE_EPIC_STATE = `epic_id: "AUTH-E01"
|
|
128
|
-
epic_title: "Authentication Module"
|
|
129
|
-
status: "in-progress"
|
|
130
|
-
current_story_index: 2
|
|
131
|
-
total_stories: 5
|
|
132
|
-
completed_stories:
|
|
133
|
-
- path: docs/sprint-artifacts/sprint-1/stories/story-01-01-login.md
|
|
134
|
-
status: done
|
|
135
|
-
- path: docs/sprint-artifacts/sprint-1/stories/story-01-02-register.md
|
|
136
|
-
status: done
|
|
137
|
-
pending_stories:
|
|
138
|
-
- path: docs/sprint-artifacts/sprint-1/stories/story-01-03-jwt-refresh.md
|
|
139
|
-
status: in-progress
|
|
140
|
-
- path: docs/sprint-artifacts/sprint-1/stories/story-01-04-logout.md
|
|
141
|
-
status: pending
|
|
142
|
-
- path: docs/sprint-artifacts/sprint-1/stories/story-01-05-password-reset.md
|
|
143
|
-
status: pending
|
|
144
|
-
next_action: "Continue story-01-03-jwt-refresh.md"
|
|
145
|
-
`
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Fixture: config.yaml
|
|
149
|
-
*/
|
|
150
|
-
export const FIXTURE_CONFIG_YAML = `project_name: "test-project"
|
|
151
|
-
version: "1.0.0"
|
|
152
|
-
communication_language: "Ukrainian"
|
|
153
|
-
|
|
154
|
-
vectorizer:
|
|
155
|
-
enabled: true
|
|
156
|
-
auto_index: true
|
|
157
|
-
debounce_ms: 500
|
|
158
|
-
exclude:
|
|
159
|
-
- node_modules
|
|
160
|
-
- dist
|
|
161
|
-
- build
|
|
162
|
-
`
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Fixture: todo list
|
|
166
|
-
*/
|
|
167
|
-
export const FIXTURE_TODOS = JSON.stringify([
|
|
168
|
-
{ id: "1", content: "Implement T2 - refresh endpoint", status: "in_progress", priority: "high" },
|
|
169
|
-
{ id: "2", content: "Write T3 - token rotation", status: "pending", priority: "high" },
|
|
170
|
-
{ id: "3", content: "Write T4 - integration tests", status: "pending", priority: "medium" },
|
|
171
|
-
])
|