@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,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Memory Leak Stress Tests
|
|
3
|
-
*
|
|
4
|
-
* Goal: prove the 3 plugins are NOT the source of a memory leak.
|
|
5
|
-
*
|
|
6
|
-
* Strategy:
|
|
7
|
-
* 1. Run each plugin's hot path N times in a tight loop
|
|
8
|
-
* 2. Measure heap snapshots at intervals
|
|
9
|
-
* 3. Assert heap growth is sub-linear (flat / bounded)
|
|
10
|
-
* 4. Specifically test known risk areas:
|
|
11
|
-
* - custom-compaction: closure variables (lastActiveAgent, lastSessionId)
|
|
12
|
-
* - file-indexer: module-level pendingFiles Map
|
|
13
|
-
* - version-check: stateless (baseline)
|
|
14
|
-
*
|
|
15
|
-
* NOTE: Tests use only the public plugin interface (no internal imports).
|
|
16
|
-
*
|
|
17
|
-
* Run only leak tests:
|
|
18
|
-
* bun test plugins/__tests__/leak-stress.test.ts
|
|
19
|
-
*/
|
|
20
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
21
|
-
import { join } from "path"
|
|
22
|
-
import {
|
|
23
|
-
createMockCtx,
|
|
24
|
-
createTempDir,
|
|
25
|
-
cleanupTempDir,
|
|
26
|
-
FIXTURE_SESSION_STATE,
|
|
27
|
-
FIXTURE_STORY_MD,
|
|
28
|
-
FIXTURE_EPIC_STATE,
|
|
29
|
-
FIXTURE_TODOS,
|
|
30
|
-
FIXTURE_CONFIG_YAML,
|
|
31
|
-
} from "./helpers/mock-ctx"
|
|
32
|
-
import { CustomCompactionPlugin } from "../custom-compaction"
|
|
33
|
-
import { FileIndexerPlugin } from "../file-indexer"
|
|
34
|
-
import { VersionCheckPlugin } from "../version-check"
|
|
35
|
-
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Helpers
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
function heapMB(): number {
|
|
40
|
-
return process.memoryUsage().heapUsed / 1024 / 1024
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function tryGC(): void {
|
|
44
|
-
if (typeof globalThis.gc === "function") {
|
|
45
|
-
globalThis.gc()
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Take N heap snapshots at equal intervals during a callback. */
|
|
50
|
-
async function profile(
|
|
51
|
-
iterations: number,
|
|
52
|
-
snapshots: number,
|
|
53
|
-
fn: (i: number) => Promise<void>,
|
|
54
|
-
): Promise<number[]> {
|
|
55
|
-
const interval = Math.floor(iterations / snapshots)
|
|
56
|
-
const heaps: number[] = []
|
|
57
|
-
|
|
58
|
-
tryGC()
|
|
59
|
-
heaps.push(heapMB())
|
|
60
|
-
|
|
61
|
-
for (let i = 0; i < iterations; i++) {
|
|
62
|
-
await fn(i)
|
|
63
|
-
if ((i + 1) % interval === 0) {
|
|
64
|
-
tryGC()
|
|
65
|
-
heaps.push(heapMB())
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return heaps
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Check that heap growth is bounded:
|
|
74
|
-
* last snapshot - first snapshot < maxGrowthMB.
|
|
75
|
-
*/
|
|
76
|
-
function assertBoundedGrowth(
|
|
77
|
-
heaps: number[],
|
|
78
|
-
maxGrowthMB: number,
|
|
79
|
-
label: string,
|
|
80
|
-
): void {
|
|
81
|
-
const growth = heaps[heaps.length - 1] - heaps[0]
|
|
82
|
-
const peak = Math.max(...heaps)
|
|
83
|
-
const valley = Math.min(...heaps)
|
|
84
|
-
|
|
85
|
-
console.log(
|
|
86
|
-
` [${label}] heap snapshots (MB): ${heaps.map((h) => h.toFixed(1)).join(" -> ")}`,
|
|
87
|
-
)
|
|
88
|
-
console.log(
|
|
89
|
-
` [${label}] growth: ${growth.toFixed(1)} MB, peak: ${peak.toFixed(1)} MB, valley: ${valley.toFixed(1)} MB`,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
expect(growth).toBeLessThan(maxGrowthMB)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// =============================================================================
|
|
96
|
-
// 1. CUSTOM COMPACTION -- hot path: chat.message + compaction
|
|
97
|
-
// =============================================================================
|
|
98
|
-
describe("leak: custom-compaction", () => {
|
|
99
|
-
const ITERATIONS = 500
|
|
100
|
-
const SNAPSHOTS = 5
|
|
101
|
-
const MAX_GROWTH_MB = 30
|
|
102
|
-
|
|
103
|
-
it("chat.message + compaction cycle doesn't leak", async () => {
|
|
104
|
-
const dir = await createTempDir({
|
|
105
|
-
".opencode/session-state.yaml": FIXTURE_SESSION_STATE,
|
|
106
|
-
".opencode/state/todos.json": FIXTURE_TODOS,
|
|
107
|
-
"docs/sprint-artifacts/sprint-1/stories/story-01-03-jwt-refresh.md":
|
|
108
|
-
FIXTURE_STORY_MD,
|
|
109
|
-
})
|
|
110
|
-
const ctx = createMockCtx(dir)
|
|
111
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
112
|
-
|
|
113
|
-
const heaps = await profile(ITERATIONS, SNAPSHOTS, async (i) => {
|
|
114
|
-
await hooks["chat.message"]!(
|
|
115
|
-
{ agent: "dev", sessionID: `s-${i}` } as any,
|
|
116
|
-
{ message: {}, parts: [] } as any,
|
|
117
|
-
)
|
|
118
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
119
|
-
await hooks["experimental.session.compacting"]!(
|
|
120
|
-
{ sessionID: `s-${i}` } as any,
|
|
121
|
-
output as any,
|
|
122
|
-
)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
assertBoundedGrowth(heaps, MAX_GROWTH_MB, "compaction-cycle")
|
|
126
|
-
await cleanupTempDir(dir)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it("chat.message rapid-fire doesn't accumulate strings", async () => {
|
|
130
|
-
const dir = await createTempDir()
|
|
131
|
-
const ctx = createMockCtx(dir)
|
|
132
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
133
|
-
|
|
134
|
-
const heaps = await profile(2000, SNAPSHOTS, async (i) => {
|
|
135
|
-
await hooks["chat.message"]!(
|
|
136
|
-
{ agent: `agent-${i % 50}`, sessionID: `s-${i}` } as any,
|
|
137
|
-
{ message: {}, parts: [] } as any,
|
|
138
|
-
)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
assertBoundedGrowth(heaps, MAX_GROWTH_MB, "chat.message-rapid")
|
|
142
|
-
await cleanupTempDir(dir)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it("event hook doesn't leak on repeated session.idle", async () => {
|
|
146
|
-
const dir = await createTempDir()
|
|
147
|
-
const ctx = createMockCtx(dir)
|
|
148
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
149
|
-
|
|
150
|
-
const heaps = await profile(1000, SNAPSHOTS, async () => {
|
|
151
|
-
await hooks.event!({ event: { type: "session.idle" } as any })
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
assertBoundedGrowth(heaps, MAX_GROWTH_MB, "event-idle")
|
|
155
|
-
await cleanupTempDir(dir)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it("plugin factory doesn't leak across many instances", async () => {
|
|
159
|
-
const heaps = await profile(100, SNAPSHOTS, async (i) => {
|
|
160
|
-
const dir = await createTempDir()
|
|
161
|
-
const ctx = createMockCtx(dir)
|
|
162
|
-
const hooks = await CustomCompactionPlugin(ctx as any)
|
|
163
|
-
await hooks["chat.message"]!(
|
|
164
|
-
{ agent: "dev", sessionID: `s-${i}` } as any,
|
|
165
|
-
{ message: {}, parts: [] } as any,
|
|
166
|
-
)
|
|
167
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
168
|
-
await hooks["experimental.session.compacting"]!(
|
|
169
|
-
{ sessionID: `s-${i}` } as any,
|
|
170
|
-
output as any,
|
|
171
|
-
)
|
|
172
|
-
await cleanupTempDir(dir)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
assertBoundedGrowth(heaps, 50, "compaction-instances")
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
// =============================================================================
|
|
180
|
-
// 2. FILE INDEXER -- hot path: event handler (file.edited queueing)
|
|
181
|
-
// =============================================================================
|
|
182
|
-
describe("leak: file-indexer", () => {
|
|
183
|
-
const ITERATIONS = 1000
|
|
184
|
-
const SNAPSHOTS = 5
|
|
185
|
-
const MAX_GROWTH_MB = 30
|
|
186
|
-
|
|
187
|
-
it("file.edited event handler doesn't leak (cycling file names)", async () => {
|
|
188
|
-
const dir = await createTempDir({
|
|
189
|
-
".opencode/config.yaml": FIXTURE_CONFIG_YAML,
|
|
190
|
-
})
|
|
191
|
-
const ctx = createMockCtx(dir)
|
|
192
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
193
|
-
|
|
194
|
-
const heaps = await profile(ITERATIONS, SNAPSHOTS, async (i) => {
|
|
195
|
-
await hooks.event!({
|
|
196
|
-
event: {
|
|
197
|
-
type: "file.edited",
|
|
198
|
-
properties: { file: join(dir, `src/file-${i % 50}.ts`) },
|
|
199
|
-
} as any,
|
|
200
|
-
})
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
assertBoundedGrowth(heaps, MAX_GROWTH_MB, "file-edited-events")
|
|
204
|
-
await cleanupTempDir(dir)
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
it("disabled plugin doesn't accumulate anything", async () => {
|
|
208
|
-
const dir = await createTempDir({
|
|
209
|
-
".opencode/config.yaml": `vectorizer:\n enabled: false\n auto_index: false\n`,
|
|
210
|
-
})
|
|
211
|
-
const ctx = createMockCtx(dir)
|
|
212
|
-
const hooks = await FileIndexerPlugin(ctx as any)
|
|
213
|
-
|
|
214
|
-
const heaps = await profile(2000, SNAPSHOTS, async () => {
|
|
215
|
-
await hooks.event!({
|
|
216
|
-
event: {
|
|
217
|
-
type: "file.edited",
|
|
218
|
-
properties: { file: join(dir, "src/app.ts") },
|
|
219
|
-
} as any,
|
|
220
|
-
})
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
assertBoundedGrowth(heaps, 10, "disabled-plugin")
|
|
224
|
-
await cleanupTempDir(dir)
|
|
225
|
-
})
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
// =============================================================================
|
|
229
|
-
// 3. VERSION CHECK -- hot path: event handler (no-op)
|
|
230
|
-
// =============================================================================
|
|
231
|
-
describe("leak: version-check", () => {
|
|
232
|
-
const ITERATIONS = 2000
|
|
233
|
-
const SNAPSHOTS = 5
|
|
234
|
-
const MAX_GROWTH_MB = 10
|
|
235
|
-
|
|
236
|
-
it("no-op event handler doesn't leak", async () => {
|
|
237
|
-
const dir = await createTempDir()
|
|
238
|
-
const ctx = createMockCtx(dir)
|
|
239
|
-
const hooks = await VersionCheckPlugin(ctx as any)
|
|
240
|
-
|
|
241
|
-
const heaps = await profile(ITERATIONS, SNAPSHOTS, async () => {
|
|
242
|
-
await hooks.event!({ event: { type: "session.idle" } as any })
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
assertBoundedGrowth(heaps, MAX_GROWTH_MB, "version-noop")
|
|
246
|
-
await cleanupTempDir(dir)
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
it("multiple plugin instances don't leak", async () => {
|
|
250
|
-
const heaps = await profile(100, SNAPSHOTS, async () => {
|
|
251
|
-
const dir = await createTempDir()
|
|
252
|
-
const ctx = createMockCtx(dir)
|
|
253
|
-
const hooks = await VersionCheckPlugin(ctx as any)
|
|
254
|
-
await hooks.event!({ event: { type: "session.idle" } as any })
|
|
255
|
-
await cleanupTempDir(dir)
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
assertBoundedGrowth(heaps, 30, "version-instances")
|
|
259
|
-
})
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
// =============================================================================
|
|
263
|
-
// 4. COMBINED -- simulate a realistic session with all 3 plugins
|
|
264
|
-
// =============================================================================
|
|
265
|
-
describe("leak: combined realistic session", () => {
|
|
266
|
-
it("all 3 plugins running together don't leak", async () => {
|
|
267
|
-
const dir = await createTempDir({
|
|
268
|
-
".opencode/session-state.yaml": FIXTURE_SESSION_STATE,
|
|
269
|
-
".opencode/state/todos.json": FIXTURE_TODOS,
|
|
270
|
-
".opencode/config.yaml": FIXTURE_CONFIG_YAML,
|
|
271
|
-
"docs/sprint-artifacts/sprint-1/stories/story-01-03-jwt-refresh.md":
|
|
272
|
-
FIXTURE_STORY_MD,
|
|
273
|
-
})
|
|
274
|
-
const ctx = createMockCtx(dir)
|
|
275
|
-
|
|
276
|
-
const [compaction, indexer, versionCheck] = await Promise.all([
|
|
277
|
-
CustomCompactionPlugin(ctx as any),
|
|
278
|
-
FileIndexerPlugin(ctx as any),
|
|
279
|
-
VersionCheckPlugin(ctx as any),
|
|
280
|
-
])
|
|
281
|
-
|
|
282
|
-
const heaps = await profile(300, 5, async (i) => {
|
|
283
|
-
// Simulate realistic session cycle:
|
|
284
|
-
// 1. Agent sends message
|
|
285
|
-
await compaction["chat.message"]!(
|
|
286
|
-
{ agent: "dev", sessionID: `s-${i}` } as any,
|
|
287
|
-
{ message: {}, parts: [] } as any,
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
// 2. File gets edited
|
|
291
|
-
await indexer.event!({
|
|
292
|
-
event: {
|
|
293
|
-
type: "file.edited",
|
|
294
|
-
properties: { file: join(dir, `src/module-${i % 20}.ts`) },
|
|
295
|
-
} as any,
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
// 3. Session idle
|
|
299
|
-
await compaction.event!({ event: { type: "session.idle" } as any })
|
|
300
|
-
await versionCheck.event!({ event: { type: "session.idle" } as any })
|
|
301
|
-
|
|
302
|
-
// 4. Every 50 iterations, simulate compaction
|
|
303
|
-
if (i % 50 === 0) {
|
|
304
|
-
const output = { context: [] as string[], prompt: undefined }
|
|
305
|
-
await compaction["experimental.session.compacting"]!(
|
|
306
|
-
{ sessionID: `s-${i}` } as any,
|
|
307
|
-
output as any,
|
|
308
|
-
)
|
|
309
|
-
}
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
assertBoundedGrowth(heaps, 40, "combined-session")
|
|
313
|
-
await cleanupTempDir(dir)
|
|
314
|
-
})
|
|
315
|
-
})
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { readFile } from "fs/promises"
|
|
3
|
-
import { join } from "path"
|
|
4
|
-
import { createTempDir, cleanupTempDir } from "./helpers/mock-ctx"
|
|
5
|
-
import { write, update, read, read_by_id, read_five } from "../../tools/usethis_todo"
|
|
6
|
-
|
|
7
|
-
describe("usethis_todo tool", () => {
|
|
8
|
-
let tempDir: string
|
|
9
|
-
let originalXdg: string | undefined
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
tempDir = await createTempDir()
|
|
13
|
-
originalXdg = process.env.XDG_DATA_HOME
|
|
14
|
-
process.env.XDG_DATA_HOME = join(tempDir, "xdg-data")
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterEach(async () => {
|
|
18
|
-
if (originalXdg === undefined) {
|
|
19
|
-
delete process.env.XDG_DATA_HOME
|
|
20
|
-
} else {
|
|
21
|
-
process.env.XDG_DATA_HOME = originalXdg
|
|
22
|
-
}
|
|
23
|
-
await cleanupTempDir(tempDir)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it("writes enhanced and native todo files", async () => {
|
|
27
|
-
const ctx = { sessionID: "sess-test", directory: tempDir } as any
|
|
28
|
-
const output = await write.execute(
|
|
29
|
-
{
|
|
30
|
-
todos: [
|
|
31
|
-
{ id: "A1", content: "First task", description: "Longer details", status: "todo", priority: "HIGH" },
|
|
32
|
-
{ id: "A2", content: "Second task", status: "todo", priority: "LOW" },
|
|
33
|
-
],
|
|
34
|
-
},
|
|
35
|
-
ctx
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
expect(output).toContain("TODO Graph")
|
|
39
|
-
|
|
40
|
-
const enhancedPath = join(tempDir, ".opencode", "session-todos", "sess-test.json")
|
|
41
|
-
const enhanced = JSON.parse(await readFile(enhancedPath, "utf-8"))
|
|
42
|
-
expect(enhanced.length).toBe(2)
|
|
43
|
-
expect(enhanced[0].content).toBe("First task")
|
|
44
|
-
expect(enhanced[0].description).toBe("Longer details")
|
|
45
|
-
|
|
46
|
-
const nativePath = join(
|
|
47
|
-
process.env.XDG_DATA_HOME!,
|
|
48
|
-
"opencode",
|
|
49
|
-
"storage",
|
|
50
|
-
"todo",
|
|
51
|
-
"sess-test.json"
|
|
52
|
-
)
|
|
53
|
-
const native = JSON.parse(await readFile(nativePath, "utf-8"))
|
|
54
|
-
expect(native.length).toBe(2)
|
|
55
|
-
expect(native[0].content).toContain("First task")
|
|
56
|
-
expect(native[0].content).toContain("Longer details")
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it("update merges by id and can add new tasks", async () => {
|
|
60
|
-
const ctx = { sessionID: "sess-merge", directory: tempDir } as any
|
|
61
|
-
|
|
62
|
-
await write.execute(
|
|
63
|
-
{
|
|
64
|
-
todos: [
|
|
65
|
-
{ id: "A1", content: "First task", description: "v1", status: "todo", priority: "MED" },
|
|
66
|
-
],
|
|
67
|
-
},
|
|
68
|
-
ctx
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
const result = await update.execute(
|
|
72
|
-
{
|
|
73
|
-
todos: [
|
|
74
|
-
{ id: "A1", content: "First task", description: "v2", status: "done", priority: "MED" },
|
|
75
|
-
{ id: "A2", content: "Second task", status: "todo", priority: "LOW" },
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
ctx
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
expect(result).toContain("Updated 2 task(s)")
|
|
82
|
-
|
|
83
|
-
const enhancedPath = join(tempDir, ".opencode", "session-todos", "sess-merge.json")
|
|
84
|
-
const enhanced = JSON.parse(await readFile(enhancedPath, "utf-8"))
|
|
85
|
-
expect(enhanced.length).toBe(2)
|
|
86
|
-
expect(enhanced.find((t: any) => t.id === "A1")?.status).toBe("done")
|
|
87
|
-
expect(enhanced.find((t: any) => t.id === "A1")?.description).toBe("v2")
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it("auto-promotes ready -> done when releases exist", async () => {
|
|
91
|
-
const ctx = { sessionID: "sess-rel", directory: tempDir } as any
|
|
92
|
-
|
|
93
|
-
await write.execute(
|
|
94
|
-
{
|
|
95
|
-
todos: [
|
|
96
|
-
{
|
|
97
|
-
id: "A1",
|
|
98
|
-
content: "Ship it",
|
|
99
|
-
status: "ready",
|
|
100
|
-
priority: "HIGH",
|
|
101
|
-
releases: ["4.38.4"],
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
},
|
|
105
|
-
ctx
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
const enhancedPath = join(tempDir, ".opencode", "session-todos", "sess-rel.json")
|
|
109
|
-
const enhanced = JSON.parse(await readFile(enhancedPath, "utf-8"))
|
|
110
|
-
expect(enhanced.find((t: any) => t.id === "A1")?.status).toBe("done")
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it("read returns graph with content", async () => {
|
|
114
|
-
const ctx = { sessionID: "sess-read", directory: tempDir } as any
|
|
115
|
-
await write.execute(
|
|
116
|
-
{
|
|
117
|
-
todos: [
|
|
118
|
-
{ id: "A1", content: "Task content", status: "todo", priority: "HIGH" },
|
|
119
|
-
],
|
|
120
|
-
},
|
|
121
|
-
ctx
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
const output = await read.execute({}, ctx)
|
|
125
|
-
expect(output).toContain("Task content")
|
|
126
|
-
expect(output).toContain("Available Now")
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it("read_five returns up to 5 available tasks with description", async () => {
|
|
130
|
-
const ctx = { sessionID: "sess-five", directory: tempDir } as any
|
|
131
|
-
await write.execute(
|
|
132
|
-
{
|
|
133
|
-
todos: [
|
|
134
|
-
{ id: "A1", content: "T1", description: "D1", status: "todo", priority: "HIGH", blockedBy: ["B1"] },
|
|
135
|
-
{ id: "A2", content: "T2", status: "todo", priority: "MED" },
|
|
136
|
-
{ id: "A3", content: "T3", status: "todo", priority: "LOW" },
|
|
137
|
-
{ id: "A4", content: "T4", status: "todo", priority: "LOW" },
|
|
138
|
-
{ id: "A5", content: "T5", status: "todo", priority: "LOW" },
|
|
139
|
-
{ id: "A6", content: "T6", status: "todo", priority: "LOW" },
|
|
140
|
-
{ id: "B1", content: "B", status: "in_progress", priority: "LOW", blockedBy: ["C1"] },
|
|
141
|
-
{ id: "C1", content: "C", status: "in_progress", priority: "MED" },
|
|
142
|
-
],
|
|
143
|
-
},
|
|
144
|
-
ctx
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
const out = await read_five.execute({}, ctx)
|
|
148
|
-
expect(out).toContain("Next 5")
|
|
149
|
-
expect(out).toContain("A1")
|
|
150
|
-
expect(out).toContain("T1")
|
|
151
|
-
expect(out).toContain("D1")
|
|
152
|
-
expect(out).toContain("+1 more")
|
|
153
|
-
expect(out).toContain("Blocked By (resolved)")
|
|
154
|
-
expect(out).toContain("B1")
|
|
155
|
-
expect(out).toContain("C1")
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it("read_by_id returns task with resolved blockers", async () => {
|
|
159
|
-
const ctx = { sessionID: "sess-get", directory: tempDir } as any
|
|
160
|
-
await write.execute(
|
|
161
|
-
{
|
|
162
|
-
todos: [
|
|
163
|
-
{
|
|
164
|
-
id: "A1",
|
|
165
|
-
content: "Task content",
|
|
166
|
-
description: "More details",
|
|
167
|
-
status: "todo",
|
|
168
|
-
priority: "HIGH",
|
|
169
|
-
blockedBy: ["B1"],
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
id: "B1",
|
|
173
|
-
content: "Blocker task",
|
|
174
|
-
status: "done",
|
|
175
|
-
priority: "LOW",
|
|
176
|
-
blockedBy: ["C1"],
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
id: "C1",
|
|
180
|
-
content: "Root blocker",
|
|
181
|
-
status: "todo",
|
|
182
|
-
priority: "MED",
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
},
|
|
186
|
-
ctx
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
const out = await read_by_id.execute({ id: "A1" }, ctx)
|
|
190
|
-
expect(out).toContain("Task:")
|
|
191
|
-
expect(out).toContain("A1")
|
|
192
|
-
expect(out).toContain("Task content")
|
|
193
|
-
expect(out).toContain("More details")
|
|
194
|
-
|
|
195
|
-
expect(out).toContain("Blocked By (resolved):")
|
|
196
|
-
expect(out).toContain("B1")
|
|
197
|
-
expect(out).toContain("C1")
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it("read_by_id returns not found", async () => {
|
|
201
|
-
const ctx = { sessionID: "sess-miss", directory: tempDir } as any
|
|
202
|
-
const out = await read_by_id.execute({ id: "NOPE" }, ctx)
|
|
203
|
-
expect(out).toContain("not found")
|
|
204
|
-
})
|
|
205
|
-
})
|