Skip to content

Commit d486295

Browse files
authored
test: AsyncQueue/work utility and State.invalidate coverage (#364)
Add unit tests for AsyncQueue and work() concurrency utility (queue.ts) and State.invalidate (altimate_change in state.ts) — both were completely untested. These tests mitigate risk of race conditions in streaming and stale state after config invalidation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> https://claude.ai/code/session_01AkMKqcoyJ1vURZ4crtpEZu
1 parent dd29651 commit d486295

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

packages/opencode/test/project/state.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, expect, test } from "bun:test"
22

33
import { Instance } from "../../src/project/instance"
4+
import { State } from "../../src/project/state"
45
import { tmpdir } from "../fixture/fixture"
56

67
afterEach(async () => {
@@ -113,3 +114,31 @@ test("Instance.state dedupes concurrent promise initialization", async () => {
113114
expect(a).toBe(b)
114115
expect(n).toBe(1)
115116
})
117+
118+
test("State.invalidate removes cached entry for re-initialization", async () => {
119+
await using tmp = await tmpdir()
120+
let n = 0
121+
const init = () => ({ n: ++n })
122+
const state = Instance.state(init)
123+
124+
const a = await Instance.provide({
125+
directory: tmp.path,
126+
fn: async () => state(),
127+
})
128+
expect(a.n).toBe(1)
129+
130+
// Invalidate the cached entry so next access re-initializes
131+
State.invalidate(tmp.path, init)
132+
133+
const b = await Instance.provide({
134+
directory: tmp.path,
135+
fn: async () => state(),
136+
})
137+
expect(b.n).toBe(2)
138+
expect(a).not.toBe(b)
139+
})
140+
141+
test("State.invalidate on nonexistent key is a no-op", () => {
142+
// Should not throw
143+
State.invalidate("/nonexistent/path", () => {})
144+
})
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { AsyncQueue, work } from "../../src/util/queue"
3+
4+
describe("AsyncQueue", () => {
5+
test("push before next resolves immediately", async () => {
6+
const q = new AsyncQueue<number>()
7+
q.push(1)
8+
q.push(2)
9+
const a = await q.next()
10+
const b = await q.next()
11+
expect(a).toBe(1)
12+
expect(b).toBe(2)
13+
})
14+
15+
test("next before push waits for value", async () => {
16+
const q = new AsyncQueue<string>()
17+
const promise = q.next()
18+
q.push("hello")
19+
expect(await promise).toBe("hello")
20+
})
21+
22+
test("multiple waiters resolve in order", async () => {
23+
const q = new AsyncQueue<number>()
24+
const p1 = q.next()
25+
const p2 = q.next()
26+
q.push(10)
27+
q.push(20)
28+
expect(await p1).toBe(10)
29+
expect(await p2).toBe(20)
30+
})
31+
32+
test("async iterator yields pushed values", async () => {
33+
const q = new AsyncQueue<number>()
34+
const collected: number[] = []
35+
36+
q.push(1)
37+
q.push(2)
38+
q.push(3)
39+
40+
let count = 0
41+
for await (const val of q) {
42+
collected.push(val)
43+
count++
44+
if (count === 3) break
45+
}
46+
expect(collected).toEqual([1, 2, 3])
47+
})
48+
49+
test("interleaved push and next", async () => {
50+
const q = new AsyncQueue<number>()
51+
q.push(1)
52+
expect(await q.next()).toBe(1)
53+
const p = q.next() // waiting
54+
q.push(2)
55+
expect(await p).toBe(2)
56+
q.push(3)
57+
q.push(4)
58+
expect(await q.next()).toBe(3)
59+
expect(await q.next()).toBe(4)
60+
})
61+
})
62+
63+
describe("work", () => {
64+
test("processes all items", async () => {
65+
const results: number[] = []
66+
await work(2, [1, 2, 3, 4, 5], async (item) => {
67+
results.push(item)
68+
})
69+
expect(results.sort()).toEqual([1, 2, 3, 4, 5])
70+
})
71+
72+
test("respects concurrency limit", async () => {
73+
let active = 0
74+
let maxActive = 0
75+
await work(2, [1, 2, 3, 4, 5], async () => {
76+
active++
77+
maxActive = Math.max(maxActive, active)
78+
await Bun.sleep(10)
79+
active--
80+
})
81+
expect(maxActive).toBeLessThanOrEqual(2)
82+
})
83+
84+
test("handles empty items array", async () => {
85+
let called = false
86+
await work(3, [], async () => {
87+
called = true
88+
})
89+
expect(called).toBe(false)
90+
})
91+
92+
test("concurrency of 1 processes sequentially (LIFO due to pop)", async () => {
93+
const order: number[] = []
94+
await work(1, [1, 2, 3], async (item) => {
95+
order.push(item)
96+
})
97+
// work() uses pending.pop(), so items are processed in reverse order
98+
expect(order).toEqual([3, 2, 1])
99+
})
100+
101+
test("propagates errors from worker", async () => {
102+
await expect(
103+
work(2, [1, 2, 3], async (item) => {
104+
if (item === 2) throw new Error("boom")
105+
}),
106+
).rejects.toThrow("boom")
107+
})
108+
})

0 commit comments

Comments
 (0)