|
| 1 | +import type { Message, VatOneResolution } from '@agoric/swingset-liveslots'; |
| 2 | +import type { CapData } from '@endo/marshal'; |
| 3 | +import { makePromiseKit } from '@endo/promise-kit'; |
| 4 | +import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| 5 | +import type { MockInstance } from 'vitest'; |
| 6 | + |
| 7 | +import { KernelQueue } from './KernelQueue.ts'; |
| 8 | +import type { KernelStore } from './store/index.ts'; |
| 9 | +import type { |
| 10 | + KRef, |
| 11 | + RunQueueItem, |
| 12 | + RunQueueItemNotify, |
| 13 | + RunQueueItemSend, |
| 14 | +} from './types.ts'; |
| 15 | + |
| 16 | +vi.mock('./services/garbage-collection.ts', () => ({ |
| 17 | + processGCActionSet: vi.fn().mockReturnValue(null), |
| 18 | +})); |
| 19 | + |
| 20 | +vi.mock('@endo/promise-kit', () => ({ |
| 21 | + makePromiseKit: vi.fn(), |
| 22 | +})); |
| 23 | + |
| 24 | +describe('KernelQueue', () => { |
| 25 | + let kernelStore: KernelStore; |
| 26 | + let kernelQueue: KernelQueue; |
| 27 | + let mockPromiseKit: ReturnType<typeof makePromiseKit>; |
| 28 | + |
| 29 | + beforeEach(() => { |
| 30 | + mockPromiseKit = { |
| 31 | + promise: Promise.resolve(), |
| 32 | + resolve: vi.fn(), |
| 33 | + reject: vi.fn(), |
| 34 | + }; |
| 35 | + (makePromiseKit as unknown as MockInstance).mockReturnValue(mockPromiseKit); |
| 36 | + kernelStore = { |
| 37 | + nextTerminatedVatCleanup: vi.fn(), |
| 38 | + collectGarbage: vi.fn(), |
| 39 | + runQueueLength: vi.fn(), |
| 40 | + dequeueRun: vi.fn(), |
| 41 | + enqueueRun: vi.fn(), |
| 42 | + initKernelPromise: vi.fn().mockReturnValue(['kp1']), |
| 43 | + incrementRefCount: vi.fn(), |
| 44 | + getKernelPromise: vi.fn(), |
| 45 | + resolveKernelPromise: vi.fn(), |
| 46 | + nextReapAction: vi.fn().mockReturnValue(null), |
| 47 | + getGCActions: vi.fn().mockReturnValue([]), |
| 48 | + } as unknown as KernelStore; |
| 49 | + |
| 50 | + kernelQueue = new KernelQueue(kernelStore); |
| 51 | + }); |
| 52 | + |
| 53 | + describe('run', () => { |
| 54 | + it('processes items from the run queue and performs cleanup', async () => { |
| 55 | + const mockItem: RunQueueItem = { |
| 56 | + type: 'send', |
| 57 | + target: 'ko123', |
| 58 | + message: {} as Message, |
| 59 | + }; |
| 60 | + ( |
| 61 | + kernelStore.runQueueLength as unknown as MockInstance |
| 62 | + ).mockReturnValueOnce(1); |
| 63 | + (kernelStore.dequeueRun as unknown as MockInstance).mockReturnValue( |
| 64 | + mockItem, |
| 65 | + ); |
| 66 | + const deliverError = new Error('stop'); |
| 67 | + const deliver = vi.fn().mockRejectedValue(deliverError); |
| 68 | + await expect(kernelQueue.run(deliver)).rejects.toBe(deliverError); |
| 69 | + expect(kernelStore.nextTerminatedVatCleanup).toHaveBeenCalled(); |
| 70 | + expect(deliver).toHaveBeenCalledWith(mockItem); |
| 71 | + }); |
| 72 | + }); |
| 73 | + |
| 74 | + describe('enqueueMessage', () => { |
| 75 | + it('creates a message, enqueues it, and returns a promise for the result', async () => { |
| 76 | + const target = 'ko123'; |
| 77 | + const method = 'test'; |
| 78 | + const args = ['arg1', { key: 'value' }]; |
| 79 | + const resultValue = { body: 'result', slots: [] }; |
| 80 | + let resolvePromise = (_value: CapData<KRef>): void => { |
| 81 | + // do nothing |
| 82 | + }; |
| 83 | + const resultPromiseRaw = new Promise<CapData<KRef>>((resolve) => { |
| 84 | + resolvePromise = resolve; |
| 85 | + }); |
| 86 | + const successPromiseKit = { |
| 87 | + promise: resultPromiseRaw, |
| 88 | + resolve: resolvePromise, |
| 89 | + }; |
| 90 | + (makePromiseKit as unknown as MockInstance).mockReturnValueOnce( |
| 91 | + successPromiseKit, |
| 92 | + ); |
| 93 | + const resultPromise = kernelQueue.enqueueMessage(target, method, args); |
| 94 | + expect(kernelStore.initKernelPromise).toHaveBeenCalled(); |
| 95 | + expect(kernelStore.incrementRefCount).toHaveBeenCalledWith( |
| 96 | + target, |
| 97 | + 'queue|target', |
| 98 | + ); |
| 99 | + expect(kernelStore.incrementRefCount).toHaveBeenCalledWith( |
| 100 | + 'kp1', |
| 101 | + 'queue|result', |
| 102 | + ); |
| 103 | + expect(kernelStore.enqueueRun).toHaveBeenCalledWith({ |
| 104 | + type: 'send', |
| 105 | + target, |
| 106 | + message: expect.objectContaining({ |
| 107 | + methargs: expect.anything(), |
| 108 | + result: 'kp1', |
| 109 | + }), |
| 110 | + }); |
| 111 | + expect(kernelQueue.subscriptions.has('kp1')).toBe(true); |
| 112 | + const handler = kernelQueue.subscriptions.get('kp1'); |
| 113 | + expect(handler).toBeDefined(); |
| 114 | + resolvePromise(resultValue); |
| 115 | + const result = await resultPromise; |
| 116 | + expect(result).toStrictEqual(resultValue); |
| 117 | + }); |
| 118 | + }); |
| 119 | + |
| 120 | + describe('enqueueRun', () => { |
| 121 | + it('adds an item to the run queue', () => { |
| 122 | + const item: RunQueueItemSend = { |
| 123 | + type: 'send', |
| 124 | + target: 'ko123', |
| 125 | + message: {} as Message, |
| 126 | + }; |
| 127 | + (kernelStore.runQueueLength as unknown as MockInstance).mockReturnValue( |
| 128 | + 0, |
| 129 | + ); |
| 130 | + kernelQueue.enqueueRun(item); |
| 131 | + expect(kernelStore.enqueueRun).toHaveBeenCalledWith(item); |
| 132 | + }); |
| 133 | + }); |
| 134 | + |
| 135 | + describe('enqueueNotify', () => { |
| 136 | + it('creates a notify item and adds it to the run queue', () => { |
| 137 | + const vatId = 'v1'; |
| 138 | + const kpid = 'kp123'; |
| 139 | + kernelQueue.enqueueNotify(vatId, kpid); |
| 140 | + const expectedNotifyItem: RunQueueItemNotify = { |
| 141 | + type: 'notify', |
| 142 | + vatId, |
| 143 | + kpid, |
| 144 | + }; |
| 145 | + expect(kernelStore.enqueueRun).toHaveBeenCalledWith(expectedNotifyItem); |
| 146 | + expect(kernelStore.incrementRefCount).toHaveBeenCalledWith( |
| 147 | + kpid, |
| 148 | + 'notify', |
| 149 | + ); |
| 150 | + }); |
| 151 | + }); |
| 152 | + |
| 153 | + describe('resolvePromises', () => { |
| 154 | + it('resolves kernel promises and notifies subscribers', () => { |
| 155 | + const vatId = 'v1'; |
| 156 | + const kpid = 'kp123'; |
| 157 | + const resolution: VatOneResolution = [ |
| 158 | + kpid, |
| 159 | + false, |
| 160 | + { body: 'resolved value', slots: ['slot1'] } as CapData<KRef>, |
| 161 | + ]; |
| 162 | + (kernelStore.getKernelPromise as unknown as MockInstance).mockReturnValue( |
| 163 | + { |
| 164 | + state: 'unresolved', |
| 165 | + decider: vatId, |
| 166 | + subscribers: ['v2', 'v3'], |
| 167 | + }, |
| 168 | + ); |
| 169 | + const resolveHandler = vi.fn(); |
| 170 | + kernelQueue.subscriptions.set(kpid, resolveHandler); |
| 171 | + kernelQueue.resolvePromises(vatId, [resolution]); |
| 172 | + expect(kernelStore.incrementRefCount).toHaveBeenCalledWith( |
| 173 | + kpid, |
| 174 | + 'resolve|kpid', |
| 175 | + ); |
| 176 | + expect(kernelStore.incrementRefCount).toHaveBeenCalledWith( |
| 177 | + 'slot1', |
| 178 | + 'resolve|slot', |
| 179 | + ); |
| 180 | + expect(kernelStore.enqueueRun).toHaveBeenCalledWith({ |
| 181 | + type: 'notify', |
| 182 | + vatId: 'v2', |
| 183 | + kpid, |
| 184 | + }); |
| 185 | + expect(kernelStore.enqueueRun).toHaveBeenCalledWith({ |
| 186 | + type: 'notify', |
| 187 | + vatId: 'v3', |
| 188 | + kpid, |
| 189 | + }); |
| 190 | + expect(kernelStore.resolveKernelPromise).toHaveBeenCalledWith( |
| 191 | + kpid, |
| 192 | + false, |
| 193 | + { body: 'resolved value', slots: ['slot1'] }, |
| 194 | + ); |
| 195 | + expect(resolveHandler).toHaveBeenCalledWith({ |
| 196 | + body: 'resolved value', |
| 197 | + slots: ['slot1'], |
| 198 | + }); |
| 199 | + expect(kernelQueue.subscriptions.has(kpid)).toBe(false); |
| 200 | + }); |
| 201 | + |
| 202 | + it('throws error if a promise is already resolved', () => { |
| 203 | + const vatId = 'v1'; |
| 204 | + const kpid = 'kp123'; |
| 205 | + const resolution: VatOneResolution = [ |
| 206 | + kpid, |
| 207 | + false, |
| 208 | + { body: 'resolved value', slots: [] } as CapData<KRef>, |
| 209 | + ]; |
| 210 | + (kernelStore.getKernelPromise as unknown as MockInstance).mockReturnValue( |
| 211 | + { |
| 212 | + state: 'fulfilled', |
| 213 | + decider: vatId, |
| 214 | + }, |
| 215 | + ); |
| 216 | + expect(() => kernelQueue.resolvePromises(vatId, [resolution])).toThrow( |
| 217 | + '"kp123" was already resolved', |
| 218 | + ); |
| 219 | + }); |
| 220 | + |
| 221 | + it('throws error if the resolver is not the decider', () => { |
| 222 | + const vatId = 'v1'; |
| 223 | + const wrongVatId = 'v2'; |
| 224 | + const kpid = 'kp123'; |
| 225 | + const resolution: VatOneResolution = [ |
| 226 | + kpid, |
| 227 | + false, |
| 228 | + { body: 'resolved value', slots: [] } as CapData<KRef>, |
| 229 | + ]; |
| 230 | + (kernelStore.getKernelPromise as unknown as MockInstance).mockReturnValue( |
| 231 | + { |
| 232 | + state: 'unresolved', |
| 233 | + decider: wrongVatId, |
| 234 | + }, |
| 235 | + ); |
| 236 | + expect(() => kernelQueue.resolvePromises(vatId, [resolution])).toThrow( |
| 237 | + '"v1" not permitted to resolve "kp123" because "its decider is v2"', |
| 238 | + ); |
| 239 | + }); |
| 240 | + }); |
| 241 | +}); |
0 commit comments