返回 oh-my-ppt
resource-lock.test.ts
根目录 / tests / unit / agent-runtime / resource-lock.test.ts
1 import { describe, expect, it, vi } from 'vitest'
2 import { ResourceLock } from '../../../src/main/agent-runtime/lock/resource-lock'
3
4 describe('ResourceLock', () => {
5 it('shares reads and blocks writers until every reader releases', async () => {
6 const lock = new ResourceLock()
7 const readerOne = await lock.acquire({ read: ['session:1'] }, { ownerToken: Symbol('reader-1'), wait: 'block' })
8 const readerTwo = await lock.acquire({ read: ['session:1'] }, { ownerToken: Symbol('reader-2'), wait: 'block' })
9 if (!readerOne || !readerTwo) throw new Error('expected read leases')
10
11 let writerGranted = false
12 const writer = lock.acquire(
13 { write: ['session:1'] },
14 { ownerToken: Symbol('writer'), wait: 'block' }
15 )
16 writer.then(() => {
17 writerGranted = true
18 })
19
20 await Promise.resolve()
21 expect(writerGranted).toBe(false)
22
23 readerOne()
24 await Promise.resolve()
25 expect(writerGranted).toBe(false)
26
27 readerTwo()
28 const writerRelease = await writer
29 expect(writerRelease).toBeTypeOf('function')
30 writerRelease?.()
31 })
32
33 it('normalizes duplicate keys and treats a read/write claim as a write', async () => {
34 const lock = new ResourceLock()
35 const lease = await lock.acquire(
36 { read: ['session:1', 'session:1'], write: ['session:1', 'session:1'] },
37 { ownerToken: Symbol('writer'), wait: 'block' }
38 )
39 if (!lease) throw new Error('expected write lease')
40
41 await expect(
42 lock.acquire({ read: ['session:1'] }, { ownerToken: Symbol('reader'), wait: 'fail' })
43 ).resolves.toBeNull()
44
45 lease()
46 })
47
48 it('uses a global queue: unrelated work bypasses a blocked multi-key claim', async () => {
49 const lock = new ResourceLock()
50 const held = await lock.acquire({ write: ['resource:b'] }, { ownerToken: Symbol('held'), wait: 'block' })
51 if (!held) throw new Error('expected held lease')
52
53 const multiKey = lock.acquire(
54 { write: ['resource:a', 'resource:b'] },
55 { ownerToken: Symbol('multi-key'), wait: 'block' }
56 )
57 const unrelated = await lock.acquire(
58 { write: ['resource:c'] },
59 { ownerToken: Symbol('unrelated'), wait: 'block' }
60 )
61 if (!unrelated) throw new Error('expected unrelated lease to bypass')
62
63 unrelated()
64 held()
65 const multiKeyRelease = await multiKey
66 expect(multiKeyRelease).toBeTypeOf('function')
67 multiKeyRelease?.()
68 })
69
70 it('does not let a later waiter cross any key of an earlier blocked multi-key claim', async () => {
71 const lock = new ResourceLock()
72 const held = await lock.acquire(
73 { write: ['resource:b'] },
74 { ownerToken: Symbol('held-b'), wait: 'block' }
75 )
76 if (!held) throw new Error('expected held lease')
77
78 const multiKey = lock.acquire(
79 { write: ['resource:a', 'resource:b'] },
80 { ownerToken: Symbol('multi-key'), wait: 'block' }
81 )
82 let laterGranted = false
83 const laterA = lock.acquire(
84 { write: ['resource:a'] },
85 { ownerToken: Symbol('later-a'), wait: 'block' }
86 )
87 laterA.then(() => {
88 laterGranted = true
89 })
90
91 await Promise.resolve()
92 expect(laterGranted).toBe(false)
93
94 held()
95 const multiKeyRelease = await multiKey
96 expect(laterGranted).toBe(false)
97 multiKeyRelease?.()
98
99 const laterARelease = await laterA
100 laterARelease?.()
101 })
102
103 it('does not let a later reader bypass an earlier writer for the same key', async () => {
104 const lock = new ResourceLock()
105 const initialReader = await lock.acquire(
106 { read: ['session:1'] },
107 { ownerToken: Symbol('initial-reader'), wait: 'block' }
108 )
109 if (!initialReader) throw new Error('expected initial reader lease')
110
111 const writer = lock.acquire(
112 { write: ['session:1'] },
113 { ownerToken: Symbol('writer'), wait: 'block' }
114 )
115 let laterReaderGranted = false
116 const laterReader = lock.acquire(
117 { read: ['session:1'] },
118 { ownerToken: Symbol('later-reader'), wait: 'block' }
119 )
120 laterReader.then(() => {
121 laterReaderGranted = true
122 })
123
124 initialReader()
125 const writerRelease = await writer
126 expect(laterReaderGranted).toBe(false)
127 writerRelease?.()
128
129 const laterReaderRelease = await laterReader
130 expect(laterReaderRelease).toBeTypeOf('function')
131 laterReaderRelease?.()
132 })
133
134 it('removes an aborted waiter and drains remaining work', async () => {
135 const lock = new ResourceLock()
136 const held = await lock.acquire({ write: ['session:1'] }, { ownerToken: Symbol('held'), wait: 'block' })
137 if (!held) throw new Error('expected held lease')
138
139 const controller = new AbortController()
140 const aborted = lock.acquire(
141 { write: ['session:1'] },
142 { ownerToken: Symbol('aborted'), signal: controller.signal, wait: 'block' }
143 )
144 controller.abort()
145
146 await expect(aborted).rejects.toMatchObject({ name: 'AbortError' })
147 held()
148
149 const next = await lock.acquire({ write: ['session:1'] }, { ownerToken: Symbol('next'), wait: 'block' })
150 expect(next).toBeTypeOf('function')
151 next?.()
152 })
153
154 it('rejects nested claims for the same owner and makes release idempotent', async () => {
155 const lock = new ResourceLock()
156 const ownerToken = Symbol('owner')
157 const lease = await lock.acquire({ write: ['session:1'] }, { ownerToken, wait: 'block' })
158 if (!lease) throw new Error('expected lease')
159
160 await expect(lock.acquire({ write: ['session:2'] }, { ownerToken, wait: 'block' })).rejects.toThrow(
161 'does not support nested claims'
162 )
163
164 lease()
165 lease()
166
167 const next = await lock.acquire({ write: ['session:1'] }, { ownerToken, wait: 'block' })
168 next?.()
169 })
170
171 it('treats an earlier conflicting waiter as busy for fail-fast claims', async () => {
172 const lock = new ResourceLock()
173 const held = await lock.acquire({ write: ['session:1'] }, { ownerToken: Symbol('held'), wait: 'block' })
174 if (!held) throw new Error('expected held lease')
175
176 const waiting = lock.acquire(
177 { write: ['session:1'] },
178 { ownerToken: Symbol('waiting'), wait: 'block' }
179 )
180 await expect(
181 lock.acquire({ read: ['session:1'] }, { ownerToken: Symbol('fail-fast'), wait: 'fail' })
182 ).resolves.toBeNull()
183
184 held()
185 const waitingRelease = await waiting
186 waitingRelease?.()
187 })
188 })
189
189 lines TYPESCRIPT