chore(stage-ui): lint and timeout constraint

This commit is contained in:
Neko Ayaka
2026-03-23 13:45:59 +08:00
parent dc515af0cd
commit 2c4ac66b61
4 changed files with 142 additions and 26 deletions
@@ -0,0 +1,93 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { reactive, toRefs } from 'vue'
const stream = vi.fn()
const getProviderInstance = vi.fn()
vi.mock('pinia', async () => {
const actual = await vi.importActual<typeof import('pinia')>('pinia')
return {
...actual,
storeToRefs: (store: object) => toRefs(store as never),
}
})
vi.mock('../../stores/llm', () => ({
useLLM: () => ({
stream,
}),
}))
vi.mock('../../stores/providers', () => ({
useProvidersStore: () => ({
getProviderInstance,
}),
}))
vi.mock('../../stores/modules/vision', () => ({
useVisionStore: () => reactive({
activeProvider: 'mock-provider',
activeModel: 'mock-model',
ollamaThinkingEnabled: false,
}),
}))
vi.mock('./use-vision-workloads', () => ({
getVisionWorkload: () => ({
prompt: 'Interpret this frame',
}),
}))
describe('useVisionInference', () => {
beforeEach(() => {
vi.useFakeTimers()
stream.mockReset()
getProviderInstance.mockReset()
getProviderInstance.mockResolvedValue({
chat: vi.fn().mockReturnValue({
apiKey: 'test-key',
baseURL: 'https://example.com/v1/',
}),
})
})
afterEach(() => {
vi.useRealTimers()
})
it('passes an abort signal to llmStore.stream', async () => {
stream.mockImplementation(async (_model, _provider, _messages, options) => {
expect(options?.abortSignal).toBeInstanceOf(AbortSignal)
options?.onStreamEvent?.({ type: 'text-delta', text: 'Frame summary' })
})
const { useVisionInference } = await import('./use-vision-inference')
const { runVisionInference } = useVisionInference()
await expect(runVisionInference({
imageDataUrl: 'data:image/png;base64,Zm9v',
workloadId: 'screen:interpret',
})).resolves.toBe('Frame summary')
})
it('aborts vision inference when the stream never settles', async () => {
stream.mockImplementation((_model, _provider, _messages, options) => new Promise((_, reject) => {
options?.abortSignal?.addEventListener('abort', () => {
reject(options.abortSignal?.reason)
}, { once: true })
}))
const { useVisionInference } = await import('./use-vision-inference')
const { runVisionInference } = useVisionInference()
const result = runVisionInference({
imageDataUrl: 'data:image/png;base64,Zm9v',
workloadId: 'screen:interpret',
})
const expectation = expect(result).rejects.toThrow('Vision inference timed out after 15000ms')
await vi.advanceTimersByTimeAsync(15_000)
await expectation
})
})
@@ -17,6 +17,9 @@ export interface VisionInferenceInput {
promptOverride?: string
}
// TODO: this should be configurable
const VISION_INFERENCE_TIMEOUT_MS = 60_000
function parseDataUrl(dataUrl: string) {
if (!dataUrl.startsWith('data:'))
return { mimeType: 'image/png', base64: dataUrl, url: dataUrl }
@@ -74,13 +77,32 @@ export function useVisionInference() {
]
let buffer = ''
await llmStore.stream(activeModel.value, visionProvider, messages, {
onStreamEvent: (event) => {
if (event.type === 'text-delta') {
buffer += event.text
}
},
})
const abortController = new AbortController()
const timeoutHandle = setTimeout(() => {
abortController.abort(new Error(`Vision inference timed out after ${VISION_INFERENCE_TIMEOUT_MS}ms`))
}, VISION_INFERENCE_TIMEOUT_MS)
try {
await llmStore.stream(activeModel.value, visionProvider, messages, {
abortSignal: abortController.signal,
onStreamEvent: (event) => {
if (event.type === 'text-delta') {
buffer += event.text
}
},
})
}
catch (error) {
if (abortController.signal.aborted) {
throw abortController.signal.reason instanceof Error
? abortController.signal.reason
: new Error(`Vision inference timed out after ${VISION_INFERENCE_TIMEOUT_MS}ms`)
}
throw error
}
finally {
clearTimeout(timeoutHandle)
}
lastText.value = buffer.trim()
return lastText.value
@@ -1,3 +1,4 @@
import { errorMessageFrom } from '@moeru/std'
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
@@ -112,7 +113,7 @@ export const useVisionProcessingStore = defineStore('vision-processing', () => {
recordContextUpdates(outcome.contextUpdates)
}
catch (error) {
lastError.value = error instanceof Error ? error.message : String(error)
lastError.value = errorMessageFrom(error) || 'Unknown error'
}
finally {
recordProcessingDuration(performance.now() - start)
+18 -18
View File
@@ -1,3 +1,5 @@
catalogMode: prefer
shellEmulator: true
packages:
- packages/**
- plugins/**
@@ -7,6 +9,22 @@ packages:
- docs/**
- apps/**
- '!**/dist/**'
overrides:
array-flatten: npm:@nolyfill/array-flatten@^1.0.44
axios: npm:feaxios@^0.0.23
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
isarray: npm:@nolyfill/isarray@^1.0.44
onnxruntime-web: npm:onnxruntime-web@^1.24.3
safe-buffer: npm:@nolyfill/safe-buffer@^1.0.44
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
side-channel: npm:@nolyfill/side-channel@^1.0.44
string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44
patchedDependencies:
'@mediapipe/tasks-vision': patches/@mediapipe__tasks-vision.patch
mineflayer-pathfinder: patches/mineflayer-pathfinder.patch
mineflayer@4.33.0: patches/mineflayer@4.33.0.patch
pixi-live2d-display: patches/pixi-live2d-display.patch
srvx: patches/srvx.patch
catalog:
'@capacitor/android': ^8.2.0
'@capacitor/cli': ^8.2.0
@@ -89,7 +107,6 @@ catalog:
xsschema: 0.4.0-beta.13
yaml: ^2.8.3
zod: ^4.3.6
catalogMode: prefer
catalogs:
vitest:
'@vitest/browser-playwright': ^4.1.0
@@ -127,16 +144,6 @@ onlyBuiltDependencies:
- spawn-sync
- utf-8-validate
- vue-demi
overrides:
array-flatten: npm:@nolyfill/array-flatten@^1.0.44
axios: npm:feaxios@^0.0.23
is-core-module: npm:@nolyfill/is-core-module@^1.0.39
isarray: npm:@nolyfill/isarray@^1.0.44
onnxruntime-web: npm:onnxruntime-web@^1.24.3
safe-buffer: npm:@nolyfill/safe-buffer@^1.0.44
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
side-channel: npm:@nolyfill/side-channel@^1.0.44
string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44
packageExtensions:
'@formkit/auto-animate':
peerDependencies:
@@ -168,10 +175,3 @@ packageExtensions:
'vue-sonner':
peerDependencies:
vue: '^3.2.0'
patchedDependencies:
'@mediapipe/tasks-vision': patches/@mediapipe__tasks-vision.patch
mineflayer-pathfinder: patches/mineflayer-pathfinder.patch
mineflayer@4.33.0: patches/mineflayer@4.33.0.patch
pixi-live2d-display: patches/pixi-live2d-display.patch
srvx: patches/srvx.patch
shellEmulator: true