diff --git a/.github/workflows/ai-unit-test.yml b/.github/workflows/ai-unit-test.yml index f9e223c45e..e01fe473dc 100644 --- a/.github/workflows/ai-unit-test.yml +++ b/.github/workflows/ai-unit-test.yml @@ -73,9 +73,9 @@ jobs: run: pnpm exec nx run-many --target=build --projects="@midscene/report,@midscene/core,@midscene/web,@midscene/cli" - name: Run tests with coverage - # Worker caps live in each package's own test config so the flags stay - # runner-agnostic: rstest rejects bare --minWorkers/--maxWorkers, so they - # must not be passed here. + # Worker caps live in the core/web-integration/cli rstest.config.ts files: + # rstest rejects bare --minWorkers/--maxWorkers, so they must not be + # passed here. run: MIDSCENE_COVERAGE_DIR=coverage-ai pnpm exec nx run-many --target=test:ai --projects=@midscene/core,@midscene/web,@midscene/cli --verbose -- --coverage id: test-ai continue-on-error: true diff --git a/packages/core/package.json b/packages/core/package.json index 71e07555d2..7eff2ea904 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -7,6 +7,18 @@ "main": "./dist/lib/index.js", "types": "./dist/types/index.d.ts", "module": "./dist/es/index.mjs", + "imports": { + "#proxy-deps": { + "node": { + "import": "./dist/es/ai-model/service-caller/proxy-deps.node.mjs", + "require": "./dist/lib/ai-model/service-caller/proxy-deps.node.js" + }, + "default": { + "import": "./dist/es/ai-model/service-caller/proxy-deps.stub.mjs", + "require": "./dist/lib/ai-model/service-caller/proxy-deps.stub.js" + } + } + }, "files": ["dist", "README.md"], "exports": { ".": { @@ -90,11 +102,10 @@ "build": "rslib build", "build:watch": "USE_DEV_REPORT=1 rslib build --watch --no-clean", "sync-report-template": "node ../../scripts/sync-core-report-template.mjs", - "test": "vitest --run", - "test:u": "vitest --run -u", - "test:ai": "AITEST=true vitest --run", - "computer": "TEST_COMPUTER=true AITEST=true vitest --run tests/ai/evaluate/computer.test.ts", - "test:parse-action": "vitest --run tests/unit-test/parse-action.test.ts" + "test": "rstest", + "test:u": "rstest -u", + "test:ai": "AITEST=true rstest", + "test:parse-action": "rstest tests/unit-test/parse-action.test.ts" }, "nx": { "targets": { @@ -131,15 +142,14 @@ }, "devDependencies": { "@rslib/core": "^0.18.3", + "@rstest/core": "0.11.5", "@types/js-yaml": "4.0.9", "@types/node": "^18.0.0", "@types/node-fetch": "2.6.11", "@types/semver": "7.7.0", - "@vitest/runner": "3.0.5", "langsmith": "^0.3.74", "sharp": "^0.34.3", - "typescript": "^5.8.3", - "vitest": "3.0.5" + "typescript": "^5.8.3" }, "engines": { "node": ">=18.19.0" diff --git a/packages/core/rslib.config.ts b/packages/core/rslib.config.ts index 61918c7387..60e123d084 100644 --- a/packages/core/rslib.config.ts +++ b/packages/core/rslib.config.ts @@ -61,6 +61,7 @@ export default defineConfig({ }, }, output: { + externals: ['#proxy-deps', 'undici', 'fetch-socks'], sourceMap: true, }, plugins: [createTypeCheckPlugin(), writeExistingReportTemplate()], diff --git a/packages/core/rstest.config.ts b/packages/core/rstest.config.ts new file mode 100644 index 0000000000..aaff087789 --- /dev/null +++ b/packages/core/rstest.config.ts @@ -0,0 +1,47 @@ +import path from 'node:path'; +import { defineConfig } from '@rstest/core'; +import dotenv from 'dotenv'; +import { createCoverageConfig } from '../../scripts/rstest-coverage'; +import { defineVersion, photonExternal } from '../../scripts/rstest-shared'; +import { version } from './package.json'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +dotenv.config({ + path: path.join(__dirname, '../../.env'), + override: true, +}); + +const enableAiTest = Boolean(process.env.AITEST); +const basicTest = ['tests/unit-test/**/*.test.ts']; + +export default defineConfig({ + coverage: createCoverageConfig(__dirname), + include: enableAiTest ? ['tests/ai/**/*.test.ts'] : basicTest, + retry: process.env.CI ? 1 : 0, + ...(enableAiTest && process.env.CI ? { pool: { maxWorkers: 4 } } : {}), + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + // Tests must not require a prior package build: route the conditional + // '#proxy-deps' subpath import to the Node source implementation. + '#proxy-deps': path.resolve( + __dirname, + 'src/ai-model/service-caller/proxy-deps.node.ts', + ), + }, + }, + source: { + define: { + ...defineVersion(version), + __DEV_REPORT_PATH__: JSON.stringify( + path.resolve(__dirname, '../../apps/report/dist/index.html'), + ), + }, + }, + output: { + externals: photonExternal, + }, +}); diff --git a/packages/core/src/ai-model/service-caller/index.ts b/packages/core/src/ai-model/service-caller/index.ts index 162909f568..a553518036 100644 --- a/packages/core/src/ai-model/service-caller/index.ts +++ b/packages/core/src/ai-model/service-caller/index.ts @@ -208,9 +208,8 @@ export async function createChatClient({ 'HTTP proxy is configured but not supported in browser environment', ); } else { - // Dynamic import with variable to avoid bundler static analysis - const moduleName = 'undici'; - const { ProxyAgent } = await import(moduleName); + const { loadUndici } = await import('#proxy-deps'); + const { ProxyAgent } = await loadUndici(); proxyAgent = new ProxyAgent({ uri: httpProxy, // Note: authentication is handled via the URI (e.g., http://user:pass@proxy.com:8080) @@ -224,9 +223,8 @@ export async function createChatClient({ ); } else { try { - // Dynamic import with variable to avoid bundler static analysis - const moduleName = 'fetch-socks'; - const { socksDispatcher } = await import(moduleName); + const { loadFetchSocks } = await import('#proxy-deps'); + const { socksDispatcher } = await loadFetchSocks(); // Parse SOCKS proxy URL (e.g., socks5://127.0.0.1:1080) const proxyUrl = new URL(socksProxy); diff --git a/packages/core/src/ai-model/service-caller/proxy-deps.d.ts b/packages/core/src/ai-model/service-caller/proxy-deps.d.ts new file mode 100644 index 0000000000..b98a6f84cd --- /dev/null +++ b/packages/core/src/ai-model/service-caller/proxy-deps.d.ts @@ -0,0 +1,4 @@ +declare module '#proxy-deps' { + export function loadUndici(): Promise; + export function loadFetchSocks(): Promise; +} diff --git a/packages/core/src/ai-model/service-caller/proxy-deps.node.ts b/packages/core/src/ai-model/service-caller/proxy-deps.node.ts new file mode 100644 index 0000000000..69d84c6a37 --- /dev/null +++ b/packages/core/src/ai-model/service-caller/proxy-deps.node.ts @@ -0,0 +1,7 @@ +export function loadUndici(): Promise { + return import('undici'); +} + +export function loadFetchSocks(): Promise { + return import('fetch-socks'); +} diff --git a/packages/core/src/ai-model/service-caller/proxy-deps.stub.ts b/packages/core/src/ai-model/service-caller/proxy-deps.stub.ts new file mode 100644 index 0000000000..6dd3c084fc --- /dev/null +++ b/packages/core/src/ai-model/service-caller/proxy-deps.stub.ts @@ -0,0 +1,11 @@ +function unavailable(): never { + throw new Error('proxy dependencies are unavailable in browser builds'); +} + +export function loadUndici(): Promise { + return unavailable(); +} + +export function loadFetchSocks(): Promise { + return unavailable(); +} diff --git a/packages/core/tests/ai/connectivity.test.ts b/packages/core/tests/ai/connectivity.test.ts index ee1f93865f..f72c203181 100644 --- a/packages/core/tests/ai/connectivity.test.ts +++ b/packages/core/tests/ai/connectivity.test.ts @@ -5,8 +5,8 @@ import { getModelRuntime } from '@/ai-model/models'; import { callAI, callAIWithObjectResponse } from '@/ai-model/service-caller'; import { globalModelConfigManager } from '@midscene/shared/env'; import { localImg2Base64 } from '@midscene/shared/img'; +import { beforeAll, describe, expect, it, rs } from '@rstest/core'; import dotenv from 'dotenv'; -import { beforeAll, describe, expect, it, vi } from 'vitest'; import { getFixture } from '../utils'; dotenv.config({ @@ -14,7 +14,7 @@ dotenv.config({ override: true, }); -vi.setConfig({ +rs.setConfig({ testTimeout: 20 * 1000, }); [ diff --git a/packages/core/tests/ai/extract/extract.test.ts b/packages/core/tests/ai/extract/extract.test.ts index 862d55d2aa..e3633e311a 100644 --- a/packages/core/tests/ai/extract/extract.test.ts +++ b/packages/core/tests/ai/extract/extract.test.ts @@ -1,15 +1,15 @@ import { AiExtractElementInfo, getModelRuntime } from '@/ai-model'; import { globalModelConfigManager } from '@midscene/shared/env'; -import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { getContextFromFixture } from '../../evaluation'; -vi.setConfig({ +rs.setConfig({ testTimeout: 240 * 1000, hookTimeout: 30 * 1000, }); -const defaultModelConfig = globalModelConfigManager.getModelConfig('default'); -const defaultModelRuntime = getModelRuntime(defaultModelConfig); +const defaultModelRuntime = () => + getModelRuntime(globalModelConfigManager.getModelConfig('default')); describe('extract', () => { it('todo', async () => { @@ -18,7 +18,7 @@ describe('extract', () => { const { parseResult } = await AiExtractElementInfo({ dataQuery: 'Array, task list, task name as string', context, - modelRuntime: defaultModelRuntime, + modelRuntime: defaultModelRuntime(), }); expect(parseResult).toBeDefined(); expect((parseResult.data as string[]).length).toBeGreaterThanOrEqual(3); @@ -31,7 +31,7 @@ describe('extract', () => { const { parseResult } = await AiExtractElementInfo({ dataQuery: '{name: string, price: string}[], 饮品名称和价格', context, - modelRuntime: defaultModelRuntime, + modelRuntime: defaultModelRuntime(), }); // Remove the thought field since it's generated dynamically by AI @@ -50,7 +50,7 @@ describe('extract', () => { dataQuery: '{checked: boolean; text: string;}[], Task list with checkbox ahead of the task name (checkbox is a round box), task name as string and `checked` is true if the task is completed. Exclude the fist row if there is no round checkbox ahead of the task name.', context, - modelRuntime: defaultModelRuntime, + modelRuntime: defaultModelRuntime(), }); // Remove the thought field since it's generated dynamically by AI diff --git a/packages/core/tests/ai/llm-inspect.test.ts b/packages/core/tests/ai/llm-inspect.test.ts index 05c00f100c..b8df7f63c2 100644 --- a/packages/core/tests/ai/llm-inspect.test.ts +++ b/packages/core/tests/ai/llm-inspect.test.ts @@ -1,15 +1,15 @@ import { AiLocateElement, AiLocateSection } from '@/ai-model'; import { getModelRuntime } from '@/ai-model/models'; import { globalModelConfigManager } from '@midscene/shared/env'; -import { expect, test, vi } from 'vitest'; +import { expect, rs, test } from '@rstest/core'; import { getContextFromFixture } from '../evaluation'; -vi.setConfig({ +rs.setConfig({ testTimeout: 120 * 1000, }); -const defaultModelConfig = globalModelConfigManager.getModelConfig('default'); -const defaultModelRuntime = getModelRuntime(defaultModelConfig); +const defaultModelRuntime = () => + getModelRuntime(globalModelConfigManager.getModelConfig('default')); test( 'basic inspect', @@ -22,7 +22,7 @@ test( const { parseResult } = await AiLocateElement({ context, targetElementDescription: 'input 输入框', - modelRuntime: defaultModelRuntime, + modelRuntime: defaultModelRuntime(), }); expect(parseResult.element).toBeDefined(); }, @@ -33,7 +33,7 @@ test('locate section', { timeout: 120 * 1000 }, async () => { const { searchAreaConfig } = await AiLocateSection({ context, sectionDescription: '搜索框', - modelRuntime: defaultModelRuntime, + modelRuntime: defaultModelRuntime(), }); expect(searchAreaConfig?.sourceRect).toBeDefined(); }); diff --git a/packages/core/tests/ai/llm-planning/basic.test.ts b/packages/core/tests/ai/llm-planning/basic.test.ts index d7525d652a..37070df5ad 100644 --- a/packages/core/tests/ai/llm-planning/basic.test.ts +++ b/packages/core/tests/ai/llm-planning/basic.test.ts @@ -1,23 +1,30 @@ import { ConversationHistory, standardPlan } from '@/ai-model'; import { getModelRuntime } from '@/ai-model/models'; import { globalModelConfigManager } from '@midscene/shared/env'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { mockActionSpace } from '../../common'; import { getContextFromFixture } from '../../evaluation'; -vi.setConfig({ +rs.setConfig({ testTimeout: 180 * 1000, hookTimeout: 30 * 1000, }); -const modelConfig = globalModelConfigManager.getModelConfig('default'); -const modelRuntime = getModelRuntime(modelConfig); +const modelConfig = () => globalModelConfigManager.getModelConfig('default'); +const modelRuntime = () => getModelRuntime(modelConfig()); +const hasModelFamily = (() => { + try { + return Boolean(modelConfig().modelFamily); + } catch { + return false; + } +})(); // These assertions check a deterministic next-action shape. In real // model-family runs, planning may choose a valid intermediate Tap before Input // or include a whole-page locate for page-level scroll, so keep this suite out // of AI CI until that prompt contract is tightened. -describe.skipIf(modelConfig.modelFamily)('automation - llm planning', () => { +describe.skipIf(hasModelFamily)('automation - llm planning', () => { it('basic run', async () => { const { context } = await getContextFromFixture('todo'); @@ -26,7 +33,7 @@ describe.skipIf(modelConfig.modelFamily)('automation - llm planning', () => { { context, actionSpace: mockActionSpace, - modelRuntime, + modelRuntime: modelRuntime(), conversationHistory: new ConversationHistory(), includeLocateInPlanning: true, effort: 'balance', @@ -46,7 +53,7 @@ describe.skipIf(modelConfig.modelFamily)('automation - llm planning', () => { { context, actionSpace: mockActionSpace, - modelRuntime, + modelRuntime: modelRuntime(), conversationHistory: new ConversationHistory(), includeLocateInPlanning: true, effort: 'balance', @@ -96,7 +103,7 @@ describe('planning', () => { const { actions } = await standardPlan(instruction, { context, actionSpace: mockActionSpace, - modelRuntime, + modelRuntime: modelRuntime(), conversationHistory: new ConversationHistory(), includeLocateInPlanning: true, effort: 'balance', @@ -118,7 +125,7 @@ describe('planning', () => { { context, actionSpace: mockActionSpace, - modelRuntime, + modelRuntime: modelRuntime(), conversationHistory: new ConversationHistory(), includeLocateInPlanning: true, effort: 'balance', @@ -137,7 +144,7 @@ describe('planning', () => { { context, actionSpace: mockActionSpace, - modelRuntime, + modelRuntime: modelRuntime(), conversationHistory: new ConversationHistory(), includeLocateInPlanning: true, effort: 'balance', @@ -156,7 +163,7 @@ describe('planning', () => { { context, actionSpace: mockActionSpace, - modelRuntime, + modelRuntime: modelRuntime(), conversationHistory: new ConversationHistory(), includeLocateInPlanning: true, effort: 'balance', diff --git a/packages/core/tests/ai/llm-planning/input.test.ts b/packages/core/tests/ai/llm-planning/input.test.ts index f2879071ce..ee0441838b 100644 --- a/packages/core/tests/ai/llm-planning/input.test.ts +++ b/packages/core/tests/ai/llm-planning/input.test.ts @@ -1,16 +1,16 @@ import { ConversationHistory, standardPlan } from '@/ai-model'; import { getModelRuntime } from '@/ai-model/models'; import { globalModelConfigManager } from '@midscene/shared/env'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { mockActionSpace } from '../../common'; import { getContextFromFixture } from '../../evaluation'; -vi.setConfig({ +rs.setConfig({ testTimeout: 180 * 1000, hookTimeout: 30 * 1000, }); -const defaultModelConfig = globalModelConfigManager.getModelConfig('default'); -const defaultModelRuntime = getModelRuntime(defaultModelConfig); +const defaultModelRuntime = () => + getModelRuntime(globalModelConfigManager.getModelConfig('default')); describe('automation - planning input', () => { it('input value', async () => { @@ -24,7 +24,7 @@ describe('automation - planning input', () => { const { actions } = await standardPlan(instruction, { context, actionSpace: mockActionSpace, - modelRuntime: defaultModelRuntime, + modelRuntime: defaultModelRuntime(), conversationHistory: new ConversationHistory(), includeLocateInPlanning: true, effort: 'balance', @@ -46,7 +46,7 @@ describe('automation - planning input', () => { const { actions } = await standardPlan(instruction, { context, actionSpace: mockActionSpace, - modelRuntime: defaultModelRuntime, + modelRuntime: defaultModelRuntime(), conversationHistory: new ConversationHistory(), includeLocateInPlanning: true, effort: 'balance', diff --git a/packages/core/tests/ai/llm-section-locator.test.ts b/packages/core/tests/ai/llm-section-locator.test.ts index bac1973cb9..8e702d841f 100644 --- a/packages/core/tests/ai/llm-section-locator.test.ts +++ b/packages/core/tests/ai/llm-section-locator.test.ts @@ -3,13 +3,20 @@ import { AiLocateSection } from '@/ai-model/workflows/grounding'; import { getTmpFile } from '@/utils'; import { globalModelConfigManager } from '@midscene/shared/env'; import { saveBase64Image } from '@midscene/shared/img'; -import { expect, test } from 'vitest'; +import { expect, test } from '@rstest/core'; import { getContextFromFixture } from '../evaluation'; -const modelConfig = globalModelConfigManager.getModelConfig('default'); -const modelRuntime = getModelRuntime(modelConfig); +const modelConfig = () => globalModelConfigManager.getModelConfig('default'); +const modelRuntime = () => getModelRuntime(modelConfig()); +const hasModelFamily = (() => { + try { + return Boolean(modelConfig().modelFamily); + } catch { + return false; + } +})(); -test.skipIf(!modelConfig.modelFamily)( +test.skipIf(!hasModelFamily)( 'locate section', { timeout: 120 * 1000, @@ -19,7 +26,7 @@ test.skipIf(!modelConfig.modelFamily)( const { searchAreaConfig } = await AiLocateSection({ context, sectionDescription: 'the version info on the top right corner', - modelRuntime, + modelRuntime: modelRuntime(), }); expect(searchAreaConfig?.sourceRect).toBeDefined(); expect(searchAreaConfig?.image.imageBase64).toBeDefined(); diff --git a/packages/core/tests/ai/service/service.test.ts b/packages/core/tests/ai/service/service.test.ts index b062e17739..62bff79cea 100644 --- a/packages/core/tests/ai/service/service.test.ts +++ b/packages/core/tests/ai/service/service.test.ts @@ -2,15 +2,22 @@ import { getModelRuntime } from '@/ai-model/models'; import Service from '@/service'; import { sleep } from '@/utils'; import { globalModelConfigManager } from '@midscene/shared/env'; -import { beforeAll, describe, expect, test, vi } from 'vitest'; +import { describe, expect, rs, test } from '@rstest/core'; import { getContextFromFixture } from '../../evaluation'; -vi.setConfig({ +rs.setConfig({ testTimeout: 120 * 1000, }); -const modelConfig = globalModelConfigManager.getModelConfig('insight'); -const modelRuntime = getModelRuntime(modelConfig); +const modelConfig = () => globalModelConfigManager.getModelConfig('insight'); +const modelRuntime = () => getModelRuntime(modelConfig()); +const hasModelFamily = (() => { + try { + return Boolean(modelConfig().modelFamily); + } catch { + return false; + } +})(); const locateTestOptions = { // Allow three 180s model attempts plus two 60s retry intervals. timeout: 12 * 60 * 1000, @@ -24,60 +31,57 @@ function distance( return Math.sqrt((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2); } -describe.skipIf(!modelConfig.modelFamily)( - 'service locate with deep think', - () => { - test('service locate with search area', locateTestOptions, async () => { +describe.skipIf(!hasModelFamily)('service locate with deep think', () => { + test('service locate with search area', locateTestOptions, async () => { + const { context } = await getContextFromFixture('taobao'); + + const service = new Service(context); + const { element } = await service.locate( + { + prompt: '购物车 icon', + deepLocate: true, + }, + {}, + modelRuntime(), + ); + expect(element).toBeDefined(); + + await sleep(3000); + }); + + test( + 'service locate with search area - deep think', + locateTestOptions, + async () => { const { context } = await getContextFromFixture('taobao'); const service = new Service(context); - const { element } = await service.locate( + const { element, rect } = await service.locate( { - prompt: '购物车 icon', + prompt: '顶部购物车 icon', deepLocate: true, }, {}, - modelRuntime, + modelRuntime(), ); expect(element).toBeDefined(); - - await sleep(3000); - }); - - test( - 'service locate with search area - deep think', - locateTestOptions, - async () => { - const { context } = await getContextFromFixture('taobao'); - - const service = new Service(context); - const { element, rect } = await service.locate( + expect(rect).toBeDefined(); + expect( + distance( { - prompt: '顶部购物车 icon', - deepLocate: true, + x: element!.rect.left, + y: element!.rect.top, }, - {}, - modelRuntime, - ); - expect(element).toBeDefined(); - expect(rect).toBeDefined(); - expect( - distance( - { - x: element!.rect.left, - y: element!.rect.top, - }, - { - x: rect!.left, - y: rect!.top, - }, - ), - ).toBeLessThan(100); - await sleep(3000); - }, - ); - }, -); + { + x: rect!.left, + y: rect!.top, + }, + ), + ).toBeLessThan(100); + await sleep(3000); + }, + ); +}); test.skip('service locate with search area', async () => { const { context } = await getContextFromFixture('image-only'); @@ -89,7 +93,7 @@ test.skip('service locate with search area', async () => { deepLocate: true, }, {}, - modelRuntime, + modelRuntime(), ); console.log(element, rect); await sleep(3000); @@ -111,7 +115,7 @@ describe( width: 80, height: 30, }, - modelRuntime, + modelRuntime(), ); expect(description).toBeDefined(); @@ -120,7 +124,10 @@ describe( test('service describe - by center point', async () => { const { context } = await getContextFromFixture('taobao'); const service = new Service(context); - const { description } = await service.describe([580, 140], modelRuntime); + const { description } = await service.describe( + [580, 140], + modelRuntime(), + ); expect(description).toBeDefined(); }); diff --git a/packages/core/tests/ai/streaming.test.ts b/packages/core/tests/ai/streaming.test.ts index c7a7e2e5fd..d7fdb839fc 100644 --- a/packages/core/tests/ai/streaming.test.ts +++ b/packages/core/tests/ai/streaming.test.ts @@ -3,8 +3,8 @@ import { callAI } from '@/ai-model/service-caller'; import type { CodeGenerationChunk } from '@/types'; import { globalModelConfigManager } from '@midscene/shared/env'; import { localImg2Base64 } from '@midscene/shared/img'; +import { describe, expect, it, rs } from '@rstest/core'; import dotenv from 'dotenv'; -import { beforeAll, describe, expect, it, vi } from 'vitest'; import { getFixture } from '../utils'; dotenv.config({ @@ -12,12 +12,12 @@ dotenv.config({ override: true, }); -vi.setConfig({ +rs.setConfig({ testTimeout: 30 * 1000, // Increased timeout for streaming tests }); -const defaultModelConfig = globalModelConfigManager.getModelConfig('default'); -const defaultModelRuntime = getModelRuntime(defaultModelConfig); +const defaultModelRuntime = () => + getModelRuntime(globalModelConfigManager.getModelConfig('default')); describe( 'Streaming functionality', @@ -42,7 +42,7 @@ describe( 'Explain the concept of artificial intelligence in 3-4 sentences.', }, ], - defaultModelRuntime, + defaultModelRuntime(), { stream: true, onChunk: (chunk: CodeGenerationChunk) => { @@ -115,7 +115,7 @@ describe( ], }, ], - defaultModelRuntime, + defaultModelRuntime(), { stream: true, onChunk: (chunk: CodeGenerationChunk) => { @@ -157,7 +157,7 @@ describe( content: 'What is 15 multiplied by 8? Show your thinking process.', }, ], - defaultModelRuntime, + defaultModelRuntime(), { stream: true, onChunk: (chunk: CodeGenerationChunk) => { @@ -189,7 +189,7 @@ describe( content: 'Count from 1 to 10, with each number on a new line.', }, ], - defaultModelRuntime, + defaultModelRuntime(), { stream: true, onChunk: (chunk: CodeGenerationChunk) => { @@ -225,7 +225,7 @@ describe( content: 'Say "Hi"', }, ], - defaultModelRuntime, + defaultModelRuntime(), { stream: true, onChunk: (chunk: CodeGenerationChunk) => { @@ -259,7 +259,7 @@ describe( content: 'Write a brief paragraph about the weather.', }, ], - defaultModelRuntime, + defaultModelRuntime(), { stream: true, onChunk: (chunk: CodeGenerationChunk) => { @@ -298,7 +298,7 @@ describe( content: 'What is programming?', }, ], - defaultModelRuntime, + defaultModelRuntime(), { stream: true, // onChunk is intentionally omitted diff --git a/packages/core/tests/unit-test/action-clear-input.test.ts b/packages/core/tests/unit-test/action-clear-input.test.ts index 36b58d06cc..bbf8064315 100644 --- a/packages/core/tests/unit-test/action-clear-input.test.ts +++ b/packages/core/tests/unit-test/action-clear-input.test.ts @@ -1,11 +1,11 @@ import { Agent } from '@/agent'; import { parseActionParam } from '@/ai-model'; import { actionClearInputParamSchema, defineActionClearInput } from '@/device'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const createAgentStub = () => { const agent = Object.create(Agent.prototype) as Agent; - (agent as any).callActionInActionSpace = vi.fn(async () => undefined); + (agent as any).callActionInActionSpace = rs.fn(async () => undefined); return agent; }; @@ -42,7 +42,7 @@ describe('ClearInput Action', () => { it('dispatches the ClearInput action with locate prompt', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any) - .callActionInActionSpace as ReturnType; + .callActionInActionSpace as ReturnType; await agent.aiClearInput('the search input field'); @@ -60,7 +60,7 @@ describe('ClearInput Action', () => { it('forwards locate options such as deepLocate and xpath', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any) - .callActionInActionSpace as ReturnType; + .callActionInActionSpace as ReturnType; await agent.aiClearInput('the search input field', { deepLocate: true, diff --git a/packages/core/tests/unit-test/action-keyboard-press.test.ts b/packages/core/tests/unit-test/action-keyboard-press.test.ts index 3692795d4b..e73947ce7c 100644 --- a/packages/core/tests/unit-test/action-keyboard-press.test.ts +++ b/packages/core/tests/unit-test/action-keyboard-press.test.ts @@ -5,11 +5,11 @@ import { defineActionKeyboardPress, } from '@/device'; import { ModelConfigManager } from '@midscene/shared/env'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const createAgentStub = () => { const agent = Object.create(Agent.prototype) as Agent; - (agent as any).callActionInActionSpace = vi.fn(async () => undefined); + (agent as any).callActionInActionSpace = rs.fn(async () => undefined); return agent; }; @@ -24,7 +24,7 @@ describe('KeyboardPress Action', () => { }); it('passes an undefined target to the keyboard primitive', async () => { - const keyboardPress = vi.fn(async () => undefined); + const keyboardPress = rs.fn(async () => undefined); const action = defineActionKeyboardPress(keyboardPress); await action.call({ keyName: 'Control+X' }); @@ -37,7 +37,7 @@ describe('KeyboardPress Action', () => { it('supports the recommended signature without a locate prompt', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await agent.aiKeyboardPress(undefined, { keyName: 'Control+X' }); @@ -64,7 +64,7 @@ describe('KeyboardPress Action', () => { it('keeps the legacy key-only signature working', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await agent.aiKeyboardPress('Control+X'); @@ -78,7 +78,7 @@ describe('KeyboardPress Action', () => { it('still builds a locate parameter when a target is provided', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await agent.aiKeyboardPress('the search input', { diff --git a/packages/core/tests/unit-test/action-long-press.test.ts b/packages/core/tests/unit-test/action-long-press.test.ts index d9fbc5a257..2cbd66a4a7 100644 --- a/packages/core/tests/unit-test/action-long-press.test.ts +++ b/packages/core/tests/unit-test/action-long-press.test.ts @@ -1,11 +1,11 @@ import { Agent } from '@/agent'; import { parseActionParam } from '@/ai-model'; import { ActionLongPressParamSchema, defineActionLongPress } from '@/device'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const createAgentStub = () => { const agent = Object.create(Agent.prototype) as Agent; - (agent as any).callActionInActionSpace = vi.fn(async () => undefined); + (agent as any).callActionInActionSpace = rs.fn(async () => undefined); return agent; }; @@ -53,7 +53,7 @@ describe('LongPress Action', () => { it('dispatches the LongPress action with locate prompt', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any) - .callActionInActionSpace as ReturnType; + .callActionInActionSpace as ReturnType; await agent.aiLongPress('首页任意一篇文章'); @@ -69,7 +69,7 @@ describe('LongPress Action', () => { it('forwards duration to the LongPress action', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any) - .callActionInActionSpace as ReturnType; + .callActionInActionSpace as ReturnType; await agent.aiLongPress('首页任意一篇文章', { duration: 2000 }); @@ -86,7 +86,7 @@ describe('LongPress Action', () => { it('separates locate options from action params', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any) - .callActionInActionSpace as ReturnType; + .callActionInActionSpace as ReturnType; await agent.aiLongPress('首页任意一篇文章', { context: '文章位于首页的信息流中', diff --git a/packages/core/tests/unit-test/action-param-validation.test.ts b/packages/core/tests/unit-test/action-param-validation.test.ts index 111abca280..41a8f4e737 100644 --- a/packages/core/tests/unit-test/action-param-validation.test.ts +++ b/packages/core/tests/unit-test/action-param-validation.test.ts @@ -5,7 +5,7 @@ import { defineAction, defineActionInput, } from '@/device'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { z } from 'zod'; describe('Action Parameter Validation', () => { @@ -568,14 +568,14 @@ describe('Action Parameter Validation', () => { describe('defineActionInput', () => { it('should pass keyboardTypeDelay to typeText', async () => { - const typeTextMock = vi.fn().mockResolvedValue(undefined); - const clearInputMock = vi.fn().mockResolvedValue(undefined); + const typeTextMock = rs.fn().mockResolvedValue(undefined); + const clearInputMock = rs.fn().mockResolvedValue(undefined); const action = defineActionInput({ typeText: typeTextMock, clearInput: clearInputMock, - keyboardPress: vi.fn(), - cursorMove: vi.fn(), + keyboardPress: rs.fn(), + cursorMove: rs.fn(), }); await action.call({ @@ -593,13 +593,13 @@ describe('Action Parameter Validation', () => { }); it('should pass autoDismissKeyboard to typeText', async () => { - const typeTextMock = vi.fn().mockResolvedValue(undefined); + const typeTextMock = rs.fn().mockResolvedValue(undefined); const action = defineActionInput({ typeText: typeTextMock, - clearInput: vi.fn(), - keyboardPress: vi.fn(), - cursorMove: vi.fn(), + clearInput: rs.fn(), + keyboardPress: rs.fn(), + cursorMove: rs.fn(), }); await action.call({ @@ -617,14 +617,14 @@ describe('Action Parameter Validation', () => { }); it('should call clearInput when mode is clear', async () => { - const typeTextMock = vi.fn(); - const clearInputMock = vi.fn().mockResolvedValue(undefined); + const typeTextMock = rs.fn(); + const clearInputMock = rs.fn().mockResolvedValue(undefined); const action = defineActionInput({ typeText: typeTextMock, clearInput: clearInputMock, - keyboardPress: vi.fn(), - cursorMove: vi.fn(), + keyboardPress: rs.fn(), + cursorMove: rs.fn(), }); await action.call({ @@ -637,13 +637,13 @@ describe('Action Parameter Validation', () => { }); it('should convert append mode to typeOnly', async () => { - const typeTextMock = vi.fn().mockResolvedValue(undefined); + const typeTextMock = rs.fn().mockResolvedValue(undefined); const action = defineActionInput({ typeText: typeTextMock, - clearInput: vi.fn(), - keyboardPress: vi.fn(), - cursorMove: vi.fn(), + clearInput: rs.fn(), + keyboardPress: rs.fn(), + cursorMove: rs.fn(), }); await action.call({ diff --git a/packages/core/tests/unit-test/action-pinch.test.ts b/packages/core/tests/unit-test/action-pinch.test.ts index 53502628a2..93b456f61f 100644 --- a/packages/core/tests/unit-test/action-pinch.test.ts +++ b/packages/core/tests/unit-test/action-pinch.test.ts @@ -4,7 +4,7 @@ import { defineActionPinch, normalizePinchParam, } from '@/device'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; describe('Pinch Action Parameter Validation', () => { describe('ActionPinchParamSchema', () => { @@ -114,7 +114,7 @@ describe('Pinch Action Parameter Validation', () => { }); it('should invoke the pinch primitive with normalized params', async () => { - const pinchFn = vi.fn(); + const pinchFn = rs.fn(); const action = defineActionPinch({ pinch: pinchFn, size: async () => ({ width: 400, height: 800 }), diff --git a/packages/core/tests/unit-test/agent-cache-config.test.ts b/packages/core/tests/unit-test/agent-cache-config.test.ts index 02ec5f153f..0eda484bcd 100644 --- a/packages/core/tests/unit-test/agent-cache-config.test.ts +++ b/packages/core/tests/unit-test/agent-cache-config.test.ts @@ -1,5 +1,5 @@ import { validateAgentCacheInput } from '@/agent/cache-config'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('validateAgentCacheInput', () => { it('accepts disabled or valid cache config', () => { diff --git a/packages/core/tests/unit-test/agent-context-option.test.ts b/packages/core/tests/unit-test/agent-context-option.test.ts index 39f8b7b649..e37d4e0ab5 100644 --- a/packages/core/tests/unit-test/agent-context-option.test.ts +++ b/packages/core/tests/unit-test/agent-context-option.test.ts @@ -1,6 +1,6 @@ import { Agent } from '@/agent'; import { TaskExecutionError } from '@/task-runner'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const planningModel = { config: { slot: 'default' }, @@ -15,37 +15,37 @@ const defaultModel = { const createAgentStub = () => { const agent = Object.create(Agent.prototype) as Agent; const taskExecutor = { - action: vi.fn(async (..._args: unknown[]) => ({ + action: rs.fn(async (..._args: unknown[]) => ({ output: { output: 'done', yamlFlow: [], }, })), - createTypeQueryExecution: vi.fn(async () => ({ + createTypeQueryExecution: rs.fn(async () => ({ output: true, thought: 'ok', })), }; const taskCache = { - matchPlanCache: vi.fn(), + matchPlanCache: rs.fn(), isCacheResultUsed: true, - updateOrAppendCacheRecord: vi.fn(), + updateOrAppendCacheRecord: rs.fn(), }; (agent as any).opts = { aiActContext: 'Global action context.', }; - const registerFileChooserListener = vi.fn(); + const registerFileChooserListener = rs.fn(); (agent as any).interface = { interfaceType: 'playwright', registerFileChooserListener, }; (agent as any).taskExecutor = taskExecutor; (agent as any).taskCache = taskCache; - (agent as any).resolveModelRuntime = vi.fn((slot: string) => + (agent as any).resolveModelRuntime = rs.fn((slot: string) => slot === 'planning' ? planningModel : defaultModel, ); - (agent as any).resolveReplanningCycleLimit = vi.fn(() => 3); + (agent as any).resolveReplanningCycleLimit = rs.fn(() => 3); return { agent, diff --git a/packages/core/tests/unit-test/agent-context-retry.test.ts b/packages/core/tests/unit-test/agent-context-retry.test.ts index 7b75c68b50..cf805f6cba 100644 --- a/packages/core/tests/unit-test/agent-context-retry.test.ts +++ b/packages/core/tests/unit-test/agent-context-retry.test.ts @@ -6,21 +6,15 @@ import { MIDSCENE_MODEL_BASE_URL, MIDSCENE_MODEL_NAME, } from '@midscene/shared/env'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; -vi.mock('openai'); +rs.mock('openai'); // Mock commonContextParser to avoid real image processing -vi.mock('@/agent/utils', async (importOriginal) => { - const actual = (await importOriginal()) as Record; - return { - ...actual, - commonContextParser: vi.fn(), - }; -}); +rs.mock('@/agent/utils', { spy: true }); import { commonContextParser } from '@/agent/utils'; -const mockedCommonContextParser = vi.mocked(commonContextParser); +const mockedCommonContextParser = rs.mocked(commonContextParser); const modelConfig = { [MIDSCENE_MODEL_NAME]: 'test-model', @@ -53,7 +47,7 @@ class RetryableAgent extends Agent { describe('Agent context retry via isRetryableContextError', () => { afterEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('retries on retryable errors and succeeds', async () => { diff --git a/packages/core/tests/unit-test/agent-custom-model.test.ts b/packages/core/tests/unit-test/agent-custom-model.test.ts index eb7cd478a7..9186c23d42 100644 --- a/packages/core/tests/unit-test/agent-custom-model.test.ts +++ b/packages/core/tests/unit-test/agent-custom-model.test.ts @@ -12,7 +12,7 @@ import { MIDSCENE_PLANNING_MODEL_BASE_URL, MIDSCENE_PLANNING_MODEL_NAME, } from '@midscene/shared/env'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; const defaultModelConfig = { [MIDSCENE_MODEL_NAME]: 'qwen2.5-vl-max', @@ -39,11 +39,11 @@ const createMockInterface = () => describe('Agent with custom OpenAI client', () => { beforeEach(() => { - vi.mock('openai'); + rs.mock('openai'); }); afterEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); describe('default modelConfig without createOpenAIClient', () => { @@ -246,8 +246,8 @@ describe('Agent with custom OpenAI client', () => { describe('constructor with createOpenAIClient', () => { it('should accept createOpenAIClient in AgentOpt with modelConfig', () => { - const mockCreateClient = vi.fn(async () => ({ - chat: { completions: { create: vi.fn() } }, + const mockCreateClient = rs.fn(async () => ({ + chat: { completions: { create: rs.fn() } }, })); // Create a mock interface instance @@ -263,8 +263,8 @@ describe('Agent with custom OpenAI client', () => { }); it('should pass createOpenAIClient to ModelConfigManager when modelConfig is provided', () => { - const mockCreateClient = vi.fn(async () => ({ - chat: { completions: { create: vi.fn() } }, + const mockCreateClient = rs.fn(async () => ({ + chat: { completions: { create: rs.fn() } }, })); // Create a mock interface instance @@ -301,12 +301,12 @@ describe('Agent with custom OpenAI client', () => { describe('intent-specific custom clients', () => { it('should support different clients for different intents', () => { - const mockCreateClient: CreateOpenAIClientFn = vi.fn( + const mockCreateClient: CreateOpenAIClientFn = rs.fn( async (_client, opts) => { const { apiKey } = opts as { apiKey?: string }; // Return different mock clients based on provided options return { - chat: { completions: { create: vi.fn() } }, + chat: { completions: { create: rs.fn() } }, _apiKey: apiKey, // For testing purposes }; }, @@ -338,13 +338,13 @@ describe('Agent with custom OpenAI client', () => { describe('observability wrapper integration', () => { it('should support wrapping clients with langsmith-style wrappers', async () => { - const mockWrapOpenAI = vi.fn((client, options) => ({ + const mockWrapOpenAI = rs.fn((client, options) => ({ ...client, _wrapped: true, _options: options, })); - const mockCreateClient: CreateOpenAIClientFn = vi.fn( + const mockCreateClient: CreateOpenAIClientFn = rs.fn( async (client, opts) => { const options = opts as { apiKey?: string }; @@ -385,7 +385,7 @@ describe('Agent with custom OpenAI client', () => { expect(planningConfig.createOpenAIClient).toBeDefined(); // Simulate calling the client creator - const baseClient = { chat: { completions: { create: vi.fn() } } }; + const baseClient = { chat: { completions: { create: rs.fn() } } }; const clientOptions = { baseURL: planningConfig.openaiBaseURL, apiKey: planningConfig.openaiApiKey, @@ -412,8 +412,8 @@ describe('Agent with custom OpenAI client', () => { }); it('should provide all config parameters to createOpenAIClient', async () => { - const mockCreateClient: CreateOpenAIClientFn = vi.fn(async () => ({ - chat: { completions: { create: vi.fn() } }, + const mockCreateClient: CreateOpenAIClientFn = rs.fn(async () => ({ + chat: { completions: { create: rs.fn() } }, })); // Create a mock interface instance @@ -433,7 +433,7 @@ describe('Agent with custom OpenAI client', () => { ); // Simulate what createChatClient does - const baseClient = { chat: { completions: { create: vi.fn() } } }; + const baseClient = { chat: { completions: { create: rs.fn() } } }; const options = { baseURL: config.openaiBaseURL, apiKey: config.openaiApiKey, @@ -448,8 +448,8 @@ describe('Agent with custom OpenAI client', () => { describe('performance characteristics', () => { it('should inject createOpenAIClient during config initialization, not on getModelConfig', () => { - const mockCreateClient = vi.fn(async () => ({ - chat: { completions: { create: vi.fn() } }, + const mockCreateClient = rs.fn(async () => ({ + chat: { completions: { create: rs.fn() } }, })); // Create a mock interface instance @@ -492,7 +492,7 @@ describe('Agent with custom OpenAI client', () => { defaultModelConfig[MIDSCENE_MODEL_BASE_URL], }, }); - const actionSpy = vi + const actionSpy = rs .spyOn((agent as any).taskExecutor, 'action') .mockResolvedValue({ output: { @@ -514,7 +514,7 @@ describe('Agent with custom OpenAI client', () => { const agent = new Agent(mockInterface, { modelConfig: defaultModelConfig, }); - const actionSpy = vi + const actionSpy = rs .spyOn((agent as any).taskExecutor, 'action') .mockResolvedValue({ output: { @@ -536,14 +536,14 @@ describe('Agent with custom OpenAI client', () => { const agent = new Agent(mockInterface, { modelConfig: defaultModelConfig, }); - const actionSpy = vi + const actionSpy = rs .spyOn((agent as any).taskExecutor, 'action') .mockResolvedValue({ output: { yamlFlow: [], }, }); - const warnSpy = vi + const warnSpy = rs .spyOn(console, 'warn') .mockImplementation(() => undefined); @@ -564,14 +564,14 @@ describe('Agent with custom OpenAI client', () => { const agent = new Agent(mockInterface, { modelConfig: defaultModelConfig, }); - const actionSpy = vi + const actionSpy = rs .spyOn((agent as any).taskExecutor, 'action') .mockResolvedValue({ output: { yamlFlow: [], }, }); - const warnSpy = vi + const warnSpy = rs .spyOn(console, 'warn') .mockImplementation(() => undefined); @@ -592,14 +592,14 @@ describe('Agent with custom OpenAI client', () => { const agent = new Agent(mockInterface, { modelConfig: defaultModelConfig, }); - const actionSpy = vi + const actionSpy = rs .spyOn((agent as any).taskExecutor, 'action') .mockResolvedValue({ output: { yamlFlow: [], }, }); - const warnSpy = vi + const warnSpy = rs .spyOn(console, 'warn') .mockImplementation(() => undefined); @@ -614,14 +614,14 @@ describe('Agent with custom OpenAI client', () => { const agent = new Agent(mockInterface, { modelConfig: defaultModelConfig, }); - const actionSpy = vi + const actionSpy = rs .spyOn((agent as any).taskExecutor, 'action') .mockResolvedValue({ output: { yamlFlow: [], }, }); - const warnSpy = vi + const warnSpy = rs .spyOn(console, 'warn') .mockImplementation(() => undefined); @@ -642,8 +642,8 @@ describe('Agent with custom OpenAI client', () => { [MIDSCENE_MODEL_FAMILY]: 'auto-glm', }, }); - const actionSpy = vi.spyOn((agent as any).taskExecutor, 'action'); - vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const actionSpy = rs.spyOn((agent as any).taskExecutor, 'action'); + rs.spyOn(console, 'warn').mockImplementation(() => undefined); await expect( agent.aiAct('click the submit button', { effort: 'fast' }), @@ -662,14 +662,14 @@ describe('Agent with custom OpenAI client', () => { [MIDSCENE_MODEL_FAMILY]: 'auto-glm', }, }); - const actionSpy = vi + const actionSpy = rs .spyOn((agent as any).taskExecutor, 'action') .mockResolvedValue({ output: { yamlFlow: [], }, }); - const warnSpy = vi + const warnSpy = rs .spyOn(console, 'warn') .mockImplementation(() => undefined); @@ -691,14 +691,14 @@ describe('Agent with custom OpenAI client', () => { [MIDSCENE_MODEL_FAMILY]: 'auto-glm', }, }); - const actionSpy = vi + const actionSpy = rs .spyOn((agent as any).taskExecutor, 'action') .mockResolvedValue({ output: { yamlFlow: [], }, }); - const warnSpy = vi + const warnSpy = rs .spyOn(console, 'warn') .mockImplementation(() => undefined); diff --git a/packages/core/tests/unit-test/agent-describe-element.test.ts b/packages/core/tests/unit-test/agent-describe-element.test.ts index 967a597955..9feb34fbb8 100644 --- a/packages/core/tests/unit-test/agent-describe-element.test.ts +++ b/packages/core/tests/unit-test/agent-describe-element.test.ts @@ -13,7 +13,7 @@ import { MIDSCENE_MODEL_NAME, } from '@midscene/shared/env'; import { localImg2Base64 } from '@midscene/shared/img'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; import { getFixture } from '../utils'; const modelConfig = { @@ -55,7 +55,7 @@ function mockServiceLocate( description?: string; }, ) { - return vi.spyOn(agent.service, 'locate').mockResolvedValue({ + return rs.spyOn(agent.service, 'locate').mockResolvedValue({ element: { ...element, description: element.description || 'mock element', @@ -67,7 +67,7 @@ function mockServiceLocate( describe('element describer utils', () => { afterEach(() => { - vi.restoreAllMocks(); + rs.restoreAllMocks(); }); it('skips locator verification when verifyPrompt is false', async () => { @@ -75,10 +75,10 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - vi.spyOn(agent.service, 'describe').mockResolvedValue({ + rs.spyOn(agent.service, 'describe').mockResolvedValue({ description: 'LocalSearch title', }); - const locate = vi + const locate = rs .spyOn(agent.service, 'locate') .mockRejectedValue(new Error('should not verify locator')); @@ -106,7 +106,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - vi.spyOn(agent.service, 'describe').mockResolvedValue({ + rs.spyOn(agent.service, 'describe').mockResolvedValue({ description: 'LocalSearch title', }); const locate = mockServiceLocate(agent, { @@ -145,7 +145,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi.spyOn(agent.service, 'describe').mockResolvedValue({ + const describe = rs.spyOn(agent.service, 'describe').mockResolvedValue({ description: 'Broad row container', }); mockServiceLocate(agent, { @@ -183,10 +183,10 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - vi.spyOn(agent.service, 'describe').mockResolvedValue({ + rs.spyOn(agent.service, 'describe').mockResolvedValue({ description: 'Missing target', }); - vi.spyOn(agent.service, 'locate').mockRejectedValue( + rs.spyOn(agent.service, 'locate').mockRejectedValue( new Error('failed to locate element'), ); @@ -215,7 +215,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi.spyOn(agent.service, 'describe').mockResolvedValue({ + const describe = rs.spyOn(agent.service, 'describe').mockResolvedValue({ description: 'Screenshot target', }); const locate = mockServiceLocate(agent, { @@ -268,7 +268,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi + const describe = rs .spyOn(agent.service, 'describe') .mockResolvedValueOnce({ description: 'Broad target', @@ -312,7 +312,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi + const describe = rs .spyOn(agent.service, 'describe') .mockResolvedValueOnce({ description: 'Broad target', @@ -357,7 +357,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi + const describe = rs .spyOn(agent.service, 'describe') .mockResolvedValueOnce({ description: 'Broad target', @@ -405,7 +405,7 @@ describe('element describer utils', () => { /^data:image\/[a-zA-Z0-9.+-]+;base64,/, '', ); - const describe = vi.spyOn(agent.service, 'describe').mockResolvedValue({ + const describe = rs.spyOn(agent.service, 'describe').mockResolvedValue({ description: 'Screenshot target', }); mockServiceLocate(agent, { @@ -442,7 +442,7 @@ describe('element describer utils', () => { shrunkShotToLogicalRatio: 1, _isFrozen: true, }; - const runPlans = vi + const runPlans = rs .spyOn(agent.taskExecutor, 'runPlans') .mockResolvedValue({ output: { @@ -510,7 +510,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi + const describe = rs .spyOn(agent.service, 'describe') .mockResolvedValueOnce({ description: 'First target', @@ -547,7 +547,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi + const describe = rs .spyOn(agent.service, 'describe') .mockResolvedValueOnce({ description: 'First target', @@ -594,7 +594,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - vi.spyOn(agent.service, 'describe') + rs.spyOn(agent.service, 'describe') .mockResolvedValueOnce({ description: 'First target', }) @@ -632,7 +632,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi + const describe = rs .spyOn(agent.service, 'describe') .mockResolvedValueOnce({ description: 'First target', @@ -672,7 +672,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi.spyOn(agent.service, 'describe').mockResolvedValue({ + const describe = rs.spyOn(agent.service, 'describe').mockResolvedValue({ description: 'Mapped target', }); const locate = mockServiceLocate(agent, { @@ -720,7 +720,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi.spyOn(agent.service, 'describe').mockResolvedValue({ + const describe = rs.spyOn(agent.service, 'describe').mockResolvedValue({ description: 'should not run', }); @@ -742,7 +742,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const describe = vi.spyOn(agent.service, 'describe').mockResolvedValue({ + const describe = rs.spyOn(agent.service, 'describe').mockResolvedValue({ description: 'Actual-size target', }); @@ -862,7 +862,7 @@ describe('element describer utils', () => { generateReport: false, modelConfig, }); - const locate = vi + const locate = rs .spyOn(agent.service, 'locate') .mockRejectedValue(new Error('should not verify locator')); diff --git a/packages/core/tests/unit-test/agent-dump-update.test.ts b/packages/core/tests/unit-test/agent-dump-update.test.ts index e688ccca3d..7e15d81f76 100644 --- a/packages/core/tests/unit-test/agent-dump-update.test.ts +++ b/packages/core/tests/unit-test/agent-dump-update.test.ts @@ -5,9 +5,9 @@ import { MIDSCENE_MODEL_BASE_URL, MIDSCENE_MODEL_NAME, } from '@midscene/shared/env'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; -vi.mock('openai'); +rs.mock('openai'); const modelConfig = { [MIDSCENE_MODEL_NAME]: 'test-model', @@ -33,7 +33,7 @@ function createLargeBase64DataUri(byteSize: number): string { describe('Agent dump update screenshot serialization', () => { afterEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('passes report attributes to report generator updates', async () => { @@ -45,10 +45,10 @@ describe('Agent dump update screenshot serialization', () => { }); const reportGeneratorStub = { - onExecutionUpdate: vi.fn(), - flush: vi.fn(async () => {}), - finalize: vi.fn(async () => undefined), - getReportPath: vi.fn(() => undefined), + onExecutionUpdate: rs.fn(), + flush: rs.fn(async () => {}), + finalize: rs.fn(async () => undefined), + getReportPath: rs.fn(() => undefined), }; (agent as any).reportGenerator = reportGeneratorStub; @@ -76,14 +76,14 @@ describe('Agent dump update screenshot serialization', () => { screenshot.markPersistedInline('/tmp/mock-report.html'); } }, - flush: vi.fn(async () => {}), - finalize: vi.fn(async () => undefined), - getReportPath: vi.fn(() => undefined), + flush: rs.fn(async () => {}), + finalize: rs.fn(async () => undefined), + getReportPath: rs.fn(() => undefined), }; (agent as any).reportGenerator = reportGeneratorStub; - const listener = vi.fn(); + const listener = rs.fn(); agent.onDumpUpdate = listener; await agent.recordToReport('snapshot', { content: 'check screenshot' }); @@ -103,7 +103,7 @@ describe('Agent dump update screenshot serialization', () => { }); it('uses provided screenshot data when recording to report', async () => { - const screenshotBase64 = vi + const screenshotBase64 = rs .fn() .mockRejectedValue(new Error('should not capture again')); const agent = new Agent( @@ -118,10 +118,10 @@ describe('Agent dump update screenshot serialization', () => { ); const reportGeneratorStub = { - onExecutionUpdate: vi.fn(), - flush: vi.fn(async () => {}), - finalize: vi.fn(async () => undefined), - getReportPath: vi.fn(() => undefined), + onExecutionUpdate: rs.fn(), + flush: rs.fn(async () => {}), + finalize: rs.fn(async () => undefined), + getReportPath: rs.fn(() => undefined), }; (agent as any).reportGenerator = reportGeneratorStub; @@ -139,7 +139,7 @@ describe('Agent dump update screenshot serialization', () => { }); it('records multiple provided screenshots in one report entry', async () => { - const screenshotBase64 = vi + const screenshotBase64 = rs .fn() .mockRejectedValue(new Error('should not capture again')); const agent = new Agent( @@ -154,10 +154,10 @@ describe('Agent dump update screenshot serialization', () => { ); const reportGeneratorStub = { - onExecutionUpdate: vi.fn(), - flush: vi.fn(async () => {}), - finalize: vi.fn(async () => undefined), - getReportPath: vi.fn(() => undefined), + onExecutionUpdate: rs.fn(), + flush: rs.fn(async () => {}), + finalize: rs.fn(async () => undefined), + getReportPath: rs.fn(() => undefined), }; (agent as any).reportGenerator = reportGeneratorStub; @@ -198,7 +198,7 @@ describe('Agent dump update screenshot serialization', () => { }); it('rejects invalid recordToReport option types before capturing screenshots', async () => { - const screenshotBase64 = vi + const screenshotBase64 = rs .fn() .mockRejectedValue(new Error('should not capture again')); const agent = new Agent( @@ -236,7 +236,7 @@ describe('Agent dump update screenshot serialization', () => { }); it('rejects unsupported custom screenshot data URI formats', async () => { - const screenshotBase64 = vi + const screenshotBase64 = rs .fn() .mockRejectedValue(new Error('should not capture again')); const agent = new Agent( @@ -264,7 +264,7 @@ describe('Agent dump update screenshot serialization', () => { }); it('rejects an empty custom screenshot list', async () => { - const screenshotBase64 = vi + const screenshotBase64 = rs .fn() .mockRejectedValue(new Error('should not capture again')); const agent = new Agent( @@ -279,10 +279,10 @@ describe('Agent dump update screenshot serialization', () => { ); const reportGeneratorStub = { - onExecutionUpdate: vi.fn(), - flush: vi.fn(async () => {}), - finalize: vi.fn(async () => undefined), - getReportPath: vi.fn(() => undefined), + onExecutionUpdate: rs.fn(), + flush: rs.fn(async () => {}), + finalize: rs.fn(async () => undefined), + getReportPath: rs.fn(() => undefined), }; (agent as any).reportGenerator = reportGeneratorStub; @@ -300,7 +300,7 @@ describe('Agent dump update screenshot serialization', () => { }); it('rejects multiple custom screenshot sources', async () => { - const screenshotBase64 = vi + const screenshotBase64 = rs .fn() .mockRejectedValue(new Error('should not capture again')); const agent = new Agent( @@ -315,10 +315,10 @@ describe('Agent dump update screenshot serialization', () => { ); const reportGeneratorStub = { - onExecutionUpdate: vi.fn(), - flush: vi.fn(async () => {}), - finalize: vi.fn(async () => undefined), - getReportPath: vi.fn(() => undefined), + onExecutionUpdate: rs.fn(), + flush: rs.fn(async () => {}), + finalize: rs.fn(async () => undefined), + getReportPath: rs.fn(() => undefined), }; (agent as any).reportGenerator = reportGeneratorStub; @@ -345,10 +345,10 @@ describe('Agent dump update screenshot serialization', () => { }); const reportGeneratorStub = { - onExecutionUpdate: vi.fn(), - flush: vi.fn(async () => {}), - finalize: vi.fn(async () => undefined), - getReportPath: vi.fn(() => undefined), + onExecutionUpdate: rs.fn(), + flush: rs.fn(async () => {}), + finalize: rs.fn(async () => undefined), + getReportPath: rs.fn(() => undefined), }; (agent as any).reportGenerator = reportGeneratorStub; @@ -409,9 +409,9 @@ describe('Agent dump update screenshot serialization', () => { screenshot.markPersistedInline('/tmp/mock-report.html'); } }, - flush: vi.fn(async () => {}), - finalize: vi.fn(async () => undefined), - getReportPath: vi.fn(() => undefined), + flush: rs.fn(async () => {}), + finalize: rs.fn(async () => undefined), + getReportPath: rs.fn(() => undefined), }; (agent as any).reportGenerator = reportGeneratorStub; @@ -458,7 +458,7 @@ describe('Agent dump update screenshot serialization', () => { const agent = new Agent( { ...createMockInterface(), - destroy: vi.fn(async () => { + destroy: rs.fn(async () => { order.push('interface.destroy'); }), } as any, @@ -469,15 +469,15 @@ describe('Agent dump update screenshot serialization', () => { ); (agent as any).reportGenerator = { - onExecutionUpdate: vi.fn(), - flush: vi.fn(async () => { + onExecutionUpdate: rs.fn(), + flush: rs.fn(async () => { order.push('report.flush'); }), - finalize: vi.fn(async () => { + finalize: rs.fn(async () => { order.push('report.finalize'); return undefined; }), - getReportPath: vi.fn(() => undefined), + getReportPath: rs.fn(() => undefined), }; await agent.destroy(); diff --git a/packages/core/tests/unit-test/agent-metrics.test.ts b/packages/core/tests/unit-test/agent-metrics.test.ts index c66f9e2dca..0af0eae39c 100644 --- a/packages/core/tests/unit-test/agent-metrics.test.ts +++ b/packages/core/tests/unit-test/agent-metrics.test.ts @@ -7,9 +7,9 @@ import { MIDSCENE_MODEL_BASE_URL, MIDSCENE_MODEL_NAME, } from '@midscene/shared/env'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; -vi.mock('openai'); +rs.mock('openai'); const modelConfig = { [MIDSCENE_MODEL_NAME]: 'test-model', @@ -120,7 +120,7 @@ describe('MetricsCollector', () => { describe('Agent usage metrics', () => { afterEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('counts each task usage once across re-emitted snapshots', async () => { @@ -187,7 +187,7 @@ describe('Agent usage metrics', () => { }); it('invokes the onLLMUsage callback once per usage', async () => { - const onLLMUsage = vi.fn(); + const onLLMUsage = rs.fn(); const agent = new Agent(createMockInterface(), { modelConfig, generateReport: false, @@ -212,11 +212,11 @@ describe('Agent usage metrics', () => { describe('Agent usage via ModelRuntime.onUsage (real lifecycle)', () => { afterEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('collects usage reported through the model runtime onUsage callback', async () => { - const onLLMUsage = vi.fn(); + const onLLMUsage = rs.fn(); const agent = new Agent(createMockInterface(), { modelConfig, generateReport: false, @@ -275,7 +275,7 @@ describe('Agent usage via ModelRuntime.onUsage (real lifecycle)', () => { }); it('deduplicates across onUsage and collectUsageMetrics paths by request_id', async () => { - const onLLMUsage = vi.fn(); + const onLLMUsage = rs.fn(); const agent = new Agent(createMockInterface(), { modelConfig, generateReport: false, @@ -312,7 +312,7 @@ describe('Agent usage via ModelRuntime.onUsage (real lifecycle)', () => { }); it('deduplicates across paths by internal call id when request_id is absent', async () => { - const onLLMUsage = vi.fn(); + const onLLMUsage = rs.fn(); const agent = new Agent(createMockInterface(), { modelConfig, generateReport: false, @@ -380,7 +380,7 @@ describe('Agent usage via ModelRuntime.onUsage (real lifecycle)', () => { }); it('isolates onLLMUsage listener errors from metrics collection', async () => { - const onLLMUsage = vi.fn(() => { + const onLLMUsage = rs.fn(() => { throw new Error('listener boom'); }); const agent = new Agent(createMockInterface(), { diff --git a/packages/core/tests/unit-test/agent-progress-bus.test.ts b/packages/core/tests/unit-test/agent-progress-bus.test.ts index af0b1b2945..3b555de8aa 100644 --- a/packages/core/tests/unit-test/agent-progress-bus.test.ts +++ b/packages/core/tests/unit-test/agent-progress-bus.test.ts @@ -1,6 +1,6 @@ import { AgentProgressBus } from '@/agent/progress'; import type { AgentProgressEvent } from '@/types'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; describe('AgentProgressBus', () => { it('wraps payloads in an envelope and stamps a monotonic sequence', async () => { @@ -67,7 +67,7 @@ describe('AgentProgressBus', () => { it('stops delivering after the disposer returned by subscribe is called', async () => { const bus = new AgentProgressBus(); - const listener = vi.fn(); + const listener = rs.fn(); const dispose = bus.subscribe(listener); await bus.publish('aiAct', 'start', {}); @@ -80,8 +80,8 @@ describe('AgentProgressBus', () => { it('removes a listener by reference via unsubscribe', async () => { const bus = new AgentProgressBus(); - const keep = vi.fn(); - const drop = vi.fn(); + const keep = rs.fn(); + const drop = rs.fn(); bus.subscribe(keep); bus.subscribe(drop); @@ -94,9 +94,9 @@ describe('AgentProgressBus', () => { it('clears all listeners', async () => { const bus = new AgentProgressBus(); - const listener = vi.fn(); + const listener = rs.fn(); bus.subscribe(listener); - bus.subscribe(vi.fn()); + bus.subscribe(rs.fn()); bus.clear(); await bus.publish('aiAct', 'start', {}); @@ -107,7 +107,7 @@ describe('AgentProgressBus', () => { it('isolates a throwing listener so the others still run and publish resolves', async () => { const bus = new AgentProgressBus(); - const after = vi.fn(); + const after = rs.fn(); bus.subscribe(() => { throw new Error('listener boom'); }); diff --git a/packages/core/tests/unit-test/agent-report-filename.test.ts b/packages/core/tests/unit-test/agent-report-filename.test.ts index 5727f7f91d..77067912ac 100644 --- a/packages/core/tests/unit-test/agent-report-filename.test.ts +++ b/packages/core/tests/unit-test/agent-report-filename.test.ts @@ -5,7 +5,7 @@ import { MIDSCENE_MODEL_BASE_URL, MIDSCENE_MODEL_NAME, } from '@midscene/shared/env'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const modelConfig = { [MIDSCENE_MODEL_NAME]: 'test-model', diff --git a/packages/core/tests/unit-test/agent-report-html.test.ts b/packages/core/tests/unit-test/agent-report-html.test.ts index 5be42b6320..425fb77314 100644 --- a/packages/core/tests/unit-test/agent-report-html.test.ts +++ b/packages/core/tests/unit-test/agent-report-html.test.ts @@ -6,15 +6,14 @@ import { MIDSCENE_MODEL_BASE_URL, MIDSCENE_MODEL_NAME, } from '@midscene/shared/env'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; -vi.mock('@/utils', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - reportHTMLContent: vi.fn(() => 'report'), - }; -}); +import * as utilsActual from '@/utils' with { rstest: 'importActual' }; + +rs.mock('@/utils', () => ({ + ...utilsActual, + reportHTMLContent: rs.fn(() => 'report'), +})); const modelConfig = { [MIDSCENE_MODEL_NAME]: 'test-model', diff --git a/packages/core/tests/unit-test/agent-scroll-compat.test.ts b/packages/core/tests/unit-test/agent-scroll-compat.test.ts index 9734eb0f6f..0860a204b1 100644 --- a/packages/core/tests/unit-test/agent-scroll-compat.test.ts +++ b/packages/core/tests/unit-test/agent-scroll-compat.test.ts @@ -1,9 +1,9 @@ import { Agent } from '@/agent'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const createAgentStub = () => { const agent = Object.create(Agent.prototype) as Agent; - (agent as any).callActionInActionSpace = vi.fn(async () => undefined); + (agent as any).callActionInActionSpace = rs.fn(async () => undefined); return agent; }; @@ -11,7 +11,7 @@ describe('Agent aiScroll legacy scrollType compatibility', () => { it('normalizes legacy scrollType values in legacy signature', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await agent.aiScroll({ direction: 'down', scrollType: 'once' } as any); @@ -28,7 +28,7 @@ describe('Agent aiScroll legacy scrollType compatibility', () => { it('normalizes legacy scrollType values in new signature', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await agent.aiScroll('product list', { @@ -48,7 +48,7 @@ describe('Agent aiScroll legacy scrollType compatibility', () => { it('uses new signature when scroll options is an empty object', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await agent.aiScroll('计数器', {} as any); @@ -67,7 +67,7 @@ describe('Agent aiScroll legacy scrollType compatibility', () => { it('uses new signature when locatePrompt is an object with prompt', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await agent.aiScroll({ prompt: '计数器' } as any, {} as any); @@ -86,7 +86,7 @@ describe('Agent aiScroll legacy scrollType compatibility', () => { it('treats null locatePrompt as a global scroll', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await agent.aiScroll( diff --git a/packages/core/tests/unit-test/agent-ui-observer.test.ts b/packages/core/tests/unit-test/agent-ui-observer.test.ts index e858392468..22bbdbb92d 100644 --- a/packages/core/tests/unit-test/agent-ui-observer.test.ts +++ b/packages/core/tests/unit-test/agent-ui-observer.test.ts @@ -10,7 +10,7 @@ import { ScreenshotItem } from '@/screenshot-item'; import type { UIContext } from '@/types'; import { resolveObservationArtifactAdapter } from '@midscene/shared/agent-tools/observation-artifact'; import type { UIObservationRecord } from '@midscene/shared/agent-tools/types'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; const defaultModel = { config: { slot: 'default' } }; const tempDirectories: string[] = []; @@ -33,20 +33,20 @@ const fakeContext = (tag: string): UIContext => const createAgentStub = (opts: { openFrameSource?: () => any } = {}) => { const agent = Object.create(Agent.prototype) as Agent; - const createTypeQueryExecution = vi.fn(async () => ({ + const createTypeQueryExecution = rs.fn(async () => ({ output: true, thought: 'ok', })); - const screenshotBase64 = vi.fn(async () => dataUrl('fallback')); + const screenshotBase64 = rs.fn(async () => dataUrl('fallback')); (agent as any).opts = {}; (agent as any).ownedObservers = new Set(); (agent as any).taskExecutor = { createTypeQueryExecution }; - (agent as any).resolveModelRuntime = vi.fn(() => defaultModel); + (agent as any).resolveModelRuntime = rs.fn(() => defaultModel); (agent as any).interface = { screenshotBase64, ...(opts.openFrameSource ? { openFrameSource: opts.openFrameSource } : {}), }; - (agent as any).getUIContext = vi.fn(async () => + (agent as any).getUIContext = rs.fn(async () => fakeContext('representative'), ); return { agent, createTypeQueryExecution, screenshotBase64 }; @@ -60,12 +60,12 @@ describe('Agent.startObserving', () => { }); it('returns a fixed observation that can run aiAssert', async () => { - const decode = vi.fn(async (refs: any[]) => + const decode = rs.fn(async (refs: any[]) => refs.map((frame) => dataUrl(`decoded:${frame.ref}`)), ); - const stop = vi.fn(); + const stop = rs.fn(); let tick = 0; - const openFrameSource = vi.fn(async () => ({ + const openFrameSource = rs.fn(async () => ({ latest: () => ({ ref: `frame-${tick++}`, capturedAt: tick }), decode, stop, @@ -99,11 +99,11 @@ describe('Agent.startObserving', () => { }); it('rejects a second active observer but permits another after stop', async () => { - const decode = vi.fn(async (refs: any[]) => + const decode = rs.fn(async (refs: any[]) => refs.map((frame) => dataUrl(`decoded:${frame.ref}`)), ); - const stop = vi.fn(); - const openFrameSource = vi.fn(async () => ({ + const stop = rs.fn(); + const openFrameSource = rs.fn(async () => ({ latest: () => ({ ref: 'f0', capturedAt: 0 }), decode, stop, @@ -142,10 +142,10 @@ describe('Agent.startObserving', () => { bufferedFrame.persisted, ); (agent as any).reportGenerator = { - flush: vi.fn().mockResolvedValue(undefined), - finalize: vi.fn().mockResolvedValue(undefined), + flush: rs.fn().mockResolvedValue(undefined), + finalize: rs.fn().mockResolvedValue(undefined), }; - (agent as any).resetDump = vi.fn(); + (agent as any).resetDump = rs.fn(); expect(existsSync(framePath)).toBe(true); await agent.destroy(); diff --git a/packages/core/tests/unit-test/ai-act-file-upload-tap.test.ts b/packages/core/tests/unit-test/ai-act-file-upload-tap.test.ts index 4294590cd2..1bba7fc845 100644 --- a/packages/core/tests/unit-test/ai-act-file-upload-tap.test.ts +++ b/packages/core/tests/unit-test/ai-act-file-upload-tap.test.ts @@ -10,7 +10,7 @@ import { defineActionRegisterFileChooserAccept, } from '@/device'; import type { DeviceAction, PlanningAction } from '@/types'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const fixtureFile = join(__dirname, 'ai-act-file-upload-tap.test.ts'); type TestFileChooserHandler = (chooser: { @@ -19,7 +19,7 @@ type TestFileChooserHandler = (chooser: { describe('aiAct file chooser registration', () => { it('should serialize file chooser registration separately from Tap', () => { - const register = vi.fn(async () => {}); + const register = rs.fn(async () => {}); const plans: PlanningAction[] = [ { type: 'RegisterFileChooserAccept', @@ -43,7 +43,7 @@ describe('aiAct file chooser registration', () => { description: 'Tap the element', interfaceAlias: 'aiTap', paramSchema: actionTapParamSchema, - call: vi.fn(), + call: rs.fn(), }, ] as DeviceAction[]; @@ -62,14 +62,14 @@ describe('aiAct file chooser registration', () => { it('should replace registered files and clear the active registration', async () => { const registrations: Array<{ handler: TestFileChooserHandler; - dispose: ReturnType; + dispose: ReturnType; }> = []; const acceptedFiles: string[][] = []; const mockInterface = { interfaceType: 'playwright', - registerFileChooserListener: vi.fn( + registerFileChooserListener: rs.fn( async (handler: TestFileChooserHandler) => { - const dispose = vi.fn(); + const dispose = rs.fn(); registrations.push({ handler, dispose }); return { dispose, getError: () => undefined }; }, @@ -95,10 +95,10 @@ describe('aiAct file chooser registration', () => { }); it('clears the active registration instead of accepting an empty file list', async () => { - const dispose = vi.fn(); + const dispose = rs.fn(); const mockInterface = { interfaceType: 'playwright', - registerFileChooserListener: vi.fn(async () => ({ + registerFileChooserListener: rs.fn(async () => ({ dispose, getError: () => undefined, })), @@ -114,10 +114,10 @@ describe('aiAct file chooser registration', () => { it('should return a file chooser handling error while disposing the registration', async () => { const uploadError = new Error('file upload failed'); - const dispose = vi.fn(); + const dispose = rs.fn(); const mockInterface = { interfaceType: 'playwright', - registerFileChooserListener: vi.fn(async () => ({ + registerFileChooserListener: rs.fn(async () => ({ dispose, getError: () => uploadError, })), @@ -150,9 +150,9 @@ describe('aiAct file chooser registration', () => { const acceptedFiles: string[][] = []; const mockInterface = { interfaceType: 'playwright', - registerFileChooserListener: vi.fn( + registerFileChooserListener: rs.fn( async (handler: TestFileChooserHandler) => ({ - dispose: vi.fn(), + dispose: rs.fn(), getError: () => undefined, handler, }), @@ -173,8 +173,8 @@ describe('aiAct file chooser registration', () => { }); it('requires fileChooserAllowedDir for model-driven uploads', async () => { - const registerFileChooserListener = vi.fn(async () => ({ - dispose: vi.fn(), + const registerFileChooserListener = rs.fn(async () => ({ + dispose: rs.fn(), getError: () => undefined, })); const agent = new Agent( @@ -205,15 +205,15 @@ describe('aiAct file chooser registration', () => { }, }, }; - (agent as any).resolveModelRuntime = vi.fn(() => modelRuntime); - (agent as any).resolveReplanningCycleLimit = vi.fn(() => 3); + (agent as any).resolveModelRuntime = rs.fn(() => modelRuntime); + (agent as any).resolveReplanningCycleLimit = rs.fn(() => 3); - vi.spyOn(agent.taskExecutor, 'action').mockImplementation(async () => { + rs.spyOn(agent.taskExecutor, 'action').mockImplementation(async () => { await registerAction?.call({ files: basename(fixtureFile) }); return { output: { output: 'uploaded', yamlFlow: [] } } as any; }); - const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(__dirname); + const cwdSpy = rs.spyOn(process, 'cwd').mockReturnValue(__dirname); try { await expect(agent.aiAct('Upload a file')).rejects.toThrow( /requires aiAct option fileChooserAllowedDir/, diff --git a/packages/core/tests/unit-test/ai-act-plan-cache-fallback.test.ts b/packages/core/tests/unit-test/ai-act-plan-cache-fallback.test.ts index 9261c7b9e1..aa44c2256e 100644 --- a/packages/core/tests/unit-test/ai-act-plan-cache-fallback.test.ts +++ b/packages/core/tests/unit-test/ai-act-plan-cache-fallback.test.ts @@ -6,7 +6,7 @@ import { MIDSCENE_MODEL_NAME, } from '@midscene/shared/env'; import { uuid } from '@midscene/shared/utils'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; const modelConfig = { [MIDSCENE_MODEL_NAME]: 'qwen2.5-vl-max', @@ -59,7 +59,7 @@ function createAgentWithPlanCache( describe('aiAct plan cache fallback', () => { afterEach(() => { - vi.restoreAllMocks(); + rs.restoreAllMocks(); }); it('disables the stale plan cache instead of caching fallback flow when cached YAML fails', async () => { @@ -76,8 +76,8 @@ describe('aiAct plan cache fallback', () => { prompt, ); const taskExecutor = { - loadYamlFlowAsPlanning: vi.fn().mockResolvedValue(undefined), - action: vi.fn().mockResolvedValue({ + loadYamlFlowAsPlanning: rs.fn().mockResolvedValue(undefined), + action: rs.fn().mockResolvedValue({ output: { output: 'completed after fallback', yamlFlow: [{ aiTap: 'final confirmation button' }], @@ -86,7 +86,7 @@ describe('aiAct plan cache fallback', () => { }; agent.taskExecutor = taskExecutor as any; - vi.spyOn(agent, 'runYaml').mockRejectedValue( + rs.spyOn(agent, 'runYaml').mockRejectedValue( new Error('optional popup close button not found after opening summary'), ); @@ -108,8 +108,8 @@ describe('aiAct plan cache fallback', () => { it('disables the stale plan cache when fallback succeeds without a new flow', async () => { const { agent, internal } = createAgentWithPlanCache(); agent.taskExecutor = { - loadYamlFlowAsPlanning: vi.fn().mockResolvedValue(undefined), - action: vi.fn().mockResolvedValue({ + loadYamlFlowAsPlanning: rs.fn().mockResolvedValue(undefined), + action: rs.fn().mockResolvedValue({ output: { output: 'nothing to do', yamlFlow: [], @@ -117,7 +117,7 @@ describe('aiAct plan cache fallback', () => { }), } as any; - vi.spyOn(agent, 'runYaml').mockRejectedValue( + rs.spyOn(agent, 'runYaml').mockRejectedValue( new Error('optional popup close button not found'), ); @@ -132,8 +132,8 @@ describe('aiAct plan cache fallback', () => { it('keeps using the cached YAML when it succeeds', async () => { const { agent } = createAgentWithPlanCache(); const taskExecutor = { - loadYamlFlowAsPlanning: vi.fn().mockResolvedValue(undefined), - action: vi.fn().mockResolvedValue({ + loadYamlFlowAsPlanning: rs.fn().mockResolvedValue(undefined), + action: rs.fn().mockResolvedValue({ output: { output: 'replanned', yamlFlow: [{ aiTap: 'stable submit button' }], @@ -141,7 +141,7 @@ describe('aiAct plan cache fallback', () => { }), }; agent.taskExecutor = taskExecutor as any; - const runYaml = vi.spyOn(agent, 'runYaml').mockResolvedValue({ + const runYaml = rs.spyOn(agent, 'runYaml').mockResolvedValue({ result: {}, }); diff --git a/packages/core/tests/unit-test/ai-judge-order-sensitive.test.ts b/packages/core/tests/unit-test/ai-judge-order-sensitive.test.ts index 7a109e6ff9..e7177acd60 100644 --- a/packages/core/tests/unit-test/ai-judge-order-sensitive.test.ts +++ b/packages/core/tests/unit-test/ai-judge-order-sensitive.test.ts @@ -3,15 +3,15 @@ import { callAIWithObjectResponse } from '@/ai-model/service-caller'; import { AiJudgeOrderSensitive } from '@/ai-model/workflows/insight'; import type { AIUsageInfo } from '@/types'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -vi.mock('@/ai-model/service-caller', () => ({ - callAIWithObjectResponse: vi.fn(), +rs.mock('@/ai-model/service-caller', () => ({ + callAIWithObjectResponse: rs.fn(), })); describe('AiJudgeOrderSensitive', () => { beforeEach(() => { - vi.mocked(callAIWithObjectResponse).mockReset(); + rs.mocked(callAIWithObjectResponse).mockReset(); }); it('judges order sensitivity with generated messages', async () => { @@ -29,7 +29,7 @@ describe('AiJudgeOrderSensitive', () => { request_id: undefined, }; - vi.mocked(callAIWithObjectResponse).mockResolvedValue({ + rs.mocked(callAIWithObjectResponse).mockResolvedValue({ content: { isOrderSensitive: true }, usage, contentString: '{"isOrderSensitive": true}', diff --git a/packages/core/tests/unit-test/aiaction-cacheable.test.ts b/packages/core/tests/unit-test/aiaction-cacheable.test.ts index 87d78375f2..263ebf244b 100644 --- a/packages/core/tests/unit-test/aiaction-cacheable.test.ts +++ b/packages/core/tests/unit-test/aiaction-cacheable.test.ts @@ -4,34 +4,34 @@ import type { AbstractInterface } from '@/device'; import { ScreenshotItem } from '@/screenshot-item'; import type { ExecutionTask, ExecutionTaskApply } from '@/types'; import { uuid } from '@midscene/shared/utils'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import type Service from '../../src'; import { getMidsceneLocationSchema, z } from '../../src'; +import * as planningActual from '@/ai-model/workflows/planning' with { + rstest: 'importActual', +}; + // Mock AI planning to avoid real AI calls -vi.mock('@/ai-model/workflows/planning', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - standardPlan: vi.fn().mockResolvedValue({ - actions: [ - { - type: 'Click', - param: { - locate: { - prompt: 'button', - }, +rs.mock('@/ai-model/workflows/planning', () => ({ + ...planningActual, + standardPlan: rs.fn().mockResolvedValue({ + actions: [ + { + type: 'Click', + param: { + locate: { + prompt: 'button', }, - thought: 'test thought', }, - ], - more_actions_needed_by_instruction: false, - log: 'test log', - yamlFlow: [], - }), - }; -}); + thought: 'test thought', + }, + ], + more_actions_needed_by_instruction: false, + log: 'test log', + yamlFlow: [], + }), +})); const createRuntimeTask = (task: ExecutionTaskApply): ExecutionTask => ({ ...task, @@ -54,26 +54,26 @@ describe('aiAction cacheable option propagation', () => { // Create mock interface mockInterface = { interfaceType: 'web', - screenshotBase64: vi.fn().mockResolvedValue(validBase64Image), - size: vi.fn().mockResolvedValue({ width: 1920, height: 1080 }), - actionSpace: vi.fn().mockReturnValue([ + screenshotBase64: rs.fn().mockResolvedValue(validBase64Image), + size: rs.fn().mockResolvedValue({ width: 1920, height: 1080 }), + actionSpace: rs.fn().mockReturnValue([ { name: 'Click', paramSchema: z.object({ locate: getMidsceneLocationSchema(), }), - call: vi.fn().mockResolvedValue({}), + call: rs.fn().mockResolvedValue({}), }, ]), - cacheFeatureForPoint: vi.fn().mockResolvedValue({ + cacheFeatureForPoint: rs.fn().mockResolvedValue({ feature: 'mock-feature', }), - rectMatchesCacheFeature: vi.fn().mockResolvedValue(undefined), + rectMatchesCacheFeature: rs.fn().mockResolvedValue(undefined), }; // Create mock insight mockService = { - contextRetrieverFn: vi.fn().mockImplementation(async () => ({ + contextRetrieverFn: rs.fn().mockImplementation(async () => ({ screenshot: ScreenshotItem.create(validBase64Image, Date.now()), shotSize: { width: 1920, height: 1080 }, shrunkShotToLogicalRatio: 1, @@ -83,7 +83,7 @@ describe('aiAction cacheable option propagation', () => { children: [], }, })), - locate: vi.fn().mockResolvedValue({ + locate: rs.fn().mockResolvedValue({ element: { id: 'element-id', center: [100, 100], @@ -107,7 +107,7 @@ describe('aiAction cacheable option propagation', () => { it('should propagate cacheable: false to locate subtasks in aiAction', async () => { // Create a spy on matchElementFromCache to verify it's not called - const matchElementFromCacheSpy = vi.spyOn(taskCache, 'matchLocateCache'); + const matchElementFromCacheSpy = rs.spyOn(taskCache, 'matchLocateCache'); // Mock planning result with a Locate action followed by Click // This simulates the typical aiAction behavior @@ -182,11 +182,11 @@ describe('aiAction cacheable option propagation', () => { it.skip('should propagate cacheable: false through action method', async () => { // This test verifies that the action method propagates cacheable: false to subtasks // We'll verify this through the convertPlanToExecutable method that's called internally - const convertPlanSpy = vi.spyOn(taskExecutor, 'convertPlanToExecutable'); + const convertPlanSpy = rs.spyOn(taskExecutor, 'convertPlanToExecutable'); // Mock the planning result // @ts-ignore: historical skipped test uses an old locate result shape. - vi.spyOn(mockService, 'locate').mockResolvedValue({ + rs.spyOn(mockService, 'locate').mockResolvedValue({ element: { description: 'element-id', center: [100, 100], @@ -330,7 +330,7 @@ describe('aiAction cacheable option propagation', () => { // TODO: Fix this test - Agent API changed, needs update to match new constructor it.skip('should fall through to normal execution when cache yamlWorkflow is undefined', async () => { // Mock matchPlanCache to return a cache entry with undefined yamlWorkflow - const matchPlanCacheSpy = vi + const matchPlanCacheSpy = rs .spyOn(taskCache, 'matchPlanCache') .mockReturnValue({ cacheContent: { @@ -339,11 +339,11 @@ describe('aiAction cacheable option propagation', () => { yamlWorkflow: undefined as any, }, cacheUsable: false, - updateFn: vi.fn(), + updateFn: rs.fn(), }); // Mock the action method to track if it gets called (normal execution path) - const actionSpy = vi + const actionSpy = rs .spyOn(taskExecutor, 'action') .mockResolvedValue({} as any); @@ -360,19 +360,19 @@ describe('aiAction cacheable option propagation', () => { }); // Mock the modelConfigManager to return valid config - vi.spyOn(agent as any, 'modelConfigManager', 'get').mockReturnValue({ + rs.spyOn(agent as any, 'modelConfigManager', 'get').mockReturnValue({ getModelConfig: (intent: string) => ({ baseUrl: 'https://test.com', apiKey: 'test-key', model: 'gpt-4o-mini', modelFamily: true, }), - throwErrorIfNonVLModel: vi.fn(), - getUploadTestServerUrl: vi.fn().mockReturnValue(undefined), + throwErrorIfNonVLModel: rs.fn(), + getUploadTestServerUrl: rs.fn().mockReturnValue(undefined), }); // Spy on runYaml to ensure it's NOT called with undefined - const runYamlSpy = vi.spyOn(agent, 'runYaml'); + const runYamlSpy = rs.spyOn(agent, 'runYaml'); // Call aiAct await agent.aiAct('test prompt'); @@ -390,7 +390,7 @@ describe('aiAction cacheable option propagation', () => { // TODO: Fix this test - Agent API changed, needs update to match new constructor it.skip('should fall through to normal execution when cache yamlWorkflow is empty string', async () => { // Mock matchPlanCache to return a cache entry with empty string yamlWorkflow - const matchPlanCacheSpy = vi + const matchPlanCacheSpy = rs .spyOn(taskCache, 'matchPlanCache') .mockReturnValue({ cacheContent: { @@ -399,11 +399,11 @@ describe('aiAction cacheable option propagation', () => { yamlWorkflow: '', }, cacheUsable: false, - updateFn: vi.fn(), + updateFn: rs.fn(), }); // Mock the action method to track if it gets called (normal execution path) - const actionSpy = vi + const actionSpy = rs .spyOn(taskExecutor, 'action') .mockResolvedValue({} as any); @@ -420,19 +420,19 @@ describe('aiAction cacheable option propagation', () => { }); // Mock the modelConfigManager to return valid config - vi.spyOn(agent as any, 'modelConfigManager', 'get').mockReturnValue({ + rs.spyOn(agent as any, 'modelConfigManager', 'get').mockReturnValue({ getModelConfig: (intent: string) => ({ baseUrl: 'https://test.com', apiKey: 'test-key', model: 'gpt-4o-mini', modelFamily: true, }), - throwErrorIfNonVLModel: vi.fn(), - getUploadTestServerUrl: vi.fn().mockReturnValue(undefined), + throwErrorIfNonVLModel: rs.fn(), + getUploadTestServerUrl: rs.fn().mockReturnValue(undefined), }); // Spy on runYaml to ensure it's NOT called with empty string - const runYamlSpy = vi.spyOn(agent, 'runYaml'); + const runYamlSpy = rs.spyOn(agent, 'runYaml'); // Call aiAct await agent.aiAct('test prompt'); @@ -450,7 +450,7 @@ describe('aiAction cacheable option propagation', () => { // TODO: Fix this test - Agent API changed, needs update to match new constructor it.skip('should fall through to normal execution when cache yamlWorkflow is whitespace-only', async () => { // Mock matchPlanCache to return a cache entry with whitespace-only yamlWorkflow - const matchPlanCacheSpy = vi + const matchPlanCacheSpy = rs .spyOn(taskCache, 'matchPlanCache') .mockReturnValue({ cacheContent: { @@ -459,11 +459,11 @@ describe('aiAction cacheable option propagation', () => { yamlWorkflow: ' \n\t ', }, cacheUsable: false, - updateFn: vi.fn(), + updateFn: rs.fn(), }); // Mock the action method to track if it gets called (normal execution path) - const actionSpy = vi + const actionSpy = rs .spyOn(taskExecutor, 'action') .mockResolvedValue({} as any); @@ -480,19 +480,19 @@ describe('aiAction cacheable option propagation', () => { }); // Mock the modelConfigManager to return valid config - vi.spyOn(agent as any, 'modelConfigManager', 'get').mockReturnValue({ + rs.spyOn(agent as any, 'modelConfigManager', 'get').mockReturnValue({ getModelConfig: (intent: string) => ({ baseUrl: 'https://test.com', apiKey: 'test-key', model: 'gpt-4o-mini', modelFamily: true, }), - throwErrorIfNonVLModel: vi.fn(), - getUploadTestServerUrl: vi.fn().mockReturnValue(undefined), + throwErrorIfNonVLModel: rs.fn(), + getUploadTestServerUrl: rs.fn().mockReturnValue(undefined), }); // Spy on runYaml to ensure it's NOT called with whitespace - const runYamlSpy = vi.spyOn(agent, 'runYaml'); + const runYamlSpy = rs.spyOn(agent, 'runYaml'); // Call aiAct await agent.aiAct('test prompt'); @@ -512,7 +512,7 @@ describe('aiAction cacheable option propagation', () => { const validYaml = 'actions:\n - type: Click\n thought: test'; // Mock matchPlanCache to return a cache entry with valid yamlWorkflow - const matchPlanCacheSpy = vi + const matchPlanCacheSpy = rs .spyOn(taskCache, 'matchPlanCache') .mockReturnValue({ cacheContent: { @@ -521,11 +521,11 @@ describe('aiAction cacheable option propagation', () => { yamlWorkflow: validYaml, }, cacheUsable: true, - updateFn: vi.fn(), + updateFn: rs.fn(), }); // Mock the action method - it should NOT be called when using cache - const actionSpy = vi + const actionSpy = rs .spyOn(taskExecutor, 'action') .mockResolvedValue({} as any); @@ -542,19 +542,19 @@ describe('aiAction cacheable option propagation', () => { }); // Mock the modelConfigManager to return valid config - vi.spyOn(agent as any, 'modelConfigManager', 'get').mockReturnValue({ + rs.spyOn(agent as any, 'modelConfigManager', 'get').mockReturnValue({ getModelConfig: (intent: string) => ({ baseUrl: 'https://test.com', apiKey: 'test-key', model: 'gpt-4o-mini', modelFamily: true, }), - throwErrorIfNonVLModel: vi.fn(), - getUploadTestServerUrl: vi.fn().mockReturnValue(undefined), + throwErrorIfNonVLModel: rs.fn(), + getUploadTestServerUrl: rs.fn().mockReturnValue(undefined), }); // Mock runYaml to avoid actual execution - const runYamlSpy = vi.spyOn(agent, 'runYaml').mockResolvedValue({} as any); + const runYamlSpy = rs.spyOn(agent, 'runYaml').mockResolvedValue({} as any); // Call aiAct await agent.aiAct('test prompt'); diff --git a/packages/core/tests/unit-test/bbox-locate-cache.test.ts b/packages/core/tests/unit-test/bbox-locate-cache.test.ts index e64810e724..bee9346962 100644 --- a/packages/core/tests/unit-test/bbox-locate-cache.test.ts +++ b/packages/core/tests/unit-test/bbox-locate-cache.test.ts @@ -21,7 +21,7 @@ import type Service from '@/service'; import type { ExecutionTask, ExecutionTaskApply, ServiceDump } from '@/types'; import type { IModelConfig } from '@midscene/shared/env'; import { uuid } from '@midscene/shared/utils'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { getMidsceneLocationSchema, z } from '../../src'; /** @@ -85,15 +85,15 @@ describe('bbox locate cache fix', () => { // Create mock interface with typed methods mockInterface = { interfaceType: 'web', - screenshotBase64: vi.fn().mockResolvedValue(validBase64Image), - size: vi.fn().mockResolvedValue({ width: 1920, height: 1080 }), - actionSpace: vi.fn().mockReturnValue([ + screenshotBase64: rs.fn().mockResolvedValue(validBase64Image), + size: rs.fn().mockResolvedValue({ width: 1920, height: 1080 }), + actionSpace: rs.fn().mockReturnValue([ { name: 'Tap', paramSchema: z.object({ locate: getMidsceneLocationSchema(), }), - call: vi.fn().mockResolvedValue({}), + call: rs.fn().mockResolvedValue({}), }, { name: 'Input', @@ -101,19 +101,19 @@ describe('bbox locate cache fix', () => { locate: getMidsceneLocationSchema(), value: z.string(), }), - call: vi.fn().mockResolvedValue({}), + call: rs.fn().mockResolvedValue({}), }, ]), - cacheFeatureForPoint: vi.fn().mockResolvedValue({ + cacheFeatureForPoint: rs.fn().mockResolvedValue({ xpaths: ['/html/body/input[1]'], texts: ['search box'], }), - rectMatchesCacheFeature: vi.fn().mockResolvedValue(undefined), + rectMatchesCacheFeature: rs.fn().mockResolvedValue(undefined), } as unknown as AbstractInterface; // Create mock service with typed methods mockService = { - contextRetrieverFn: vi.fn().mockImplementation(async () => { + contextRetrieverFn: rs.fn().mockImplementation(async () => { const screenshot = ScreenshotItem.create(validBase64Image, Date.now()); return { screenshot, @@ -126,7 +126,7 @@ describe('bbox locate cache fix', () => { }, }; }), - locate: vi.fn().mockResolvedValue({ + locate: rs.fn().mockResolvedValue({ element: { id: 'element-id', center: [500, 300], @@ -370,7 +370,7 @@ describe('bbox locate cache fix', () => { const locateTask = tasks.find((task) => task.subType === 'Locate'); // Clear the mock to track new calls - vi.mocked(mockInterface.cacheFeatureForPoint!).mockClear(); + rs.mocked(mockInterface.cacheFeatureForPoint!).mockClear(); await locateTask!.executor(locateTask!.param, { task: createRuntimeTask(locateTask!), @@ -383,7 +383,7 @@ describe('bbox locate cache fix', () => { }); it('should annotate AI locate usage with default intent while preserving raw slot', async () => { - vi.mocked(mockService.locate).mockResolvedValueOnce({ + rs.mocked(mockService.locate).mockResolvedValueOnce({ element: { id: 'element-id', center: [500, 300], @@ -469,7 +469,7 @@ describe('bbox locate cache fix', () => { }); // Mock rectMatchesCacheFeature to return a rect (simulating cache hit) - vi.mocked(mockInterface.rectMatchesCacheFeature!).mockResolvedValue({ + rs.mocked(mockInterface.rectMatchesCacheFeature!).mockResolvedValue({ left: 300, top: 400, width: 100, @@ -595,7 +595,7 @@ describe('bbox locate cache fix', () => { it('should handle cacheFeatureForPoint returning empty object', async () => { // Mock cacheFeatureForPoint to return empty object - vi.mocked(mockInterface.cacheFeatureForPoint!).mockResolvedValue({}); + rs.mocked(mockInterface.cacheFeatureForPoint!).mockResolvedValue({}); const plansWithBbox = [ { @@ -660,14 +660,14 @@ describe('bbox locate cache fix', () => { internal.cacheOriginalLength = 1; // 2. Mock rectMatchesCacheFeature to reject (simulates xpath validation failure) - vi.mocked(mockInterface.rectMatchesCacheFeature!).mockRejectedValue( + rs.mocked(mockInterface.rectMatchesCacheFeature!).mockRejectedValue( new Error( 'No matching element rect found for the provided cache feature', ), ); // 3. Mock AI locate to return new element with new xpath - vi.mocked(mockService.locate).mockResolvedValue({ + rs.mocked(mockService.locate).mockResolvedValue({ element: { description: 'new-element', center: [600, 400], @@ -677,7 +677,7 @@ describe('bbox locate cache fix', () => { }); // 4. Mock cacheFeatureForPoint to return new xpath - vi.mocked(mockInterface.cacheFeatureForPoint!).mockResolvedValue({ + rs.mocked(mockInterface.cacheFeatureForPoint!).mockResolvedValue({ xpaths: ['/html/body/div[2]/label[1]'], texts: ['高一'], }); @@ -758,12 +758,12 @@ describe('bbox locate cache fix', () => { internal.cacheOriginalLength = 1; // Mock validation failure - vi.mocked(mockInterface.rectMatchesCacheFeature!).mockRejectedValue( + rs.mocked(mockInterface.rectMatchesCacheFeature!).mockRejectedValue( new Error('Element not found'), ); // Mock AI locate success with new xpath - vi.mocked(mockService.locate).mockResolvedValue({ + rs.mocked(mockService.locate).mockResolvedValue({ element: { description: 'submit-btn', center: [500, 300], @@ -772,7 +772,7 @@ describe('bbox locate cache fix', () => { dump: mockServiceDump, }); - vi.mocked(mockInterface.cacheFeatureForPoint!).mockResolvedValue({ + rs.mocked(mockInterface.cacheFeatureForPoint!).mockResolvedValue({ xpaths: ['/html/body/form[1]/button[1]'], texts: ['Submit'], }); diff --git a/packages/core/tests/unit-test/codex-app-server-provider.test.ts b/packages/core/tests/unit-test/codex-app-server-provider.test.ts index 2f7fbc7bfd..5736e075c1 100644 --- a/packages/core/tests/unit-test/codex-app-server-provider.test.ts +++ b/packages/core/tests/unit-test/codex-app-server-provider.test.ts @@ -1,14 +1,17 @@ -import { EventEmitter } from 'node:events'; +import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; import { __shutdownCodexAppServerForTests, buildCodexTurnPayloadFromMessages, + callAIWithCodexAppServer, isCodexAppServerProvider, normalizeCodexLocalImagePath, resolveCodexReasoningEffort, } from '@/ai-model/service-caller/codex-app-server'; import type { IModelConfig } from '@midscene/shared/env'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; import type { ChatCompletionMessageParam } from 'openai/resources/index'; -import { afterEach, describe, expect, it, vi } from 'vitest'; const baseModelConfig: IModelConfig = { modelName: 'gpt-5.4', @@ -17,13 +20,24 @@ const baseModelConfig: IModelConfig = { slot: 'default', }; +const temporaryDirectories: string[] = []; + +const createTemporaryDirectory = async () => { + const directory = await mkdtemp(path.join(tmpdir(), 'midscene-codex-test-')); + temporaryDirectories.push(directory); + return directory; +}; + describe('codex app-server provider helper', () => { afterEach(async () => { await __shutdownCodexAppServerForTests(); - vi.restoreAllMocks(); - vi.resetModules(); - vi.unmock('node:child_process'); - vi.unmock('node:readline'); + rs.unstubAllEnvs(); + rs.restoreAllMocks(); + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true })), + ); }); it('detects codex provider base url', () => { @@ -275,172 +289,67 @@ describe('codex app-server provider helper', () => { }); it('surfaces codex spawn errors as regular model errors', async () => { - vi.resetModules(); - - const lineReader = new EventEmitter() as EventEmitter & { - close: ReturnType; - on: EventEmitter['on']; - }; - lineReader.close = vi.fn(); - - const stdout = new EventEmitter() as EventEmitter & { - unref: ReturnType; - }; - stdout.unref = vi.fn(); - - const stderr = new EventEmitter() as EventEmitter & { - unref: ReturnType; - }; - stderr.unref = vi.fn(); - - const stdin = { - end: vi.fn(), - unref: vi.fn(), - write: vi.fn( - ( - _line: string, - callback?: (error?: Error | null | undefined) => void, - ) => { - callback?.(null); - return true; - }, - ), - }; - - const child = new EventEmitter() as EventEmitter & { - stdin: typeof stdin; - stdout: typeof stdout; - stderr: typeof stderr; - kill: ReturnType; - unref: ReturnType; - }; - child.stdin = stdin; - child.stdout = stdout; - child.stderr = stderr; - child.kill = vi.fn(); - child.unref = vi.fn(); - - vi.doMock('node:child_process', () => ({ - spawn: vi.fn(() => { - queueMicrotask(() => { - child.emit('error', new Error('spawn ENOENT')); - }); - return child; - }), - })); - - vi.doMock('node:readline', () => ({ - createInterface: vi.fn(() => lineReader), - })); - - const mockedModule = await import( - '@/ai-model/service-caller/codex-app-server' - ); + rs.stubEnv('PATH', await createTemporaryDirectory()); await expect( - mockedModule.callAIWithCodexAppServer( + callAIWithCodexAppServer( [{ role: 'user', content: 'hello' }], baseModelConfig, ), - ).rejects.toThrow(/codex app-server process error: spawn ENOENT/); + ).rejects.toThrow( + /(?:codex app-server process error: spawn codex ENOENT|failed writing to codex app-server stdin: write EPIPE)/, + ); }); it('reports Codex JSON-RPC requests, responses, and turn notifications', async () => { - vi.resetModules(); - - const lineReader = new EventEmitter() as EventEmitter & { - close: ReturnType; - on: EventEmitter['on']; - }; - lineReader.close = vi.fn(); - - const stdout = new EventEmitter() as EventEmitter & { - unref: ReturnType; - }; - stdout.unref = vi.fn(); - - const stderr = new EventEmitter() as EventEmitter & { - unref: ReturnType; - }; - stderr.unref = vi.fn(); - - const sendResponse = (id: number, result: unknown) => { - queueMicrotask(() => { - lineReader.emit('line', JSON.stringify({ id, result })); - }); - }; - const stdin = { - end: vi.fn(), - unref: vi.fn(), - write: vi.fn( - ( - line: string, - callback?: (error?: Error | null | undefined) => void, - ) => { - const message = JSON.parse(line); - if (message.method === 'initialize') { - sendResponse(message.id, {}); - } else if (message.method === 'thread/start') { - sendResponse(message.id, { thread: { id: 'thread-1' } }); - } else if (message.method === 'turn/start') { - sendResponse(message.id, { turn: { id: 'turn-1' } }); - queueMicrotask(() => { - lineReader.emit( - 'line', - JSON.stringify({ - method: 'item/agentMessage/delta', - params: { - threadId: 'thread-1', - turnId: 'turn-1', - delta: 'hello', - }, - }), - ); - lineReader.emit( - 'line', - JSON.stringify({ - method: 'turn/completed', - params: { - threadId: 'thread-1', - turn: { id: 'turn-1', status: 'completed' }, - }, - }), - ); - }); - } else if (message.method === 'thread/unsubscribe') { - sendResponse(message.id, {}); - } - callback?.(null); - return true; - }, - ), - }; - - const child = new EventEmitter() as EventEmitter & { - stdin: typeof stdin; - stdout: typeof stdout; - stderr: typeof stderr; - kill: ReturnType; - unref: ReturnType; - }; - child.stdin = stdin; - child.stdout = stdout; - child.stderr = stderr; - child.kill = vi.fn(); - child.unref = vi.fn(); - - vi.doMock('node:child_process', () => ({ - spawn: vi.fn(() => child), - })); - vi.doMock('node:readline', () => ({ - createInterface: vi.fn(() => lineReader), - })); - - const mockedModule = await import( - '@/ai-model/service-caller/codex-app-server' + const executableDirectory = await createTemporaryDirectory(); + const serverPath = path.join(executableDirectory, 'codex-server.cjs'); + await writeFile( + serverPath, + `const readline = require('node:readline').createInterface({ input: process.stdin }); +const send = (message) => process.stdout.write(JSON.stringify(message) + '\\n'); +readline.on('line', (line) => { + const message = JSON.parse(line); + if (message.method === 'initialize') { + send({ id: message.id, result: {} }); + } else if (message.method === 'thread/start') { + send({ id: message.id, result: { thread: { id: 'thread-1' } } }); + } else if (message.method === 'turn/start') { + send({ id: message.id, result: { turn: { id: 'turn-1' } } }); + send({ + method: 'item/agentMessage/delta', + params: { threadId: 'thread-1', turnId: 'turn-1', delta: 'hello' }, + }); + send({ + method: 'turn/completed', + params: { + threadId: 'thread-1', + turn: { id: 'turn-1', status: 'completed' }, + }, + }); + } else if (message.method === 'thread/unsubscribe') { + send({ id: message.id, result: {} }); + } +}); +`, ); + const executablePath = path.join(executableDirectory, 'codex'); + await writeFile( + executablePath, + "#!/usr/bin/env node\nrequire('./codex-server.cjs');\n", + ); + await chmod(executablePath, 0o755); + await writeFile( + path.join(executableDirectory, 'codex.cmd'), + `@"${process.execPath}" "%~dp0\\codex-server.cjs" %*\r\n`, + ); + rs.stubEnv( + 'PATH', + `${executableDirectory}${path.delimiter}${process.env.PATH ?? ''}`, + ); + const events: unknown[] = []; - const result = await mockedModule.callAIWithCodexAppServer( + const result = await callAIWithCodexAppServer( [{ role: 'user', content: 'hello' }], baseModelConfig, { onRecordEvent: (event) => events.push(event) }, @@ -494,6 +403,6 @@ describe('codex app-server provider helper', () => { ]), ); - await mockedModule.__shutdownCodexAppServerForTests(); + await __shutdownCodexAppServerForTests(); }); }); diff --git a/packages/core/tests/unit-test/common-context-parser-orientation.test.ts b/packages/core/tests/unit-test/common-context-parser-orientation.test.ts index a7bc23f5de..4f509ad290 100644 --- a/packages/core/tests/unit-test/common-context-parser-orientation.test.ts +++ b/packages/core/tests/unit-test/common-context-parser-orientation.test.ts @@ -1,39 +1,39 @@ import { commonContextParser } from '@/agent/utils'; import type { AbstractInterface } from '@/device'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; // Mock imageInfoOfBase64 to control screenshot dimensions -vi.mock('@midscene/shared/img', () => ({ - createImgBase64ByFormat: vi.fn(), - imageInfoOfBase64: vi.fn(), - resizeBase64ImageToJpeg: vi +rs.mock('@midscene/shared/img', () => ({ + createImgBase64ByFormat: rs.fn(), + imageInfoOfBase64: rs.fn(), + resizeBase64ImageToJpeg: rs .fn() .mockResolvedValue('data:image/jpeg;base64,mock-resized-base64-data'), })); import { imageInfoOfBase64 } from '@midscene/shared/img'; -const mockedImageInfo = vi.mocked(imageInfoOfBase64); +const mockedImageInfo = rs.mocked(imageInfoOfBase64); function createMockInterface( logicalWidth: number, logicalHeight: number, ): AbstractInterface { return { - screenshotBase64: vi + screenshotBase64: rs .fn() .mockResolvedValue('data:image/jpeg;base64,mock-base64-data'), - size: vi + size: rs .fn() .mockResolvedValue({ width: logicalWidth, height: logicalHeight }), - actionSpace: vi.fn(() => []), - describe: vi.fn(() => ''), + actionSpace: rs.fn(() => []), + describe: rs.fn(() => ''), } as unknown as AbstractInterface; } describe('commonContextParser orientation mismatch detection', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('should compute correct dpr when logical size and screenshot have same orientation (both portrait)', async () => { diff --git a/packages/core/tests/unit-test/common-context-parser-shrink-factor.test.ts b/packages/core/tests/unit-test/common-context-parser-shrink-factor.test.ts index 2b9ca7f747..d720f1ad2b 100644 --- a/packages/core/tests/unit-test/common-context-parser-shrink-factor.test.ts +++ b/packages/core/tests/unit-test/common-context-parser-shrink-factor.test.ts @@ -1,11 +1,11 @@ import { commonContextParser } from '@/agent/utils'; import type { AbstractInterface } from '@/device'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -vi.mock('@midscene/shared/img', () => ({ - createImgBase64ByFormat: vi.fn(), - imageInfoOfBase64: vi.fn(), - resizeBase64ImageToJpeg: vi +rs.mock('@midscene/shared/img', () => ({ + createImgBase64ByFormat: rs.fn(), + imageInfoOfBase64: rs.fn(), + resizeBase64ImageToJpeg: rs .fn() .mockResolvedValue('data:image/jpeg;base64,mock-resized-base64-data'), })); @@ -16,26 +16,26 @@ import { } from '@midscene/shared/img'; const mockScreenshotBase64 = 'data:image/png;base64,mock-base64-data'; -const mockedImageInfo = vi.mocked(imageInfoOfBase64); -const mockedResizeToJpeg = vi.mocked(resizeBase64ImageToJpeg); +const mockedImageInfo = rs.mocked(imageInfoOfBase64); +const mockedResizeToJpeg = rs.mocked(resizeBase64ImageToJpeg); function createMockInterface( logicalWidth: number, logicalHeight: number, ): AbstractInterface { return { - screenshotBase64: vi.fn().mockResolvedValue(mockScreenshotBase64), - size: vi + screenshotBase64: rs.fn().mockResolvedValue(mockScreenshotBase64), + size: rs .fn() .mockResolvedValue({ width: logicalWidth, height: logicalHeight }), - actionSpace: vi.fn(() => []), - describe: vi.fn(() => ''), + actionSpace: rs.fn(() => []), + describe: rs.fn(() => ''), } as unknown as AbstractInterface; } describe('commonContextParser screenshotShrinkFactor', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('converts PNG screenshots to JPEG quality 90 when not shrinking', async () => { diff --git a/packages/core/tests/unit-test/connectivity-service-cycle.test.ts b/packages/core/tests/unit-test/connectivity-service-cycle.test.ts index 23d2b1c94d..f1809c440d 100644 --- a/packages/core/tests/unit-test/connectivity-service-cycle.test.ts +++ b/packages/core/tests/unit-test/connectivity-service-cycle.test.ts @@ -1,37 +1,37 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import type { IModelConfig } from '@midscene/shared/env'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import ts from 'typescript'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -const mocks = vi.hoisted(() => ({ - callAI: vi.fn(), - callAIWithObjectResponse: vi.fn(), - AiExtractElementInfo: vi.fn(), - AiLocateElement: vi.fn(), - AiLocateSection: vi.fn(), - buildSearchAreaConfig: vi.fn(), + +const mocks = rs.hoisted(() => ({ + callAI: rs.fn(), + callAIWithObjectResponse: rs.fn(), + AiExtractElementInfo: rs.fn(), + AiLocateElement: rs.fn(), + AiLocateSection: rs.fn(), + buildSearchAreaConfig: rs.fn(), })); -vi.mock('@/ai-model/service-caller', () => ({ +rs.mock('@/ai-model/service-caller', () => ({ AIResponseParseError: class AIResponseParseError extends Error {}, callAI: mocks.callAI, callAIWithObjectResponse: mocks.callAIWithObjectResponse, })); -vi.mock('@/ai-model/service-caller/index', () => ({ +rs.mock('@/ai-model/service-caller/index', () => ({ AIResponseParseError: class AIResponseParseError extends Error {}, callAI: mocks.callAI, callAIWithObjectResponse: mocks.callAIWithObjectResponse, })); -vi.mock('@/ai-model/workflows/grounding', () => ({ +rs.mock('@/ai-model/workflows/grounding', () => ({ AiLocateElement: mocks.AiLocateElement, AiLocateSection: mocks.AiLocateSection, buildSearchAreaConfig: mocks.buildSearchAreaConfig, })); -vi.mock('@/ai-model/workflows/insight', () => ({ +rs.mock('@/ai-model/workflows/insight', () => ({ AiExtractElementInfo: mocks.AiExtractElementInfo, })); @@ -87,7 +87,7 @@ describe('runConnectivityTest service load order', () => { }; beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('runs the default locate check through the real Service constructor', async () => { diff --git a/packages/core/tests/unit-test/connectivity.test.ts b/packages/core/tests/unit-test/connectivity.test.ts index e2d162a064..8d779949d8 100644 --- a/packages/core/tests/unit-test/connectivity.test.ts +++ b/packages/core/tests/unit-test/connectivity.test.ts @@ -1,14 +1,14 @@ import type { IModelConfig } from '@midscene/shared/env'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import sharp from 'sharp'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -vi.mock('@/ai-model/service-caller/index', () => ({ - callAI: vi.fn(), +rs.mock('@/ai-model/service-caller/index', () => ({ + callAI: rs.fn(), })); -vi.mock('@/service', () => ({ - default: vi.fn().mockImplementation(() => ({ - locate: vi.fn(), +rs.mock('@/service', () => ({ + default: rs.fn().mockImplementation(() => ({ + locate: rs.fn(), })), })); @@ -59,7 +59,7 @@ describe('runConnectivityTest', () => { }; beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('keeps the fixture shot size aligned with the embedded PNG', async () => { @@ -69,11 +69,11 @@ describe('runConnectivityTest', () => { }); it('returns passed when all checks succeed', async () => { - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce({ content: 'CONNECTIVITY_OK' } as any) .mockResolvedValueOnce({ content: 'What needs to be done?' } as any); - const locate = vi.fn().mockResolvedValue({ + const locate = rs.fn().mockResolvedValue({ rect: { left: 120, top: 90, width: 360, height: 60 }, element: { center: [300, 120], @@ -81,7 +81,7 @@ describe('runConnectivityTest', () => { description: 'main todo input box', }, }); - vi.mocked(Service).mockImplementation( + rs.mocked(Service).mockImplementation( () => ({ locate, @@ -106,7 +106,7 @@ describe('runConnectivityTest', () => { }), }), ); - expect(vi.mocked(callAI).mock.calls[0]?.[1]).toEqual( + expect(rs.mocked(callAI).mock.calls[0]?.[1]).toEqual( expect.objectContaining({ config: expect.objectContaining({ ...planningModelConfig, @@ -114,7 +114,7 @@ describe('runConnectivityTest', () => { }), }), ); - expect(vi.mocked(callAI).mock.calls[1]?.[1]).toEqual( + expect(rs.mocked(callAI).mock.calls[1]?.[1]).toEqual( expect.objectContaining({ config: expect.objectContaining({ ...insightModelConfig, @@ -125,7 +125,7 @@ describe('runConnectivityTest', () => { expect(defaultModelConfig.retryCount).toBe(3); expect(planningModelConfig.retryCount).toBe(3); expect(insightModelConfig.retryCount).toBe(3); - const visionCall = vi.mocked(callAI).mock.calls[1]?.[0]?.[0]; + const visionCall = rs.mocked(callAI).mock.calls[1]?.[0]?.[0]; expect(visionCall).toMatchObject({ role: 'user', content: expect.arrayContaining([ @@ -140,11 +140,11 @@ describe('runConnectivityTest', () => { }); it('marks individual failures without throwing', async () => { - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce({ content: 'wrong-token' } as any) .mockRejectedValueOnce(new Error('vision failed')); - const locate = vi.fn().mockResolvedValue({ + const locate = rs.fn().mockResolvedValue({ rect: { left: 10, top: 10, width: 20, height: 20 }, element: { center: [20, Number.NaN], @@ -152,7 +152,7 @@ describe('runConnectivityTest', () => { description: 'wrong target', }, }); - vi.mocked(Service).mockImplementation( + rs.mocked(Service).mockImplementation( () => ({ locate, diff --git a/packages/core/tests/unit-test/conversation-history.test.ts b/packages/core/tests/unit-test/conversation-history.test.ts index 2709c5e826..51cd4fbbb6 100644 --- a/packages/core/tests/unit-test/conversation-history.test.ts +++ b/packages/core/tests/unit-test/conversation-history.test.ts @@ -1,6 +1,6 @@ import { ConversationHistory } from '@/ai-model'; +import { describe, expect, it } from '@rstest/core'; import type { ChatCompletionMessageParam } from 'openai/resources/index'; -import { describe, expect, it } from 'vitest'; const userMessage = (content: string) => ({ role: 'user' as const, diff --git a/packages/core/tests/unit-test/custom-planning.test.ts b/packages/core/tests/unit-test/custom-planning.test.ts index 5f9e63914e..eda98becce 100644 --- a/packages/core/tests/unit-test/custom-planning.test.ts +++ b/packages/core/tests/unit-test/custom-planning.test.ts @@ -1,7 +1,7 @@ import { ConversationHistory } from '@/ai-model/workflows/planning/conversation-history'; import { buildCustomPlanningMessages } from '@/ai-model/workflows/planning/custom-planning'; import type { PlanOptions } from '@/ai-model/workflows/planning/types'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; function createPlanOptions( conversationHistory = new ConversationHistory(), diff --git a/packages/core/tests/unit-test/device-options.test.ts b/packages/core/tests/unit-test/device-options.test.ts index d86f6e0313..2b53df4709 100644 --- a/packages/core/tests/unit-test/device-options.test.ts +++ b/packages/core/tests/unit-test/device-options.test.ts @@ -10,7 +10,7 @@ import type { MidsceneYamlScriptAndroidEnv, MidsceneYamlScriptIOSEnv, } from '@/yaml'; -import { describe, expect, test } from 'vitest'; +import { describe, expect, test } from '@rstest/core'; describe('Device Options Type Definitions', () => { describe('AndroidDeviceOpt', () => { diff --git a/packages/core/tests/unit-test/device/input-mode.test.ts b/packages/core/tests/unit-test/device/input-mode.test.ts index 8fd308bff4..a66c459f51 100644 --- a/packages/core/tests/unit-test/device/input-mode.test.ts +++ b/packages/core/tests/unit-test/device/input-mode.test.ts @@ -1,12 +1,12 @@ import { defineActionInput } from '@/device'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; describe('Input action with mode option', () => { const mockContext = {} as any; const createInputAction = ( - clearInputMock: ReturnType, - typeTextMock: ReturnType, + clearInputMock: ReturnType, + typeTextMock: ReturnType, ) => defineActionInput({ clearInput: async (target) => { @@ -19,8 +19,8 @@ describe('Input action with mode option', () => { }); it('should request replace when mode is replace', async () => { - const clearInputMock = vi.fn(); - const typeTextMock = vi.fn(); + const clearInputMock = rs.fn(); + const typeTextMock = rs.fn(); const inputAction = createInputAction(clearInputMock, typeTextMock); @@ -42,8 +42,8 @@ describe('Input action with mode option', () => { }); it('should only clear input when mode is clear', async () => { - const clearInputMock = vi.fn(); - const typeTextMock = vi.fn(); + const clearInputMock = rs.fn(); + const typeTextMock = rs.fn(); const inputAction = createInputAction(clearInputMock, typeTextMock); @@ -62,8 +62,8 @@ describe('Input action with mode option', () => { }); it('should skip clearInput when mode is typeOnly', async () => { - const clearInputMock = vi.fn(); - const typeTextMock = vi.fn(); + const clearInputMock = rs.fn(); + const typeTextMock = rs.fn(); const inputAction = createInputAction(clearInputMock, typeTextMock); @@ -85,8 +85,8 @@ describe('Input action with mode option', () => { }); it('should request replace by default when mode is not specified', async () => { - const clearInputMock = vi.fn(); - const typeTextMock = vi.fn(); + const clearInputMock = rs.fn(); + const typeTextMock = rs.fn(); const inputAction = createInputAction(clearInputMock, typeTextMock); diff --git a/packages/core/tests/unit-test/device/input-primitives.test.ts b/packages/core/tests/unit-test/device/input-primitives.test.ts index 933c22bc6e..4568278a3b 100644 --- a/packages/core/tests/unit-test/device/input-primitives.test.ts +++ b/packages/core/tests/unit-test/device/input-primitives.test.ts @@ -1,14 +1,14 @@ import { defineActionsFromInputPrimitives } from '@/device'; import type { ExecutorContext } from '@/types'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const mockExecutorContext = { task: {} } as ExecutorContext; describe('defineActionsFromInputPrimitives', () => { it('should expose configured system input primitives as actions', async () => { - const backButton = vi.fn(); - const homeButton = vi.fn(); - const recentAppsButton = vi.fn(); + const backButton = rs.fn(); + const homeButton = rs.fn(); + const recentAppsButton = rs.fn(); const actions = defineActionsFromInputPrimitives( { diff --git a/packages/core/tests/unit-test/dump-screenshot-sequence.test.ts b/packages/core/tests/unit-test/dump-screenshot-sequence.test.ts index 7dba14babe..57d8edc0f4 100644 --- a/packages/core/tests/unit-test/dump-screenshot-sequence.test.ts +++ b/packages/core/tests/unit-test/dump-screenshot-sequence.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { ScreenshotItem } from '../../src/screenshot-item'; import { ExecutionDump, diff --git a/packages/core/tests/unit-test/dump-utils.test.ts b/packages/core/tests/unit-test/dump-utils.test.ts index fececfb30a..0443347c20 100644 --- a/packages/core/tests/unit-test/dump-utils.test.ts +++ b/packages/core/tests/unit-test/dump-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { type StoredImageRef, createInlineImageResolver, diff --git a/packages/core/tests/unit-test/execution-dump.test.ts b/packages/core/tests/unit-test/execution-dump.test.ts index 871d45a317..4797fd0659 100644 --- a/packages/core/tests/unit-test/execution-dump.test.ts +++ b/packages/core/tests/unit-test/execution-dump.test.ts @@ -1,5 +1,5 @@ import { TaskRunner } from '@/task-runner'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { ScreenshotItem } from '../../src/screenshot-item'; import { ExecutionDump, diff --git a/packages/core/tests/unit-test/extraction.test.ts b/packages/core/tests/unit-test/extraction.test.ts index ee3c5c7621..72385f61c7 100644 --- a/packages/core/tests/unit-test/extraction.test.ts +++ b/packages/core/tests/unit-test/extraction.test.ts @@ -1,5 +1,5 @@ import { parseXMLExtractionResponse } from '@/ai-model/workflows/insight/extraction-parser'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('parseXMLExtractionResponse', () => { it('should parse complete XML response with all fields', () => { diff --git a/packages/core/tests/unit-test/file-chooser-accept-path.test.ts b/packages/core/tests/unit-test/file-chooser-accept-path.test.ts index 36fcf245bf..cc7aa4b7c9 100644 --- a/packages/core/tests/unit-test/file-chooser-accept-path.test.ts +++ b/packages/core/tests/unit-test/file-chooser-accept-path.test.ts @@ -2,7 +2,7 @@ import { unlinkSync, writeFileSync } from 'node:fs'; import { join, relative, resolve } from 'node:path'; import { Agent } from '@/agent'; import { normalizeFilePaths } from '@/agent/utils'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from '@rstest/core'; const createMockInterface = () => ({ diff --git a/packages/core/tests/unit-test/freeze-context.test.ts b/packages/core/tests/unit-test/freeze-context.test.ts index 88f97aa610..9bf4315b84 100644 --- a/packages/core/tests/unit-test/freeze-context.test.ts +++ b/packages/core/tests/unit-test/freeze-context.test.ts @@ -2,20 +2,20 @@ import { Agent as PageAgent, commonContextParser } from '@/agent'; import type { AbstractInterface } from '@/device'; import { ScreenshotItem } from '@/screenshot-item'; import type { UIContext } from '@/types'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; // Mock page implementation const mockPage = { interfaceType: 'puppeteer', mouse: { - click: vi.fn(), + click: rs.fn(), }, - actionSpace: vi.fn(() => []), - screenshotBase64: vi.fn().mockResolvedValue('mock-screenshot'), - evaluateJavaScript: vi.fn(), - size: vi.fn().mockResolvedValue({ width: 1920, height: 1080 }), - url: vi.fn().mockResolvedValue('https://example.com'), - getContext: vi.fn().mockImplementation(async function ( + actionSpace: rs.fn(() => []), + screenshotBase64: rs.fn().mockResolvedValue('mock-screenshot'), + evaluateJavaScript: rs.fn(), + size: rs.fn().mockResolvedValue({ width: 1920, height: 1080 }), + url: rs.fn().mockResolvedValue('https://example.com'), + getContext: rs.fn().mockImplementation(async function ( this: AbstractInterface, ) { return await commonContextParser(this, {}); @@ -34,7 +34,7 @@ describe('PageAgent freeze/unfreeze page context', () => { let mockContext2: UIContext; beforeEach(async () => { - vi.clearAllMocks(); + rs.clearAllMocks(); // Create mock contexts mockContext = { @@ -84,7 +84,7 @@ describe('PageAgent freeze/unfreeze page context', () => { // Mock _snapshotContext method to return different contexts on successive calls let callCount = 0; - vi.spyOn(agent, '_snapshotContext').mockImplementation(async () => { + rs.spyOn(agent, '_snapshotContext').mockImplementation(async () => { callCount++; return callCount === 1 ? mockContext : mockContext2; }); @@ -176,7 +176,7 @@ describe('PageAgent freeze/unfreeze page context', () => { }); // Mock second agent's _snapshotContext - vi.spyOn(agent2, '_snapshotContext').mockResolvedValue(mockContext2); + rs.spyOn(agent2, '_snapshotContext').mockResolvedValue(mockContext2); // Freeze context for agent1 only await agent.freezePageContext(); @@ -240,8 +240,8 @@ describe('PageAgent freeze/unfreeze page context', () => { describe('getUIContext with frozen context', () => { it('should return frozen context for all actions when frozen', async () => { // Mock commonContextParser to return a new context each time - const mockParseContext = vi.fn().mockResolvedValue(mockContext2); - vi.spyOn( + const mockParseContext = rs.fn().mockResolvedValue(mockContext2); + rs.spyOn( await import('@/agent/utils'), 'commonContextParser', ).mockImplementation(mockParseContext); @@ -272,13 +272,13 @@ describe('PageAgent freeze/unfreeze page context', () => { it('should return fresh context for all actions when not frozen', async () => { // Mock commonContextParser - const mockParseContext = vi + const mockParseContext = rs .fn() .mockResolvedValueOnce({ ...mockContext, fresh: 1 }) .mockResolvedValueOnce({ ...mockContext, fresh: 2 }) .mockResolvedValueOnce({ ...mockContext, fresh: 3 }); - vi.spyOn( + rs.spyOn( await import('@/agent/utils'), 'commonContextParser', ).mockImplementation(mockParseContext); @@ -299,12 +299,12 @@ describe('PageAgent freeze/unfreeze page context', () => { it('should switch between frozen and fresh contexts correctly', async () => { // Mock commonContextParser - const mockParseContext = vi + const mockParseContext = rs .fn() .mockResolvedValueOnce({ ...mockContext2, callNumber: 1 }) .mockResolvedValueOnce({ ...mockContext2, callNumber: 2 }); - vi.spyOn( + rs.spyOn( await import('@/agent/utils'), 'commonContextParser', ).mockImplementation(mockParseContext); @@ -334,8 +334,8 @@ describe('PageAgent freeze/unfreeze page context', () => { it('should handle extract and assert actions correctly when frozen', async () => { // Mock commonContextParser - const mockParseContext = vi.fn().mockResolvedValue(mockContext2); - vi.spyOn( + const mockParseContext = rs.fn().mockResolvedValue(mockContext2); + rs.spyOn( await import('@/agent/utils'), 'commonContextParser', ).mockImplementation(mockParseContext); diff --git a/packages/core/tests/unit-test/gpt-image-detail.test.ts b/packages/core/tests/unit-test/gpt-image-detail.test.ts index 8820872d88..9c2e549556 100644 --- a/packages/core/tests/unit-test/gpt-image-detail.test.ts +++ b/packages/core/tests/unit-test/gpt-image-detail.test.ts @@ -1,12 +1,12 @@ import { getModelRuntime } from '@/ai-model/models'; import { callAI } from '@/ai-model/service-caller'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -const mockCreate = vi.fn(); +const mockCreate = rs.fn(); -vi.mock('openai', () => ({ - default: vi.fn().mockImplementation(() => ({ +rs.mock('openai', () => ({ + default: rs.fn().mockImplementation(() => ({ chat: { completions: { create: mockCreate, diff --git a/packages/core/tests/unit-test/grounding-locate-not-found.test.ts b/packages/core/tests/unit-test/grounding-locate-not-found.test.ts index b6ce9ab90c..0f1543d9d2 100644 --- a/packages/core/tests/unit-test/grounding-locate-not-found.test.ts +++ b/packages/core/tests/unit-test/grounding-locate-not-found.test.ts @@ -7,18 +7,17 @@ import { } from '@/ai-model/workflows/grounding'; import type { LocateFn } from '@/ai-model/workflows/grounding/types'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { createFakeContext } from '../utils'; -vi.mock('@/ai-model/service-caller/index', async () => { - const actual = await vi.importActual< - typeof import('@/ai-model/service-caller/index') - >('@/ai-model/service-caller/index'); - return { - ...actual, - callAI: vi.fn(), - }; -}); +import * as serviceCallerActual from '@/ai-model/service-caller/index' with { + rstest: 'importActual', +}; + +rs.mock('@/ai-model/service-caller/index', () => ({ + ...serviceCallerActual, + callAI: rs.fn(), +})); describe('grounding locate not-found parsing', () => { const modelConfig: IModelConfig = { @@ -32,12 +31,12 @@ describe('grounding locate not-found parsing', () => { }; beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(callAI).mockResolvedValue({ content: '{}', isStreamed: false }); + rs.clearAllMocks(); + rs.mocked(callAI).mockResolvedValue({ content: '{}', isStreamed: false }); }); it('keeps locate errors without parsing coordinates when result key is missing', async () => { - vi.mocked(callAI).mockResolvedValue({ + rs.mocked(callAI).mockResolvedValue({ content: '{"error":"target element is not found"}', isStreamed: false, }); @@ -70,7 +69,7 @@ describe('grounding locate not-found parsing', () => { }); it('skips coordinate parsing when result key is an empty array', async () => { - vi.mocked(callAI).mockResolvedValue({ + rs.mocked(callAI).mockResolvedValue({ content: '{"bbox":[],"error":"target element is not found"}', isStreamed: false, }); @@ -89,7 +88,7 @@ describe('grounding locate not-found parsing', () => { }); it('retries once when result codec cannot map coordinates', async () => { - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce({ content: '{"bbox":[100,null,300,400],"error":"model returned invalid coordinates"}', @@ -107,11 +106,11 @@ describe('grounding locate not-found parsing', () => { }); expect(callAI).toHaveBeenCalledTimes(2); - expect(vi.mocked(callAI).mock.calls.map((call) => call[2])).toEqual([ + expect(rs.mocked(callAI).mock.calls.map((call) => call[2])).toEqual([ expect.objectContaining({ semanticRetryAttempt: 0 }), expect.objectContaining({ semanticRetryAttempt: 1 }), ]); - const retryFeedback = vi.mocked(callAI).mock.calls[1][0].at(-1); + const retryFeedback = rs.mocked(callAI).mock.calls[1][0].at(-1); expect(retryFeedback).toMatchObject({ role: 'user' }); expect(retryFeedback?.content).toEqual( expect.stringContaining('coordinate parsing error'), @@ -121,7 +120,7 @@ describe('grounding locate not-found parsing', () => { }); it('includes model errors when coordinate parsing ultimately fails', async () => { - vi.mocked(callAI).mockResolvedValue({ + rs.mocked(callAI).mockResolvedValue({ content: '{"bbox":[100,null,300,400],"error":"model returned invalid coordinates"}', isStreamed: false, @@ -143,7 +142,7 @@ describe('grounding locate not-found parsing', () => { }); it('retries JSON parsing through the same locate retry loop', async () => { - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce({ content: '```', isStreamed: false, @@ -164,7 +163,7 @@ describe('grounding locate not-found parsing', () => { }); it('retries once when search-area result codec cannot map coordinates', async () => { - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce({ content: '{"bbox":[100,null,300,400]}', isStreamed: false, @@ -181,7 +180,7 @@ describe('grounding locate not-found parsing', () => { }); expect(callAI).toHaveBeenCalledTimes(2); - const retryFeedback = vi.mocked(callAI).mock.calls[1][0].at(-1); + const retryFeedback = rs.mocked(callAI).mock.calls[1][0].at(-1); expect(retryFeedback).toMatchObject({ role: 'user' }); expect(retryFeedback?.content).toEqual( expect.stringContaining('coordinate parsing error'), @@ -190,7 +189,7 @@ describe('grounding locate not-found parsing', () => { }); it('passes locate request context to custom locate and maps its bbox result', async () => { - const locateFn = vi.fn().mockResolvedValue({ + const locateFn = rs.fn().mockResolvedValue({ locatedPixelBbox: [100, 50, 130, 70], rawResponse: 'custom locate response', usage: { total_tokens: 12 } as any, @@ -265,7 +264,7 @@ describe('grounding locate not-found parsing', () => { }); it('keeps section locate error without parsing coordinates when result key is missing', async () => { - vi.mocked(callAI).mockResolvedValue({ + rs.mocked(callAI).mockResolvedValue({ content: '{"error":"target section is not found"}', isStreamed: false, }); @@ -281,7 +280,7 @@ describe('grounding locate not-found parsing', () => { }); it('keeps section locate error without parsing coordinates when result key is an empty array', async () => { - vi.mocked(callAI).mockResolvedValue({ + rs.mocked(callAI).mockResolvedValue({ content: '{"bbox":[],"error":"target section is not found"}', isStreamed: false, }); diff --git a/packages/core/tests/unit-test/grounding.test.ts b/packages/core/tests/unit-test/grounding.test.ts index 635dd69ce4..e491c45505 100644 --- a/packages/core/tests/unit-test/grounding.test.ts +++ b/packages/core/tests/unit-test/grounding.test.ts @@ -1,7 +1,7 @@ import { createLocateResultCodec } from '@/ai-model/shared/model-locate-result'; import { pixelBboxToRect } from '@/ai-model/workflows/grounding/locate-result-rect'; import { mapSearchAreaPixelBboxToOriginalPixelBbox } from '@/ai-model/workflows/grounding/search-area-mapping'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const actualPixelBboxAdapter = createLocateResultCodec({ coordinates: { shape: 'bbox', order: 'xy' }, diff --git a/packages/core/tests/unit-test/html-utils.test.ts b/packages/core/tests/unit-test/html-utils.test.ts index a6b96f9ffa..3e67db9efa 100644 --- a/packages/core/tests/unit-test/html-utils.test.ts +++ b/packages/core/tests/unit-test/html-utils.test.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { collectImageScriptIds, extractImageByIdSync, diff --git a/packages/core/tests/unit-test/image-preprocess.test.ts b/packages/core/tests/unit-test/image-preprocess.test.ts index 88c9211863..b44b0a27b5 100644 --- a/packages/core/tests/unit-test/image-preprocess.test.ts +++ b/packages/core/tests/unit-test/image-preprocess.test.ts @@ -5,18 +5,18 @@ import { paddingToMatchBlockByBase64, scaleImage, } from '@midscene/shared/img'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -vi.mock('@midscene/shared/img', () => ({ - compositeElementInfoImg: vi.fn(), - cropByRect: vi.fn(), - paddingToMatchBlockByBase64: vi.fn(), - scaleImage: vi.fn(), +rs.mock('@midscene/shared/img', () => ({ + compositeElementInfoImg: rs.fn(), + cropByRect: rs.fn(), + paddingToMatchBlockByBase64: rs.fn(), + scaleImage: rs.fn(), })); describe('prepareModelImage', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('returns the original image and size when no padding policy is configured', async () => { @@ -42,7 +42,7 @@ describe('prepareModelImage', () => { }); it('keeps contentSize as the original size after padding the model image', async () => { - vi.mocked(paddingToMatchBlockByBase64).mockResolvedValue({ + rs.mocked(paddingToMatchBlockByBase64).mockResolvedValue({ imageBase64: 'padded-image', width: 112, height: 84, @@ -77,12 +77,12 @@ describe('prepareModelImage', () => { describe('buildSearchAreaConfig', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('crops the expanded area, scales it, and records offset/scale mapping', async () => { const cropCalls: unknown[] = []; - vi.mocked(cropByRect).mockImplementation(async (_imageBase64, rect) => { + rs.mocked(cropByRect).mockImplementation(async (_imageBase64, rect) => { cropCalls.push({ ...rect }); return { imageBase64: 'cropped-image', @@ -90,7 +90,7 @@ describe('buildSearchAreaConfig', () => { height: 400, } as any; }); - vi.mocked(scaleImage).mockResolvedValue({ + rs.mocked(scaleImage).mockResolvedValue({ imageBase64: 'scaled-image', width: 800, height: 800, diff --git a/packages/core/tests/unit-test/insight-extract-prompt.test.ts b/packages/core/tests/unit-test/insight-extract-prompt.test.ts index 9684020676..67d17bc03b 100644 --- a/packages/core/tests/unit-test/insight-extract-prompt.test.ts +++ b/packages/core/tests/unit-test/insight-extract-prompt.test.ts @@ -1,32 +1,29 @@ import { getModelRuntime } from '@/ai-model/models'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { createFakeContext } from '../utils'; -vi.mock('@/ai-model/service-caller/index', async () => { - const actual = await vi.importActual< - typeof import('@/ai-model/service-caller/index') - >('@/ai-model/service-caller/index'); - return { - ...actual, - AIResponseParseError: class AIResponseParseError extends Error {}, - callAI: vi.fn(), - callAIWithObjectResponse: vi.fn(), - callAIWithStringResponse: vi.fn(), - }; -}); +import * as serviceCallerActual from '@/ai-model/service-caller/index' with { + rstest: 'importActual', +}; +import * as imgActual from '@midscene/shared/img' with { + rstest: 'importActual', +}; -vi.mock('@midscene/shared/img', async () => { - const actual = await vi.importActual( - '@midscene/shared/img', - ); - return { - ...actual, - preProcessImageUrl: vi - .fn() - .mockResolvedValue('data:image/png;base64,REFERENCE'), - }; -}); +rs.mock('@/ai-model/service-caller/index', () => ({ + ...serviceCallerActual, + AIResponseParseError: class AIResponseParseError extends Error {}, + callAI: rs.fn(), + callAIWithObjectResponse: rs.fn(), + callAIWithStringResponse: rs.fn(), +})); + +rs.mock('@midscene/shared/img', () => ({ + ...imgActual, + preProcessImageUrl: rs + .fn() + .mockResolvedValue('data:image/png;base64,REFERENCE'), +})); import { callAI } from '@/ai-model/service-caller/index'; import { AiExtractElementInfo } from '@/ai-model/workflows/insight'; @@ -44,8 +41,8 @@ describe('insight extraction prompt assembly', () => { }; beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(callAI).mockResolvedValue({ + rs.clearAllMocks(); + rs.mocked(callAI).mockResolvedValue({ content: 'Looks correct.{"result":true}', usage: undefined, @@ -80,7 +77,7 @@ describe('insight extraction prompt assembly', () => { true, ); - const msgs = vi.mocked(callAI).mock.calls[0]?.[0]; + const msgs = rs.mocked(callAI).mock.calls[0]?.[0]; expect(msgs).toHaveLength(5); expect(msgs?.[0]).toMatchObject({ role: 'system', @@ -160,14 +157,14 @@ describe('insight extraction prompt assembly', () => { abortSignal: abortController.signal, }); - expect(vi.mocked(callAI).mock.calls[0]?.[2]).toEqual({ + expect(rs.mocked(callAI).mock.calls[0]?.[2]).toEqual({ abortSignal: abortController.signal, semanticRetryAttempt: 0, }); }); it('retries once when the insight XML response cannot be parsed', async () => { - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce({ content: 'Looks correct.', usage: undefined, @@ -189,11 +186,11 @@ describe('insight extraction prompt assembly', () => { }); expect(callAI).toHaveBeenCalledTimes(2); - expect(vi.mocked(callAI).mock.calls.map((call) => call[2])).toEqual([ + expect(rs.mocked(callAI).mock.calls.map((call) => call[2])).toEqual([ expect.objectContaining({ semanticRetryAttempt: 0 }), expect.objectContaining({ semanticRetryAttempt: 1 }), ]); - const retryFeedback = vi.mocked(callAI).mock.calls[1]?.[0]?.at(-1); + const retryFeedback = rs.mocked(callAI).mock.calls[1]?.[0]?.at(-1); expect(retryFeedback).toMatchObject({ role: 'user' }); expect(retryFeedback?.content).toEqual( expect.stringContaining('Missing required field: data-json'), diff --git a/packages/core/tests/unit-test/insight-multi-frame.test.ts b/packages/core/tests/unit-test/insight-multi-frame.test.ts index c6734657bc..b61dc6e333 100644 --- a/packages/core/tests/unit-test/insight-multi-frame.test.ts +++ b/packages/core/tests/unit-test/insight-multi-frame.test.ts @@ -2,21 +2,20 @@ import { getModelRuntime } from '@/ai-model/models'; import { ScreenshotItem } from '@/screenshot-item'; import type { UIContext } from '@/types'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { createFakeContext } from '../utils'; -vi.mock('@/ai-model/service-caller/index', async () => { - const actual = await vi.importActual< - typeof import('@/ai-model/service-caller/index') - >('@/ai-model/service-caller/index'); - return { - ...actual, - AIResponseParseError: class AIResponseParseError extends Error {}, - callAI: vi.fn(), - callAIWithObjectResponse: vi.fn(), - callAIWithStringResponse: vi.fn(), - }; -}); +import * as serviceCallerActual from '@/ai-model/service-caller/index' with { + rstest: 'importActual', +}; + +rs.mock('@/ai-model/service-caller/index', () => ({ + ...serviceCallerActual, + AIResponseParseError: class AIResponseParseError extends Error {}, + callAI: rs.fn(), + callAIWithObjectResponse: rs.fn(), + callAIWithStringResponse: rs.fn(), +})); import { callAI } from '@/ai-model/service-caller/index'; import { AiExtractElementInfo } from '@/ai-model/workflows/insight'; @@ -31,8 +30,8 @@ describe('insight extraction multi-frame context', () => { }; beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(callAI).mockResolvedValue({ + rs.clearAllMocks(); + rs.mocked(callAI).mockResolvedValue({ content: 'Saw the toast.{"result":true}', usage: undefined, @@ -63,7 +62,7 @@ describe('insight extraction multi-frame context', () => { modelRuntime: getModelRuntime(modelConfig), }); - const msgs = vi.mocked(callAI).mock.calls[0]?.[0]; + const msgs = rs.mocked(callAI).mock.calls[0]?.[0]; const userContent = msgs?.[1]?.content as Array>; const imageParts = userContent.filter((p) => p.type === 'image_url'); @@ -110,7 +109,7 @@ describe('insight extraction multi-frame context', () => { modelRuntime: getModelRuntime(modelConfig), }); - const msgs = vi.mocked(callAI).mock.calls[0]?.[0]; + const msgs = rs.mocked(callAI).mock.calls[0]?.[0]; const userContent = msgs?.[1]?.content as Array>; const imageParts = userContent.filter((p) => p.type === 'image_url'); diff --git a/packages/core/tests/unit-test/json.test.ts b/packages/core/tests/unit-test/json.test.ts index 00b5892abc..022ead8008 100644 --- a/packages/core/tests/unit-test/json.test.ts +++ b/packages/core/tests/unit-test/json.test.ts @@ -3,7 +3,7 @@ import { extractJSONFromCodeBlock, parseModelResponseJson, } from '@/ai-model/shared/json'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('extractJSONFromCodeBlock', () => { it('should extract JSON from a direct JSON object', () => { diff --git a/packages/core/tests/unit-test/llm-planning-retry.test.ts b/packages/core/tests/unit-test/llm-planning-retry.test.ts index a03b098686..8d8d0588e8 100644 --- a/packages/core/tests/unit-test/llm-planning-retry.test.ts +++ b/packages/core/tests/unit-test/llm-planning-retry.test.ts @@ -7,25 +7,23 @@ import { ConversationHistory } from '@/ai-model/workflows/planning/conversation- import { buildYamlFlowFromPlans, getMidsceneLocationSchema } from '@/common'; import type { DeviceAction, UIContext } from '@/types'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { z } from 'zod'; -vi.mock('@/ai-model/service-caller/index', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - callAI: vi.fn(), - }; -}); +import * as serviceCallerActual from '@/ai-model/service-caller/index' with { + rstest: 'importActual', +}; +import * as commonActual from '@/common' with { rstest: 'importActual' }; -vi.mock('@/common', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - buildYamlFlowFromPlans: vi.fn(actual.buildYamlFlowFromPlans), - }; -}); +rs.mock('@/ai-model/service-caller/index', () => ({ + ...serviceCallerActual, + callAI: rs.fn(), +})); + +rs.mock('@/common', () => ({ + ...commonActual, + buildYamlFlowFromPlans: rs.fn(commonActual.buildYamlFlowFromPlans), +})); const mockAIResponse = (content: string) => ({ content, @@ -59,12 +57,12 @@ const mockActionSpace = (): DeviceAction[] => [ { name: 'Tap', description: 'Tap an element', - call: vi.fn(), + call: rs.fn(), }, ]; const latestImageDetail = () => { - const messages = vi.mocked(callAI).mock.calls[0]?.[0]; + const messages = rs.mocked(callAI).mock.calls[0]?.[0]; const latestMessage = messages?.at(-1); const imagePart = Array.isArray(latestMessage?.content) ? latestMessage.content.find((part) => part.type === 'image_url') @@ -72,21 +70,21 @@ const latestImageDetail = () => { return imagePart?.image_url.detail; }; -const latestCallAIOptions = () => vi.mocked(callAI).mock.calls[0]?.[2]; +const latestCallAIOptions = () => rs.mocked(callAI).mock.calls[0]?.[2]; const latestSystemPrompt = () => { - const message = vi.mocked(callAI).mock.calls[0]?.[0]?.[0]; + const message = rs.mocked(callAI).mock.calls[0]?.[0]?.[0]; return message?.role === 'system' ? message.content : undefined; }; describe('plan XML parse retry', () => { beforeEach(() => { - vi.mocked(callAI).mockReset(); - vi.mocked(buildYamlFlowFromPlans).mockClear(); + rs.mocked(callAI).mockReset(); + rs.mocked(buildYamlFlowFromPlans).mockClear(); }); it('uses the action-only XML protocol for fast effort', async () => { - vi.mocked(callAI).mockResolvedValueOnce( + rs.mocked(callAI).mockResolvedValueOnce( mockAIResponse(`Tap {}`), ); @@ -100,7 +98,7 @@ describe('plan XML parse retry', () => { effort: 'fast', }); - const systemPrompt = vi.mocked(callAI).mock.calls[0]?.[0]?.[0]?.content; + const systemPrompt = rs.mocked(callAI).mock.calls[0]?.[0]?.[0]?.content; expect(systemPrompt).not.toEqual(expect.stringContaining('')); expect(systemPrompt).not.toEqual(expect.stringContaining('')); expect(systemPrompt).not.toEqual(expect.stringContaining('')); @@ -110,7 +108,7 @@ describe('plan XML parse retry', () => { }); it('uses model retry settings when XML response parsing fails', async () => { - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce( mockAIResponse(`Tap button Tap @@ -140,7 +138,7 @@ describe('plan XML parse retry', () => { }); expect(callAI).toHaveBeenCalledTimes(3); - const retryFeedback = vi.mocked(callAI).mock.calls[1]?.[0]?.at(-1); + const retryFeedback = rs.mocked(callAI).mock.calls[1]?.[0]?.at(-1); expect(retryFeedback).toMatchObject({ role: 'user' }); expect(retryFeedback?.content).toEqual( expect.stringContaining('The previous response was invalid:'), @@ -158,7 +156,7 @@ describe('plan XML parse retry', () => { reasoning_content: 'The button is visible in the center of the screen.', }; const conversationHistory = new ConversationHistory(); - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce({ ...mockAIResponse(firstResponse), rawChoiceMessage: rawAssistantMessage, @@ -180,7 +178,7 @@ describe('plan XML parse retry', () => { await standardPlan('tap the button', options); await standardPlan('tap the button', options); - const secondRequestMessages = vi.mocked(callAI).mock.calls[1]?.[0]; + const secondRequestMessages = rs.mocked(callAI).mock.calls[1]?.[0]; expect(secondRequestMessages).toContainEqual(rawAssistantMessage); }); @@ -193,7 +191,7 @@ describe('plan XML parse retry', () => { reasoning_content: 'Provider-specific reasoning state.', }; const conversationHistory = new ConversationHistory(); - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce({ ...mockAIResponse(firstResponse), rawChoiceMessage: rawAssistantMessage, @@ -214,7 +212,7 @@ describe('plan XML parse retry', () => { await standardPlan('tap the button', options); await standardPlan('tap the button', options); - const secondRequestMessages = vi.mocked(callAI).mock.calls[1]?.[0]; + const secondRequestMessages = rs.mocked(callAI).mock.calls[1]?.[0]; expect(secondRequestMessages).not.toContainEqual(rawAssistantMessage); expect(secondRequestMessages).toContainEqual({ role: 'assistant', @@ -224,7 +222,7 @@ describe('plan XML parse retry', () => { it('preserves retry request errors instead of reporting them as XML parse errors', async () => { const requestError = new Error('failed to call AI model service'); - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce( mockAIResponse(`Tap {invalid json}`), @@ -246,7 +244,7 @@ describe('plan XML parse retry', () => { }); it('should tell the model when no previous aiAct actions have been executed', async () => { - vi.mocked(callAI).mockResolvedValueOnce( + rs.mocked(callAI).mockResolvedValueOnce( mockAIResponse(`Tap button Tap`), ); @@ -260,7 +258,7 @@ describe('plan XML parse retry', () => { effort: 'balance', }); - const messages = vi.mocked(callAI).mock.calls[0]?.[0]; + const messages = rs.mocked(callAI).mock.calls[0]?.[0]; const latestMessage = messages?.at(-1); const textPart = Array.isArray(latestMessage?.content) ? latestMessage.content.find((part) => part.type === 'text') @@ -276,7 +274,7 @@ describe('plan XML parse retry', () => { }); it('marks planning as requiring original image detail when locate is included', async () => { - vi.mocked(callAI).mockResolvedValueOnce( + rs.mocked(callAI).mockResolvedValueOnce( mockAIResponse(`Tap button Tap`), ); @@ -321,7 +319,7 @@ describe('plan XML parse retry', () => { }, }, }; - vi.mocked(callAI).mockResolvedValueOnce( + rs.mocked(callAI).mockResolvedValueOnce( mockAIResponse( 'Tap button\nTap', ), @@ -348,8 +346,8 @@ describe('plan XML parse retry', () => { }); it('uses the JSON parser configured by the adapter for planning actions', async () => { - const jsonParser = vi.fn(() => ({ parsedByCustomParser: true })); - vi.mocked(callAI).mockResolvedValueOnce( + const jsonParser = rs.fn(() => ({ parsedByCustomParser: true })); + rs.mocked(callAI).mockResolvedValueOnce( mockAIResponse(`Tap button Tap {custom syntax}`), @@ -388,10 +386,10 @@ describe('plan XML parse retry', () => { name: 'Tap', description: 'Tap an element', paramSchema: z.object({ locate: getMidsceneLocationSchema() }), - call: vi.fn(), + call: rs.fn(), }, ]; - vi.mocked(callAI) + rs.mocked(callAI) .mockResolvedValueOnce( mockAIResponse(`Tap {"locate":{"prompt":"submit","bbox":["invalid"]}}`), @@ -401,7 +399,7 @@ describe('plan XML parse retry', () => { {"locate":{"prompt":"submit","bbox":[100,200,300,400]}}`), ); const yamlFlowInputs: unknown[] = []; - const buildYamlFlow = vi.mocked(buildYamlFlowFromPlans); + const buildYamlFlow = rs.mocked(buildYamlFlowFromPlans); const originalBuildYamlFlow = buildYamlFlow.getMockImplementation(); const captureYamlFlowInput = ( plans: Parameters[0], diff --git a/packages/core/tests/unit-test/llm-planning.test.ts b/packages/core/tests/unit-test/llm-planning.test.ts index fcf6652f45..9a8bf68e2f 100644 --- a/packages/core/tests/unit-test/llm-planning.test.ts +++ b/packages/core/tests/unit-test/llm-planning.test.ts @@ -15,7 +15,7 @@ import { OPENAI_API_KEY, OPENAI_BASE_URL, } from '@midscene/shared/env'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; import { z } from 'zod'; const defaultMidscenePlanningProtocol = createDefaultMidscenePlanningProtocol({ @@ -42,13 +42,13 @@ const parseStandardPlanningResponse = ( describe('llm planning - doubao', () => { beforeEach(() => { - vi.stubEnv(OPENAI_BASE_URL, 'http://mock'); - vi.stubEnv(OPENAI_API_KEY, 'mock'); - vi.stubEnv(MIDSCENE_USE_DOUBAO_VISION, 'true'); + rs.stubEnv(OPENAI_BASE_URL, 'http://mock'); + rs.stubEnv(OPENAI_API_KEY, 'mock'); + rs.stubEnv(MIDSCENE_USE_DOUBAO_VISION, 'true'); }); afterEach(() => { - vi.unstubAllEnvs(); + rs.unstubAllEnvs(); }); it('adapts doubao locate result to pixel bbox', () => { @@ -511,7 +511,7 @@ describe('parseStandardPlanningResponse', () => { { name: 'Tap', paramSchema: actionTapParamSchema, - call: vi.fn(), + call: rs.fn(), }, ], }); @@ -537,7 +537,7 @@ describe('parseStandardPlanningResponse', () => { { name: 'Input', paramSchema: actionInputParamSchema, - call: vi.fn(), + call: rs.fn(), }, ], }); diff --git a/packages/core/tests/unit-test/locate-multimodal-regression.test.ts b/packages/core/tests/unit-test/locate-multimodal-regression.test.ts index a05941f106..6a682fbc09 100644 --- a/packages/core/tests/unit-test/locate-multimodal-regression.test.ts +++ b/packages/core/tests/unit-test/locate-multimodal-regression.test.ts @@ -1,6 +1,6 @@ import { Agent } from '@/agent'; import { ScriptPlayer } from '@/yaml/player'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const referenceImages = [ { @@ -22,7 +22,7 @@ const expectedLocateParam = { const createAgentStub = () => { const agent = Object.create(Agent.prototype) as Agent; - (agent as any).callActionInActionSpace = vi.fn(async () => undefined); + (agent as any).callActionInActionSpace = rs.fn(async () => undefined); return agent; }; @@ -30,7 +30,7 @@ describe('multimodal locate prompt regression', () => { it('should preserve multimodal locate options when Agent.aiTap receives a string prompt', async () => { const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await agent.aiTap('Click the icon', { @@ -63,7 +63,7 @@ describe('multimodal locate prompt regression', () => { ); const agent = createAgentStub(); const callActionSpy = (agent as any).callActionInActionSpace as ReturnType< - typeof vi.fn + typeof rs.fn >; await player.playTask( diff --git a/packages/core/tests/unit-test/locate-normalization.test.ts b/packages/core/tests/unit-test/locate-normalization.test.ts index 9809387548..afc0a0fead 100644 --- a/packages/core/tests/unit-test/locate-normalization.test.ts +++ b/packages/core/tests/unit-test/locate-normalization.test.ts @@ -2,7 +2,7 @@ import { normalizePlanningActionLocateFields } from '@/ai-model/workflows/planni import { getMidsceneLocationSchema } from '@/common'; import type { DeviceAction } from '@/device'; import type { PlanningAction } from '@/types'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { z } from 'zod'; const actionSpace: DeviceAction[] = [ @@ -25,7 +25,7 @@ const locateResultContext = { describe('normalizePlanningActionLocateFields', () => { it('leaves actions unchanged when the planned action is outside the action space', () => { - const toPixelBbox = vi.fn(); + const toPixelBbox = rs.fn(); const actions: PlanningAction[] = [ { type: 'UnknownAction', @@ -53,7 +53,7 @@ describe('normalizePlanningActionLocateFields', () => { }); it('normalizes locate params with the configured locate codec', () => { - const toPixelBbox = vi.fn(() => [10, 20, 30, 40]); + const toPixelBbox = rs.fn(() => [10, 20, 30, 40]); const actions: PlanningAction[] = [ { type: 'Tap', @@ -81,7 +81,7 @@ describe('normalizePlanningActionLocateFields', () => { }); it('accepts bbox_2d when the model adapter enables the alias', () => { - const toPixelBbox = vi.fn(() => [10, 20, 30, 40]); + const toPixelBbox = rs.fn(() => [10, 20, 30, 40]); const actions: PlanningAction[] = [ { type: 'Tap', @@ -113,7 +113,7 @@ describe('normalizePlanningActionLocateFields', () => { }); it('keeps only the prompt in prompt-only planning mode', () => { - const toPixelBbox = vi.fn(); + const toPixelBbox = rs.fn(); const actions: PlanningAction[] = [ { type: 'Tap', diff --git a/packages/core/tests/unit-test/locate-result-codec.test.ts b/packages/core/tests/unit-test/locate-result-codec.test.ts index 21d07de863..524be075a2 100644 --- a/packages/core/tests/unit-test/locate-result-codec.test.ts +++ b/packages/core/tests/unit-test/locate-result-codec.test.ts @@ -1,7 +1,7 @@ import { createLocateResultCodec } from '@/ai-model/shared/model-locate-result'; import { locateResultExampleRegions } from '@/ai-model/shared/model-locate-result/prompt-spec'; import { pixelBboxToRect } from '@/ai-model/workflows/grounding/locate-result-rect'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const locateCtx = (width: number, height: number) => ({ preparedSize: { width, height }, diff --git a/packages/core/tests/unit-test/merge-browser-parse.test.ts b/packages/core/tests/unit-test/merge-browser-parse.test.ts index 5266bf3da3..2f8c787b0a 100644 --- a/packages/core/tests/unit-test/merge-browser-parse.test.ts +++ b/packages/core/tests/unit-test/merge-browser-parse.test.ts @@ -24,7 +24,7 @@ import { } from '@/types'; import { uuid } from '@midscene/shared/utils'; import { antiEscapeScriptTag, escapeScriptTag } from '@midscene/shared/utils'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; function fakeBase64(sizeBytes: number): string { return `data:image/png;base64,${'A'.repeat(sizeBytes)}`; diff --git a/packages/core/tests/unit-test/model-adapter.test.ts b/packages/core/tests/unit-test/model-adapter.test.ts index e3d4c7247d..05ac77a93b 100644 --- a/packages/core/tests/unit-test/model-adapter.test.ts +++ b/packages/core/tests/unit-test/model-adapter.test.ts @@ -7,7 +7,7 @@ import { getModelAdapter } from '@/ai-model/models'; import { MODEL_ADAPTER_CONFIGS } from '@/ai-model/models/registry'; import { parseModelResponseJson } from '@/ai-model/shared/json'; import { MODEL_FAMILY_VALUES, type TModelFamily } from '@midscene/shared/env'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const defaultMidscenePlanningProtocol = createDefaultMidscenePlanningProtocol({ jsonParser: parseModelResponseJson, @@ -177,7 +177,7 @@ describe('ResolvedModelAdapter', () => { }); it('keeps custom planner and locate definitions while applying policy defaults', () => { - const locateFn = vi.fn(); + const locateFn = rs.fn(); const adapter = new ResolvedModelAdapter( { planning: { @@ -280,7 +280,7 @@ describe('ResolvedModelAdapter', () => { { planning: { kind: 'custom', - planFn: vi.fn(), + planFn: rs.fn(), }, locate: { kind: 'custom', @@ -354,7 +354,7 @@ describe('ResolvedModelAdapter', () => { }); it('keeps custom planning functions as a fallback escape hatch', () => { - const planFn = vi.fn(); + const planFn = rs.fn(); const adapter = new ResolvedModelAdapter( { planning: { diff --git a/packages/core/tests/unit-test/model-adapter/auto-glm/__snapshots__/prompt.test.ts.snap b/packages/core/tests/unit-test/model-adapter/auto-glm/__snapshots__/prompt.test.ts.snap index c4ba14bb3e..4bb461b34a 100644 --- a/packages/core/tests/unit-test/model-adapter/auto-glm/__snapshots__/prompt.test.ts.snap +++ b/packages/core/tests/unit-test/model-adapter/auto-glm/__snapshots__/prompt.test.ts.snap @@ -1,4 +1,4 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +// Rstest Snapshot v1 exports[`auto-glm prompts > locate prompts > auto-glm locate prompt - chinese 1`] = ` " diff --git a/packages/core/tests/unit-test/model-adapter/auto-glm/actions.test.ts b/packages/core/tests/unit-test/model-adapter/auto-glm/actions.test.ts index 5ba104e6c3..87d00a3c60 100644 --- a/packages/core/tests/unit-test/model-adapter/auto-glm/actions.test.ts +++ b/packages/core/tests/unit-test/model-adapter/auto-glm/actions.test.ts @@ -16,7 +16,7 @@ import { import { autoGlmAdapters } from '@/ai-model/models/auto-glm/adapter'; import { createCoordinateDistanceToPixels } from '@/ai-model/shared/model-locate-result'; import type { DeviceAction } from '@/device'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const defaultSize = { width: 1080, height: 1920 }; const autoGlmPlanning = new ResolvedModelAdapter( diff --git a/packages/core/tests/unit-test/model-adapter/auto-glm/adapter.test.ts b/packages/core/tests/unit-test/model-adapter/auto-glm/adapter.test.ts index 07c711ecd4..913f5b3fb0 100644 --- a/packages/core/tests/unit-test/model-adapter/auto-glm/adapter.test.ts +++ b/packages/core/tests/unit-test/model-adapter/auto-glm/adapter.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { autoGlmAdapters } from '@/ai-model/models/auto-glm/adapter'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const autoGlmAdapter = new ResolvedModelAdapter( autoGlmAdapters['auto-glm'], diff --git a/packages/core/tests/unit-test/model-adapter/auto-glm/locate.test.ts b/packages/core/tests/unit-test/model-adapter/auto-glm/locate.test.ts index f89bee3d41..1a2876ed55 100644 --- a/packages/core/tests/unit-test/model-adapter/auto-glm/locate.test.ts +++ b/packages/core/tests/unit-test/model-adapter/auto-glm/locate.test.ts @@ -5,9 +5,9 @@ import { callAIWithStringResponse } from '@/ai-model/service-caller/index'; import { AiLocateElement } from '@/ai-model/workflows/grounding'; import type { LocateOptions } from '@/ai-model/workflows/grounding/types'; import type { UIContext } from '@/types'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -const serviceCallerMock = vi.hoisted(() => { +const serviceCallerMock = rs.hoisted(() => { class AIResponseParseError extends Error { rawResponse?: string; usage?: unknown; @@ -29,15 +29,15 @@ const serviceCallerMock = vi.hoisted(() => { return { AIResponseParseError, - callAIWithStringResponse: vi.fn(), + callAIWithStringResponse: rs.fn(), }; }); -vi.mock('@/ai-model/service-caller/index', () => { +rs.mock('@/ai-model/service-caller/index', () => { return serviceCallerMock; }); -vi.mock('../../../../src/ai-model/service-caller/index', () => { +rs.mock('../../../../src/ai-model/service-caller/index', () => { return serviceCallerMock; }); @@ -107,7 +107,7 @@ function createLocateOptions(): LocateOptions { describe('Auto-GLM custom locate', () => { beforeEach(() => { - vi.mocked(callAIWithStringResponse).mockReset(); + rs.mocked(callAIWithStringResponse).mockReset(); }); it('runs Auto-GLM custom locate and maps normalized coordinates to a rect', async () => { @@ -115,7 +115,7 @@ describe('Auto-GLM custom locate', () => { if (autoGlmAdapter.locate.kind !== 'custom') { throw new Error('Auto-GLM should use custom locate adapter'); } - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: 'Found submitdo(action="Tap", element=[500,500])', usage: { total_tokens: 8 } as any, @@ -161,7 +161,7 @@ describe('Auto-GLM custom locate', () => { if (autoGlmAdapter.locate.kind !== 'custom') { throw new Error('Auto-GLM should use custom locate adapter'); } - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: 'Found item in cropdo(action="Tap", element=[500,500])', }); @@ -191,7 +191,7 @@ describe('Auto-GLM custom locate', () => { }, }); - const messages = vi.mocked(callAIWithStringResponse).mock.calls[0]?.[0]; + const messages = rs.mocked(callAIWithStringResponse).mock.calls[0]?.[0]; expect(messages).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -220,7 +220,7 @@ describe('Auto-GLM custom locate', () => { if (autoGlmAdapter.locate.kind !== 'custom') { throw new Error('Auto-GLM should use custom locate adapter'); } - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: 'do(action="Swipe", start=[100,200], end=[300,400])', }); @@ -241,7 +241,7 @@ describe('Auto-GLM custom locate', () => { if (autoGlmAdapter.locate.kind !== 'custom') { throw new Error('Auto-GLM should use custom locate adapter'); } - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: 'Found matching icondo(action="Tap", element=[500,500])', }); @@ -259,7 +259,7 @@ describe('Auto-GLM custom locate', () => { }, }); - const messages = vi.mocked(callAIWithStringResponse).mock.calls[0]?.[0]; + const messages = rs.mocked(callAIWithStringResponse).mock.calls[0]?.[0]; expect(messages).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/packages/core/tests/unit-test/model-adapter/auto-glm/planning-action-parser.test.ts b/packages/core/tests/unit-test/model-adapter/auto-glm/planning-action-parser.test.ts index 46a81ad808..b6e779c703 100644 --- a/packages/core/tests/unit-test/model-adapter/auto-glm/planning-action-parser.test.ts +++ b/packages/core/tests/unit-test/model-adapter/auto-glm/planning-action-parser.test.ts @@ -11,7 +11,7 @@ import type { WaitAction, } from '@/ai-model/models/auto-glm/actions'; import { parseAutoGLMPlanningAction } from '@/ai-model/models/auto-glm/parser'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('parseAutoGLMPlanningAction', () => { it('should parse Tap action', () => { diff --git a/packages/core/tests/unit-test/model-adapter/auto-glm/planning-messages.test.ts b/packages/core/tests/unit-test/model-adapter/auto-glm/planning-messages.test.ts index 677acea905..bac185d714 100644 --- a/packages/core/tests/unit-test/model-adapter/auto-glm/planning-messages.test.ts +++ b/packages/core/tests/unit-test/model-adapter/auto-glm/planning-messages.test.ts @@ -7,10 +7,10 @@ import { ConversationHistory } from '@/ai-model/workflows/planning/conversation- import { runCustomPlanning } from '@/ai-model/workflows/planning/custom-planning'; import type { PlanOptions } from '@/ai-model/workflows/planning/types'; import type { UIContext } from '@/types'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import type { ChatCompletionUserMessageParam } from 'openai/resources/index'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -const serviceCallerMock = vi.hoisted(() => { +const serviceCallerMock = rs.hoisted(() => { class AIResponseParseError extends Error { rawResponse?: string; usage?: unknown; @@ -32,15 +32,15 @@ const serviceCallerMock = vi.hoisted(() => { return { AIResponseParseError, - callAIWithStringResponse: vi.fn(), + callAIWithStringResponse: rs.fn(), }; }); -vi.mock('@/ai-model/service-caller/index', () => { +rs.mock('@/ai-model/service-caller/index', () => { return serviceCallerMock; }); -vi.mock('../../../../src/ai-model/service-caller/index', () => { +rs.mock('../../../../src/ai-model/service-caller/index', () => { return serviceCallerMock; }); @@ -91,7 +91,7 @@ function runAutoGlmPlanning(userInstruction: string, options: PlanOptions) { describe('createAutoGlmPlanner messages', () => { beforeEach(() => { - vi.mocked(callAIWithStringResponse).mockReset(); + rs.mocked(callAIWithStringResponse).mockReset(); }); it('passes Auto-GLM action context, reference images, and abort signal to the model call', async () => { @@ -108,7 +108,7 @@ describe('createAutoGlmPlanner messages', () => { }, ]; const conversationHistory = new ConversationHistory(); - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: 'Need to click submitdo(action="Tap", element=[500,500])', usage: { total_tokens: 12 } as any, @@ -125,7 +125,7 @@ describe('createAutoGlmPlanner messages', () => { }), ); - const [messages, runtime, callOptions] = vi.mocked(callAIWithStringResponse) + const [messages, runtime, callOptions] = rs.mocked(callAIWithStringResponse) .mock.calls[0]; expect(runtime).toMatchObject({ config: expect.objectContaining({ modelFamily: 'auto-glm' }), diff --git a/packages/core/tests/unit-test/model-adapter/auto-glm/planning.test.ts b/packages/core/tests/unit-test/model-adapter/auto-glm/planning.test.ts index 8c50ec09b1..0ada065651 100644 --- a/packages/core/tests/unit-test/model-adapter/auto-glm/planning.test.ts +++ b/packages/core/tests/unit-test/model-adapter/auto-glm/planning.test.ts @@ -7,10 +7,10 @@ import { ConversationHistory } from '@/ai-model/workflows/planning/conversation- import { runCustomPlanning } from '@/ai-model/workflows/planning/custom-planning'; import type { PlanOptions } from '@/ai-model/workflows/planning/types'; import type { UIContext } from '@/types'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { mockActionSpace } from '../../../common'; -const serviceCallerMock = vi.hoisted(() => { +const serviceCallerMock = rs.hoisted(() => { class AIResponseParseError extends Error { rawResponse?: string; usage?: unknown; @@ -32,15 +32,15 @@ const serviceCallerMock = vi.hoisted(() => { return { AIResponseParseError, - callAIWithStringResponse: vi.fn(), + callAIWithStringResponse: rs.fn(), }; }); -vi.mock('@/ai-model/service-caller/index', () => { +rs.mock('@/ai-model/service-caller/index', () => { return serviceCallerMock; }); -vi.mock('../../../../src/ai-model/service-caller/index', () => { +rs.mock('../../../../src/ai-model/service-caller/index', () => { return serviceCallerMock; }); @@ -95,11 +95,11 @@ function runAutoGlmPlanning( describe('createAutoGlmPlanner', () => { beforeEach(() => { - vi.mocked(callAIWithStringResponse).mockReset(); + rs.mocked(callAIWithStringResponse).mockReset(); }); it('runs Auto-GLM custom planning and transforms tap coordinates', async () => { - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: 'Need to click submitdo(action="Tap", element=[500,500])', usage: { total_tokens: 12 } as any, @@ -135,7 +135,7 @@ describe('createAutoGlmPlanner', () => { }); it('uses actionSpace names for Auto-GLM Back and Home planning actions', async () => { - vi.mocked(callAIWithStringResponse) + rs.mocked(callAIWithStringResponse) .mockResolvedValueOnce({ content: 'Need to go back. do(action="Back")', }) @@ -174,7 +174,7 @@ describe('createAutoGlmPlanner', () => { }); it('stops Auto-GLM custom planning on finish action', async () => { - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: 'Task is done. finish(message="done")', }); @@ -194,7 +194,7 @@ describe('createAutoGlmPlanner', () => { }); it('wraps Auto-GLM planning parse failures with raw response and usage', async () => { - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: 'do(action="UnknownAction")', usage: { total_tokens: 3 } as any, }); diff --git a/packages/core/tests/unit-test/model-adapter/auto-glm/prompt.test.ts b/packages/core/tests/unit-test/model-adapter/auto-glm/prompt.test.ts index cb896a2f0f..6d3d56c330 100644 --- a/packages/core/tests/unit-test/model-adapter/auto-glm/prompt.test.ts +++ b/packages/core/tests/unit-test/model-adapter/auto-glm/prompt.test.ts @@ -4,17 +4,17 @@ import { getAutoGLMMultilingualLocatePrompt, getAutoGLMMultilingualPlanPrompt, } from '@/ai-model/models/auto-glm/prompt'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; describe('auto-glm prompts', () => { beforeEach(() => { // Mock date to 2025-12-31 Wednesday - vi.setSystemTime(new Date('2025-12-31T00:00:00.000Z')); + rs.setSystemTime(new Date('2025-12-31T00:00:00.000Z')); }); afterEach(() => { // Restore real timers after each test - vi.useRealTimers(); + rs.useRealTimers(); }); describe('planning prompts', () => { diff --git a/packages/core/tests/unit-test/model-adapter/auto-glm/response-parser.test.ts b/packages/core/tests/unit-test/model-adapter/auto-glm/response-parser.test.ts index b94a2be23b..3100afdfc9 100644 --- a/packages/core/tests/unit-test/model-adapter/auto-glm/response-parser.test.ts +++ b/packages/core/tests/unit-test/model-adapter/auto-glm/response-parser.test.ts @@ -2,7 +2,7 @@ import { extractValueAfter, parseAutoGLMResponse, } from '@/ai-model/models/auto-glm/parser'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('auto-glm response parser', () => { describe('extractValueAfter', () => { diff --git a/packages/core/tests/unit-test/model-adapter/chat-completion.test.ts b/packages/core/tests/unit-test/model-adapter/chat-completion.test.ts index 4f79635b98..81b6f8e89e 100644 --- a/packages/core/tests/unit-test/model-adapter/chat-completion.test.ts +++ b/packages/core/tests/unit-test/model-adapter/chat-completion.test.ts @@ -1,5 +1,5 @@ import { resolveChatCompletion } from '@/ai-model/model-adapter/chat-completion'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('chat completion content extraction', () => { const defaultExtractContentAndReasoning = diff --git a/packages/core/tests/unit-test/model-adapter/deepseek.test.ts b/packages/core/tests/unit-test/model-adapter/deepseek.test.ts index 235565a3ee..71b6822d30 100644 --- a/packages/core/tests/unit-test/model-adapter/deepseek.test.ts +++ b/packages/core/tests/unit-test/model-adapter/deepseek.test.ts @@ -6,7 +6,7 @@ import { } from '@/ai-model/models/deepseek/locate-protocol'; import { systemPromptToLocateElement } from '@/ai-model/prompt/llm-locator'; import { createLocateResultPromptSpec } from '@/ai-model/shared/model-locate-result/prompt-spec'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const deepSeekAdapter = new ResolvedModelAdapter( deepSeekAdapters.deepseek, diff --git a/packages/core/tests/unit-test/model-adapter/default-locate-protocol.test.ts b/packages/core/tests/unit-test/model-adapter/default-locate-protocol.test.ts index 01d88e6f25..aa639e1260 100644 --- a/packages/core/tests/unit-test/model-adapter/default-locate-protocol.test.ts +++ b/packages/core/tests/unit-test/model-adapter/default-locate-protocol.test.ts @@ -6,7 +6,7 @@ import { systemPromptToLocateElement } from '@/ai-model/prompt/llm-locator'; import { systemPromptToLocateSection } from '@/ai-model/prompt/llm-section-locator'; import { parseModelResponseJson } from '@/ai-model/shared/json'; import { createLocateResultPromptSpec } from '@/ai-model/shared/model-locate-result/prompt-spec'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; describe('default locate protocol', () => { it('builds the existing JSON locate prompts', () => { @@ -127,7 +127,7 @@ describe('default locate protocol', () => { }); it('uses the adapter JSON parser', () => { - const jsonParser = vi.fn(() => ({ bbox: [100, 200, 300, 400] })); + const jsonParser = rs.fn(() => ({ bbox: [100, 200, 300, 400] })); const elementProtocol = createDefaultElementProtocol({ jsonParser }); const searchAreaProtocol = createDefaultSearchAreaProtocol({ jsonParser }); const locatePromptSpec = createLocateResultPromptSpec({ diff --git a/packages/core/tests/unit-test/model-adapter/doubao.test.ts b/packages/core/tests/unit-test/model-adapter/doubao.test.ts index 234e3444af..e3b1558a85 100644 --- a/packages/core/tests/unit-test/model-adapter/doubao.test.ts +++ b/packages/core/tests/unit-test/model-adapter/doubao.test.ts @@ -3,7 +3,7 @@ import { doubaoAdapters, parseDoubaoRawLocateValue, } from '@/ai-model/models/doubao'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const doubaoVisionAdapter = new ResolvedModelAdapter( doubaoAdapters['doubao-vision'], diff --git a/packages/core/tests/unit-test/model-adapter/gemini.test.ts b/packages/core/tests/unit-test/model-adapter/gemini.test.ts index 2d5b0a8930..045b6e73ae 100644 --- a/packages/core/tests/unit-test/model-adapter/gemini.test.ts +++ b/packages/core/tests/unit-test/model-adapter/gemini.test.ts @@ -3,7 +3,7 @@ import { extractGeminiContentAndReasoning, geminiAdapters, } from '@/ai-model/models/gemini'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const geminiAdapter = new ResolvedModelAdapter(geminiAdapters.gemini, 'gemini'); diff --git a/packages/core/tests/unit-test/model-adapter/glm.test.ts b/packages/core/tests/unit-test/model-adapter/glm.test.ts index ef07b3d888..13937549db 100644 --- a/packages/core/tests/unit-test/model-adapter/glm.test.ts +++ b/packages/core/tests/unit-test/model-adapter/glm.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { glmAdapters } from '@/ai-model/models/glm'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const glmAdapter = new ResolvedModelAdapter(glmAdapters['glm-v'], 'glm-v'); diff --git a/packages/core/tests/unit-test/model-adapter/gpt.test.ts b/packages/core/tests/unit-test/model-adapter/gpt.test.ts index 22a807a566..26caf5d936 100644 --- a/packages/core/tests/unit-test/model-adapter/gpt.test.ts +++ b/packages/core/tests/unit-test/model-adapter/gpt.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { gptAdapters } from '@/ai-model/models/gpt'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const gpt5Adapter = new ResolvedModelAdapter(gptAdapters['gpt-5'], 'gpt-5'); diff --git a/packages/core/tests/unit-test/model-adapter/kimi.test.ts b/packages/core/tests/unit-test/model-adapter/kimi.test.ts index 179d4176c0..31dc1afb93 100644 --- a/packages/core/tests/unit-test/model-adapter/kimi.test.ts +++ b/packages/core/tests/unit-test/model-adapter/kimi.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { kimiAdapters } from '@/ai-model/models/kimi'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const kimiAdapter = new ResolvedModelAdapter(kimiAdapters.kimi, 'kimi'); const kimi3Adapter = new ResolvedModelAdapter(kimiAdapters.kimi3, 'kimi3'); diff --git a/packages/core/tests/unit-test/model-adapter/mimo.test.ts b/packages/core/tests/unit-test/model-adapter/mimo.test.ts index 437b904929..18c24f67d6 100644 --- a/packages/core/tests/unit-test/model-adapter/mimo.test.ts +++ b/packages/core/tests/unit-test/model-adapter/mimo.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { mimoAdapters } from '@/ai-model/models/mimo'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const mimoAdapter = new ResolvedModelAdapter( mimoAdapters['xiaomi-mimo'], diff --git a/packages/core/tests/unit-test/model-adapter/qwen.test.ts b/packages/core/tests/unit-test/model-adapter/qwen.test.ts index 1a455a6c60..2048e3b172 100644 --- a/packages/core/tests/unit-test/model-adapter/qwen.test.ts +++ b/packages/core/tests/unit-test/model-adapter/qwen.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { qwenAdapters } from '@/ai-model/models/qwen'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const qwen25Adapter = new ResolvedModelAdapter( qwenAdapters['qwen2.5-vl'], diff --git a/packages/core/tests/unit-test/model-adapter/ui-tars/__snapshots__/prompt.test.ts.snap b/packages/core/tests/unit-test/model-adapter/ui-tars/__snapshots__/prompt.test.ts.snap index db06dd3abe..b759e4c235 100644 --- a/packages/core/tests/unit-test/model-adapter/ui-tars/__snapshots__/prompt.test.ts.snap +++ b/packages/core/tests/unit-test/model-adapter/ui-tars/__snapshots__/prompt.test.ts.snap @@ -1,4 +1,4 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +// Rstest Snapshot v1 exports[`ui-tars prompt > renders UI-TARS planning prompt 1`] = ` " diff --git a/packages/core/tests/unit-test/model-adapter/ui-tars/actions.test.ts b/packages/core/tests/unit-test/model-adapter/ui-tars/actions.test.ts index 4c520e399e..353bfb5a5f 100644 --- a/packages/core/tests/unit-test/model-adapter/ui-tars/actions.test.ts +++ b/packages/core/tests/unit-test/model-adapter/ui-tars/actions.test.ts @@ -1,7 +1,7 @@ import { transformUiTarsActions } from '@/ai-model/models/ui-tars/actions'; import type { UiTarsParsedPlanningResponse } from '@/ai-model/models/ui-tars/parser'; import type { PlanningAction } from '@/types'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; type UiTarsActionParam = { locate?: Record; diff --git a/packages/core/tests/unit-test/model-adapter/ui-tars/adapter-json-repair.test.ts b/packages/core/tests/unit-test/model-adapter/ui-tars/adapter-json-repair.test.ts index 72a3e185c9..65f011e53e 100644 --- a/packages/core/tests/unit-test/model-adapter/ui-tars/adapter-json-repair.test.ts +++ b/packages/core/tests/unit-test/model-adapter/ui-tars/adapter-json-repair.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { uiTarsAdapters } from '@/ai-model/models/ui-tars/adapter'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const uiTarsAdapter = new ResolvedModelAdapter( uiTarsAdapters['vlm-ui-tars'], diff --git a/packages/core/tests/unit-test/model-adapter/ui-tars/adapter.test.ts b/packages/core/tests/unit-test/model-adapter/ui-tars/adapter.test.ts index e0e4781d16..171781826f 100644 --- a/packages/core/tests/unit-test/model-adapter/ui-tars/adapter.test.ts +++ b/packages/core/tests/unit-test/model-adapter/ui-tars/adapter.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { uiTarsAdapters } from '@/ai-model/models/ui-tars/adapter'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const uiTarsAdapter = new ResolvedModelAdapter( uiTarsAdapters['vlm-ui-tars'], diff --git a/packages/core/tests/unit-test/model-adapter/ui-tars/json-parser.test.ts b/packages/core/tests/unit-test/model-adapter/ui-tars/json-parser.test.ts index a22988ec61..6be8ce4d3c 100644 --- a/packages/core/tests/unit-test/model-adapter/ui-tars/json-parser.test.ts +++ b/packages/core/tests/unit-test/model-adapter/ui-tars/json-parser.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { uiTarsAdapters } from '@/ai-model/models/ui-tars/adapter'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const uiTarsAdapter = new ResolvedModelAdapter( uiTarsAdapters['vlm-ui-tars'], diff --git a/packages/core/tests/unit-test/model-adapter/ui-tars/locate-result.test.ts b/packages/core/tests/unit-test/model-adapter/ui-tars/locate-result.test.ts index 6ee85fa233..854f14dd4d 100644 --- a/packages/core/tests/unit-test/model-adapter/ui-tars/locate-result.test.ts +++ b/packages/core/tests/unit-test/model-adapter/ui-tars/locate-result.test.ts @@ -1,6 +1,6 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { uiTarsAdapters } from '@/ai-model/models/ui-tars/adapter'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const uiTarsAdapter = new ResolvedModelAdapter( uiTarsAdapters['vlm-ui-tars'], diff --git a/packages/core/tests/unit-test/model-adapter/ui-tars/planning-response-parser.test.ts b/packages/core/tests/unit-test/model-adapter/ui-tars/planning-response-parser.test.ts index 28f098313e..35f3dbcf05 100644 --- a/packages/core/tests/unit-test/model-adapter/ui-tars/planning-response-parser.test.ts +++ b/packages/core/tests/unit-test/model-adapter/ui-tars/planning-response-parser.test.ts @@ -8,11 +8,11 @@ import { ConversationHistory } from '@/ai-model/workflows/planning/conversation- import type { PlanOptions } from '@/ai-model/workflows/planning/types'; import type { UIContext } from '@/types'; import { UITarsModelVersion } from '@midscene/shared/env'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { actionParser } from '@ui-tars/action-parser'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -vi.mock('@ui-tars/action-parser', () => ({ - actionParser: vi.fn(), +rs.mock('@ui-tars/action-parser', () => ({ + actionParser: rs.fn(), })); const context: UIContext = { @@ -57,11 +57,11 @@ function parsedResponse( describe('parseUiTarsPlanningResponse failures', () => { beforeEach(() => { - vi.mocked(actionParser).mockReset(); + rs.mocked(actionParser).mockReset(); }); it('throws action parser exceptions directly', () => { - vi.mocked(actionParser).mockImplementationOnce(() => { + rs.mocked(actionParser).mockImplementationOnce(() => { throw new Error('parser exploded'); }); @@ -75,7 +75,7 @@ describe('parseUiTarsPlanningResponse failures', () => { }); it('converts bbox tags to center coordinates before parsing', () => { - vi.mocked(actionParser).mockReturnValueOnce({ parsed: [] }); + rs.mocked(actionParser).mockReturnValueOnce({ parsed: [] }); parseUiTarsPlanningResponse( "Thought: Click converted bbox\nAction: click(start_box='400 300 600 700')", diff --git a/packages/core/tests/unit-test/model-adapter/ui-tars/planning.test.ts b/packages/core/tests/unit-test/model-adapter/ui-tars/planning.test.ts index 58a5c87a98..556ec7bc9b 100644 --- a/packages/core/tests/unit-test/model-adapter/ui-tars/planning.test.ts +++ b/packages/core/tests/unit-test/model-adapter/ui-tars/planning.test.ts @@ -9,18 +9,18 @@ import { runCustomPlanning } from '@/ai-model/workflows/planning/custom-planning import type { PlanOptions } from '@/ai-model/workflows/planning/types'; import type { UIContext } from '@/types'; import { UITarsModelVersion } from '@midscene/shared/env'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import type { ChatCompletionUserMessageParam } from 'openai/resources/index'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mockActionSpace } from '../../../common'; -vi.mock('@/ai-model/service-caller/index', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - callAIWithStringResponse: vi.fn(), - }; -}); +import * as serviceCallerActual from '@/ai-model/service-caller/index' with { + rstest: 'importActual', +}; + +rs.mock('@/ai-model/service-caller/index', () => ({ + ...serviceCallerActual, + callAIWithStringResponse: rs.fn(), +})); const context: UIContext = { screenshot: { @@ -72,7 +72,7 @@ function runUiTarsPlanning( describe('createUiTarsPlanner', () => { beforeEach(() => { - vi.mocked(callAIWithStringResponse).mockReset(); + rs.mocked(callAIWithStringResponse).mockReset(); }); it('runs UI-TARS planning through the resolved adapter planner', async () => { @@ -80,7 +80,7 @@ describe('createUiTarsPlanner', () => { if (uiTarsAdapter.planning.kind !== 'custom') { throw new Error('UI-TARS should use custom planning adapter'); } - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: `Thought: Click submit Action: click(start_box='(500,500)')`, }); @@ -110,7 +110,7 @@ Action: click(start_box='(500,500)')`, }); it('stops planning when UI-TARS returns a finished action', async () => { - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: "finished(content='已经将计数器加到3,任务完成。')", }); @@ -145,7 +145,7 @@ Action: click(start_box='(500,500)')`, ]; const conversationHistory = new ConversationHistory(); - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: `Thought: Click submit Action: click(start_box='(500,500)')`, usage: { total_tokens: 33 } as any, @@ -163,7 +163,7 @@ Action: click(start_box='(500,500)')`, UITarsModelVersion.V1_0, ); - const [messages, runtime, callOptions] = vi.mocked(callAIWithStringResponse) + const [messages, runtime, callOptions] = rs.mocked(callAIWithStringResponse) .mock.calls[0]; expect(runtime).toBe(modelRuntime); expect(callOptions).toEqual({ @@ -200,7 +200,7 @@ Action: click(start_box='(500,500)')`, }); it('wraps malformed UI-TARS planning responses with raw response and usage', async () => { - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: 'Thought: I know what to do, but no action line.', usage: { total_tokens: 5 } as any, rawChoiceMessage: { role: 'assistant', content: 'bad response' } as any, diff --git a/packages/core/tests/unit-test/model-adapter/ui-tars/prompt.test.ts b/packages/core/tests/unit-test/model-adapter/ui-tars/prompt.test.ts index 7d7ec7d319..eefe95e49b 100644 --- a/packages/core/tests/unit-test/model-adapter/ui-tars/prompt.test.ts +++ b/packages/core/tests/unit-test/model-adapter/ui-tars/prompt.test.ts @@ -2,16 +2,17 @@ import { getSummary, getUiTarsPlanningPrompt, } from '@/ai-model/models/ui-tars/prompt'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { mockNonChinaTimeZone, restoreIntl } from '../../mocks/intl-mock'; -vi.mock('@midscene/shared/env', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getPreferredLanguage: vi.fn().mockReturnValue('English'), - }; -}); +import * as sharedEnvActual from '@midscene/shared/env' with { + rstest: 'importActual', +}; + +rs.mock('@midscene/shared/env', () => ({ + ...sharedEnvActual, + getPreferredLanguage: rs.fn().mockReturnValue('English'), +})); describe('ui-tars prompt', () => { it('renders UI-TARS planning prompt', () => { diff --git a/packages/core/tests/unit-test/parse-action.test.ts b/packages/core/tests/unit-test/parse-action.test.ts index 0fddb202d8..5e2db97c1d 100644 --- a/packages/core/tests/unit-test/parse-action.test.ts +++ b/packages/core/tests/unit-test/parse-action.test.ts @@ -1,5 +1,5 @@ +import { describe, expect, it } from '@rstest/core'; import { actionParser } from '@ui-tars/action-parser'; -import { describe, expect, it } from 'vitest'; describe('parse action from vlm', () => { it('should parse action with no Thought format', () => { diff --git a/packages/core/tests/unit-test/planning-tap-locator.test.ts b/packages/core/tests/unit-test/planning-tap-locator.test.ts index 4de80d40e3..6b4a02a74e 100644 --- a/packages/core/tests/unit-test/planning-tap-locator.test.ts +++ b/packages/core/tests/unit-test/planning-tap-locator.test.ts @@ -3,10 +3,10 @@ import { AIResponseParseError } from '@/ai-model/service-caller'; import { resolvePlanningTapLocator } from '@/ai-model/workflows/grounding/planning-action-locate'; import { runCustomPlanning } from '@/ai-model/workflows/planning/custom-planning'; import { ScreenshotItem } from '@/screenshot-item'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -vi.mock('@/ai-model/workflows/planning/custom-planning', () => ({ - runCustomPlanning: vi.fn(), +rs.mock('@/ai-model/workflows/planning/custom-planning', () => ({ + runCustomPlanning: rs.fn(), })); function createPlanner(): ResolvedCustomPlanningDefinition { @@ -80,12 +80,12 @@ function createLocateRequest() { describe('resolvePlanningTapLocator', () => { beforeEach(() => { - vi.mocked(runCustomPlanning).mockReset(); + rs.mocked(runCustomPlanning).mockReset(); }); it('runs the resolved planner once with tap locate options and returns the configured bbox', async () => { const actions = [{ type: 'Tap', param: {} }]; - vi.mocked(runCustomPlanning).mockResolvedValueOnce({ + rs.mocked(runCustomPlanning).mockResolvedValueOnce({ actions, shouldContinuePlanning: false, rawResponse: 'raw planning response', @@ -94,7 +94,7 @@ describe('resolvePlanningTapLocator', () => { log: 'planner reasoning', }); - const getLocatedPixelBbox = vi.fn((): [number, number, number, number] => [ + const getLocatedPixelBbox = rs.fn((): [number, number, number, number] => [ 1, 2, 3, 4, ]); const locate = resolvePlanningTapLocator( @@ -112,7 +112,7 @@ describe('resolvePlanningTapLocator', () => { ); const [, planOptions, locatorPlanner] = - vi.mocked(runCustomPlanning).mock.calls[0]; + rs.mocked(runCustomPlanning).mock.calls[0]; expect(planOptions.context.screenshot.base64).toBe( 'data:image/png;base64,CROP==', ); @@ -142,7 +142,7 @@ describe('resolvePlanningTapLocator', () => { }); it('returns an error when the planner actions do not contain a tap bbox', async () => { - vi.mocked(runCustomPlanning).mockResolvedValueOnce({ + rs.mocked(runCustomPlanning).mockResolvedValueOnce({ actions: [{ type: 'Scroll', param: {} }], shouldContinuePlanning: false, rawResponse: 'raw planning response', @@ -175,7 +175,7 @@ describe('resolvePlanningTapLocator', () => { it('preserves raw response metadata from planner parse errors', async () => { const rawChoiceMessage = { role: 'assistant', content: 'bad response' }; const usage = { total_tokens: 5 } as any; - vi.mocked(runCustomPlanning).mockRejectedValueOnce( + rs.mocked(runCustomPlanning).mockRejectedValueOnce( new AIResponseParseError( 'Parse error: malformed response', 'raw malformed response', diff --git a/packages/core/tests/unit-test/player-action-dispatch.test.ts b/packages/core/tests/unit-test/player-action-dispatch.test.ts index 30629b3aa8..952ab929dc 100644 --- a/packages/core/tests/unit-test/player-action-dispatch.test.ts +++ b/packages/core/tests/unit-test/player-action-dispatch.test.ts @@ -1,6 +1,6 @@ import { buildYamlFlowFromPlans } from '@/common'; import { ScriptPlayer } from '@/yaml/player'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { z } from 'zod'; const runAdbShellParamSchema = z.object({ @@ -33,10 +33,10 @@ function createPlayerWithActionSpace(actionSpace: any[]) { function createMockAgent(overrides: Record = {}) { return { - callActionInActionSpace: vi.fn().mockResolvedValue('action-result'), - launch: vi.fn().mockResolvedValue('launch-result'), - terminate: vi.fn().mockResolvedValue('terminate-result'), - runAdbShell: vi.fn().mockResolvedValue('adb-result'), + callActionInActionSpace: rs.fn().mockResolvedValue('action-result'), + launch: rs.fn().mockResolvedValue('launch-result'), + terminate: rs.fn().mockResolvedValue('terminate-result'), + runAdbShell: rs.fn().mockResolvedValue('adb-result'), ...overrides, } as any; } @@ -249,7 +249,7 @@ describe('player action dispatch ordering', () => { ]; const player = createPlayerWithActionSpace(actionSpace); const agent = { - callActionInActionSpace: vi.fn().mockResolvedValue('launch-via-action'), + callActionInActionSpace: rs.fn().mockResolvedValue('launch-via-action'), } as any; const taskStatus = { @@ -277,7 +277,7 @@ describe('player action dispatch ordering', () => { ]; const player = createPlayerWithActionSpace(actionSpace); const agent = { - callActionInActionSpace: vi + callActionInActionSpace: rs .fn() .mockResolvedValue('terminate-via-action'), } as any; @@ -307,7 +307,7 @@ describe('player action dispatch ordering', () => { ]; const player = createPlayerWithActionSpace(actionSpace); const agent = createMockAgent({ - callActionInActionSpace: vi.fn().mockResolvedValue('shell output'), + callActionInActionSpace: rs.fn().mockResolvedValue('shell output'), }); const taskStatus = { @@ -333,7 +333,7 @@ describe('player action dispatch ordering', () => { ]; const player = createPlayerWithActionSpace(actionSpace); const agent = { - callActionInActionSpace: vi.fn().mockResolvedValue('fallback-result'), + callActionInActionSpace: rs.fn().mockResolvedValue('fallback-result'), } as any; const taskStatus = { @@ -376,7 +376,7 @@ describe('player action dispatch ordering', () => { it('should pass ${var} text through as a literal value', async () => { const player = createPlayerWithActionSpace([]); const agent = createMockAgent({ - aiQuery: vi.fn().mockResolvedValue('query-result'), + aiQuery: rs.fn().mockResolvedValue('query-result'), }); player.result.product_id = '110'; @@ -419,7 +419,7 @@ describe('player action dispatch ordering', () => { it('should pass variable-like values through in nested objects', async () => { const player = createPlayerWithActionSpace([]); const agent = createMockAgent({ - aiTap: vi.fn().mockResolvedValue('tap-result'), + aiTap: rs.fn().mockResolvedValue('tap-result'), }); player.result.prompt_text = 'search box'; diff --git a/packages/core/tests/unit-test/prompt-context.test.ts b/packages/core/tests/unit-test/prompt-context.test.ts index 24c715ffa7..47990e1a27 100644 --- a/packages/core/tests/unit-test/prompt-context.test.ts +++ b/packages/core/tests/unit-test/prompt-context.test.ts @@ -2,7 +2,7 @@ import { buildLocatePromptWithContext, buildPromptWithContext, } from '@/agent/prompt-context'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('buildPromptWithContext', () => { it('returns the original string prompt when context is undefined or blank', () => { diff --git a/packages/core/tests/unit-test/prompt/__snapshots__/describe.test.ts.snap b/packages/core/tests/unit-test/prompt/__snapshots__/describe.test.ts.snap index ff49392923..be9a52e805 100644 --- a/packages/core/tests/unit-test/prompt/__snapshots__/describe.test.ts.snap +++ b/packages/core/tests/unit-test/prompt/__snapshots__/describe.test.ts.snap @@ -1,4 +1,4 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +// Rstest Snapshot v1 exports[`elementDescriberInstruction > should return the correct instruction 1`] = ` " diff --git a/packages/core/tests/unit-test/prompt/__snapshots__/prompt.test.ts.snap b/packages/core/tests/unit-test/prompt/__snapshots__/prompt.test.ts.snap index a987985c47..89e169172b 100644 --- a/packages/core/tests/unit-test/prompt/__snapshots__/prompt.test.ts.snap +++ b/packages/core/tests/unit-test/prompt/__snapshots__/prompt.test.ts.snap @@ -1,4 +1,4 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +// Rstest Snapshot v1 exports[`extract element > extract element by extractDataPrompt - object 1`] = ` " diff --git a/packages/core/tests/unit-test/prompt/describe.test.ts b/packages/core/tests/unit-test/prompt/describe.test.ts index 706e7adb43..2f08387bf4 100644 --- a/packages/core/tests/unit-test/prompt/describe.test.ts +++ b/packages/core/tests/unit-test/prompt/describe.test.ts @@ -1,9 +1,9 @@ import { elementDescriberInstruction } from '@/ai-model/prompt/describe'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; describe('elementDescriberInstruction', () => { - vi.mock('@midscene/shared/env', () => ({ - getPreferredLanguage: vi.fn().mockReturnValue('English'), + rs.mock('@midscene/shared/env', () => ({ + getPreferredLanguage: rs.fn().mockReturnValue('English'), })); it('should return the correct instruction', () => { diff --git a/packages/core/tests/unit-test/prompt/planning/action-description.test.ts b/packages/core/tests/unit-test/prompt/planning/action-description.test.ts index ac8a872359..3ca6260cb5 100644 --- a/packages/core/tests/unit-test/prompt/planning/action-description.test.ts +++ b/packages/core/tests/unit-test/prompt/planning/action-description.test.ts @@ -14,8 +14,8 @@ import { defineActionSwipe, } from '@/device'; import { getMidsceneLocationSchema } from '@/index'; +import { describe, expect, it } from '@rstest/core'; import yaml from 'js-yaml'; -import { describe, expect, it } from 'vitest'; import { z } from 'zod'; const defaultMidscenePlanningProtocol = createDefaultMidscenePlanningProtocol({ diff --git a/packages/core/tests/unit-test/prompt/planning/action-output-example.test.ts b/packages/core/tests/unit-test/prompt/planning/action-output-example.test.ts index 0e963581a0..bce0119aa1 100644 --- a/packages/core/tests/unit-test/prompt/planning/action-output-example.test.ts +++ b/packages/core/tests/unit-test/prompt/planning/action-output-example.test.ts @@ -9,7 +9,7 @@ import { import { parseModelResponseJson } from '@/ai-model/shared/json'; import type { LocateResultPromptSpec } from '@/ai-model/shared/model-locate-result'; import { getMidsceneLocationSchema } from '@/common'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { z } from 'zod'; const defaultMidscenePlanningProtocol = createDefaultMidscenePlanningProtocol({ diff --git a/packages/core/tests/unit-test/prompt/planning/action-space-description.test.ts b/packages/core/tests/unit-test/prompt/planning/action-space-description.test.ts index 168ee4a265..1518d99b12 100644 --- a/packages/core/tests/unit-test/prompt/planning/action-space-description.test.ts +++ b/packages/core/tests/unit-test/prompt/planning/action-space-description.test.ts @@ -2,7 +2,7 @@ import type { StandardPlanningProtocol } from '@/ai-model/model-adapter/planning import { buildPlanningActionSpaceDescription } from '@/ai-model/prompt/planning'; import type { LocateResultPromptSpec } from '@/ai-model/shared/model-locate-result'; import { getMidsceneLocationSchema } from '@/index'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { z } from 'zod'; describe('buildPlanningActionSpaceDescription', () => { @@ -15,8 +15,8 @@ describe('buildPlanningActionSpaceDescription', () => { resultNounPlural: 'points', exampleValues: [[500, 500]], }; - const buildLocateFieldDescription = vi.fn(() => 'LOCATE_FIELD'); - const buildActionOutput = vi.fn( + const buildLocateFieldDescription = rs.fn(() => 'LOCATE_FIELD'); + const buildActionOutput = rs.fn( ({ actionName }: { actionName: string }) => `${actionName}`, ); const planningProtocol: StandardPlanningProtocol = { diff --git a/packages/core/tests/unit-test/prompt/planning/planning-response-example.test.ts b/packages/core/tests/unit-test/prompt/planning/planning-response-example.test.ts index 08461c489c..7a662fcc12 100644 --- a/packages/core/tests/unit-test/prompt/planning/planning-response-example.test.ts +++ b/packages/core/tests/unit-test/prompt/planning/planning-response-example.test.ts @@ -1,5 +1,5 @@ import { buildPlanningResponseExample } from '@/ai-model/prompt/planning'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('buildPlanningResponseExample', () => { it('builds a planning response with sub-goal state and an action', () => { diff --git a/packages/core/tests/unit-test/prompt/planning/sub-goals-text.test.ts b/packages/core/tests/unit-test/prompt/planning/sub-goals-text.test.ts index 84cd056127..ccec60ef2f 100644 --- a/packages/core/tests/unit-test/prompt/planning/sub-goals-text.test.ts +++ b/packages/core/tests/unit-test/prompt/planning/sub-goals-text.test.ts @@ -1,5 +1,5 @@ import { buildSubGoalsText } from '@/ai-model/prompt/planning'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('buildSubGoalsText', () => { it('returns an empty string when no sub-goals are provided', () => { diff --git a/packages/core/tests/unit-test/prompt/prompt.test.ts b/packages/core/tests/unit-test/prompt/prompt.test.ts index 7aa20e32c3..7b31615b32 100644 --- a/packages/core/tests/unit-test/prompt/prompt.test.ts +++ b/packages/core/tests/unit-test/prompt/prompt.test.ts @@ -13,7 +13,7 @@ import { buildStandardPlanningSystemPrompt } from '@/ai-model/prompt/planning'; import { parseModelResponseJson } from '@/ai-model/shared/json'; import type { LocateResultPromptSpec } from '@/ai-model/shared/model-locate-result'; import type { TModelFamily } from '@midscene/shared/env'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { z } from 'zod'; import { extractDataQueryPrompt, @@ -21,18 +21,19 @@ import { } from '../../../src/ai-model/prompt/extraction'; import { mockActionSpace } from '../../common'; +import * as sharedEnvActual from '@midscene/shared/env' with { + rstest: 'importActual', +}; + const defaultMidscenePlanningProtocol = createDefaultMidscenePlanningProtocol({ jsonParser: parseModelResponseJson, }); // Mock getPreferredLanguage to ensure consistent test output -vi.mock('@midscene/shared/env', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getPreferredLanguage: vi.fn().mockReturnValue('English'), - }; -}); +rs.mock('@midscene/shared/env', () => ({ + ...sharedEnvActual, + getPreferredLanguage: rs.fn().mockReturnValue('English'), +})); const locatePromptSpecFor = ( modelFamily: TModelFamily, @@ -127,7 +128,7 @@ describe('system prompts', () => { actionOutputPlaceholder: '...', buildActionOutput: ({ actionName }) => ``, - parseActionOutput: vi.fn(), + parseActionOutput: rs.fn(), }; const planningProtocol = { actionSpaceProtocol: { diff --git a/packages/core/tests/unit-test/proxy-configuration.test.ts b/packages/core/tests/unit-test/proxy-configuration.test.ts index ba893a92df..67f966d49d 100644 --- a/packages/core/tests/unit-test/proxy-configuration.test.ts +++ b/packages/core/tests/unit-test/proxy-configuration.test.ts @@ -7,27 +7,27 @@ import type { IModelConfig } from '@midscene/shared/env'; * applied when creating OpenAI clients. Uses mocking to verify that the correct * proxy implementations are instantiated with proper parameters. */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; // Mock undici and fetch-socks before importing service-caller -const mockProxyAgent = vi.fn(); -const mockSocksDispatcher = vi.fn(); +const mockProxyAgent = rs.fn(); +const mockSocksDispatcher = rs.fn(); -vi.mock('undici', () => ({ +rs.mock('undici', () => ({ ProxyAgent: mockProxyAgent, })); -vi.mock('fetch-socks', () => ({ +rs.mock('fetch-socks', () => ({ socksDispatcher: mockSocksDispatcher, })); // Mock OpenAI to avoid actual API calls -vi.mock('openai', () => { +rs.mock('openai', () => { return { - default: vi.fn().mockImplementation(() => ({ + default: rs.fn().mockImplementation(() => ({ chat: { completions: { - create: vi.fn().mockResolvedValue({ + create: rs.fn().mockResolvedValue({ choices: [{ message: { content: 'test response' } }], usage: { prompt_tokens: 10, @@ -43,11 +43,11 @@ vi.mock('openai', () => { describe('Proxy Configuration', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); afterEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); describe('HTTP Proxy', () => { diff --git a/packages/core/tests/unit-test/recorder-frame-sequence.test.ts b/packages/core/tests/unit-test/recorder-frame-sequence.test.ts index 52e7c7b753..4a0482ffbe 100644 --- a/packages/core/tests/unit-test/recorder-frame-sequence.test.ts +++ b/packages/core/tests/unit-test/recorder-frame-sequence.test.ts @@ -1,7 +1,7 @@ import { recordAndReleaseScreenshotSequence } from '@/agent/tasks'; import { ScreenshotItem } from '@/screenshot-item'; import type { ExecutionTask, UIContext } from '@/types'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const makeUiContext = (frameCount: number): UIContext => { const frames = Array.from({ length: frameCount }, (_, i) => diff --git a/packages/core/tests/unit-test/report-cli.test.ts b/packages/core/tests/unit-test/report-cli.test.ts index f80b55149b..aa5c2a5b66 100644 --- a/packages/core/tests/unit-test/report-cli.test.ts +++ b/packages/core/tests/unit-test/report-cli.test.ts @@ -8,7 +8,7 @@ import { import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { parseCliArgs, runToolsCLI } from '@midscene/shared/cli'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; import { generateDumpScriptTag, generateImageScriptTag } from '../../src/dump'; import type { ScreenshotRef } from '../../src/dump/screenshot-store'; import { @@ -672,16 +672,16 @@ describe('createReportCliCommands', () => { }); const logs: string[] = []; - const consoleSpy = vi + const consoleSpy = rs .spyOn(console, 'log') .mockImplementation((...args: unknown[]) => { logs.push(args.map((a) => String(a)).join(' ')); }); const tools = { - initTools: vi.fn().mockResolvedValue(undefined), - destroy: vi.fn().mockResolvedValue(undefined), - getToolDefinitions: vi.fn().mockReturnValue([]), + initTools: rs.fn().mockResolvedValue(undefined), + destroy: rs.fn().mockResolvedValue(undefined), + getToolDefinitions: rs.fn().mockReturnValue([]), } as any; await runToolsCLI(tools, 'midscene-test', { diff --git a/packages/core/tests/unit-test/report-generator-async-contract.test.ts b/packages/core/tests/unit-test/report-generator-async-contract.test.ts index ecb7681cc6..66333ccbc3 100644 --- a/packages/core/tests/unit-test/report-generator-async-contract.test.ts +++ b/packages/core/tests/unit-test/report-generator-async-contract.test.ts @@ -16,7 +16,7 @@ * flaky on shared CI. This structural check is cheap, reliable, and * catches the 90% regression path (someone typing `Sync` back in). */ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { ScreenshotStore } from '../../src/dump/screenshot-store'; import { ReportGenerator } from '../../src/report-generator'; diff --git a/packages/core/tests/unit-test/report-generator-directory.test.ts b/packages/core/tests/unit-test/report-generator-directory.test.ts index 7477dcaa18..0df90a0903 100644 --- a/packages/core/tests/unit-test/report-generator-directory.test.ts +++ b/packages/core/tests/unit-test/report-generator-directory.test.ts @@ -9,7 +9,7 @@ import { join } from 'node:path'; import { parseDumpScript } from '@/dump/html-utils'; import { ReportGenerator } from '@/report-generator'; import { ScreenshotItem } from '@/screenshot-item'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; import { createExecution, createPatternedPngFixture, diff --git a/packages/core/tests/unit-test/report-generator-options.test.ts b/packages/core/tests/unit-test/report-generator-options.test.ts index 69de9a529c..87ceaa722a 100644 --- a/packages/core/tests/unit-test/report-generator-options.test.ts +++ b/packages/core/tests/unit-test/report-generator-options.test.ts @@ -2,7 +2,7 @@ import { existsSync, readdirSync, rmSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { ReportGenerator, nullReportGenerator } from '@/report-generator'; import { ScreenshotItem } from '@/screenshot-item'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; import { createExecution, defaultReportMeta, @@ -21,7 +21,7 @@ describe('ReportGenerator options and factory', () => { if (existsSync(temporaryDirectory)) { rmSync(temporaryDirectory, { recursive: true, force: true }); } - vi.restoreAllMocks(); + rs.restoreAllMocks(); }); it('provides a null generator when report generation is disabled', async () => { @@ -119,7 +119,7 @@ describe('ReportGenerator options and factory', () => { ); it('prints an inline report path once after the first write', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const logSpy = rs.spyOn(console, 'log').mockImplementation(() => {}); const reportPath = join(temporaryDirectory, 'autoprint-inline.html'); const generator = new ReportGenerator({ reportPath, @@ -143,7 +143,7 @@ describe('ReportGenerator options and factory', () => { }); it('supports disabled logging and directory-mode serve instructions', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const logSpy = rs.spyOn(console, 'log').mockImplementation(() => {}); const quietGenerator = new ReportGenerator({ reportPath: join(temporaryDirectory, 'quiet.html'), screenshotMode: 'inline', diff --git a/packages/core/tests/unit-test/report-generator-reference-images.test.ts b/packages/core/tests/unit-test/report-generator-reference-images.test.ts index d7a1f2d8b3..696fa2e51a 100644 --- a/packages/core/tests/unit-test/report-generator-reference-images.test.ts +++ b/packages/core/tests/unit-test/report-generator-reference-images.test.ts @@ -14,7 +14,7 @@ import { import { restoreImageReferences } from '@/dump/screenshot-restoration'; import { ReportGenerator } from '@/report-generator'; import { ExecutionDump, type ExecutionTaskPlanningParam } from '@/types'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; import { defaultReportMeta, fakeBase64, diff --git a/packages/core/tests/unit-test/report-generator.test.ts b/packages/core/tests/unit-test/report-generator.test.ts index d2ea0d2f4c..2a203b0db2 100644 --- a/packages/core/tests/unit-test/report-generator.test.ts +++ b/packages/core/tests/unit-test/report-generator.test.ts @@ -15,7 +15,7 @@ import * as reportDumpCompactor from '@/dump/report-dump-compactor'; import { ReportGenerator } from '@/report-generator'; import { ScreenshotItem } from '@/screenshot-item'; import { ExecutionDump, ReportActionDump, type UIContext } from '@/types'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; import { buildIncrementalExecution, createExecution, @@ -246,7 +246,7 @@ describe('ReportGenerator — append-only model', () => { ); const compactionError = new Error('ENOSPC: no space left on device'); - const compactSpy = vi + const compactSpy = rs .spyOn(reportDumpCompactor, 'compactReportDumps') .mockRejectedValueOnce(compactionError); diff --git a/packages/core/tests/unit-test/report-issues.test.ts b/packages/core/tests/unit-test/report-issues.test.ts index 5921fc6a44..a02635db9d 100644 --- a/packages/core/tests/unit-test/report-issues.test.ts +++ b/packages/core/tests/unit-test/report-issues.test.ts @@ -14,7 +14,7 @@ import { ReportGenerator } from '@/report-generator'; import { ScreenshotItem } from '@/screenshot-item'; import { ExecutionDump, type ReportMeta } from '@/types'; import { antiEscapeScriptTag } from '@midscene/shared/utils'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { extractGroupedDumpScripts, getGroupedDumpScriptIds, diff --git a/packages/core/tests/unit-test/report-markdown.test.ts b/packages/core/tests/unit-test/report-markdown.test.ts index 7cfa5c9c04..3d6ba9e8b2 100644 --- a/packages/core/tests/unit-test/report-markdown.test.ts +++ b/packages/core/tests/unit-test/report-markdown.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { executionToMarkdown, reportToMarkdown, diff --git a/packages/core/tests/unit-test/report-merge-count.test.ts b/packages/core/tests/unit-test/report-merge-count.test.ts index d059cb7b1b..409b78f7ca 100644 --- a/packages/core/tests/unit-test/report-merge-count.test.ts +++ b/packages/core/tests/unit-test/report-merge-count.test.ts @@ -20,7 +20,7 @@ import { type UIContext, } from '@/types'; import { antiEscapeScriptTag } from '@midscene/shared/utils'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; // ---------- helpers ---------- diff --git a/packages/core/tests/unit-test/report-merge-status.test.ts b/packages/core/tests/unit-test/report-merge-status.test.ts index 946b87b464..22e80442da 100644 --- a/packages/core/tests/unit-test/report-merge-status.test.ts +++ b/packages/core/tests/unit-test/report-merge-status.test.ts @@ -16,7 +16,7 @@ import { } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; import { generateDumpScriptTag, generateImageScriptTag } from '../../src/dump'; import { mergeReportFiles } from '../../src/report-cli'; import { ScreenshotItem } from '../../src/screenshot-item'; diff --git a/packages/core/tests/unit-test/report-split.test.ts b/packages/core/tests/unit-test/report-split.test.ts index aa8fec590a..83cea8ee11 100644 --- a/packages/core/tests/unit-test/report-split.test.ts +++ b/packages/core/tests/unit-test/report-split.test.ts @@ -7,7 +7,7 @@ import { } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; import { generateDumpScriptTag, generateImageScriptTag } from '../../src/dump'; import type { ImageUrlRef, diff --git a/packages/core/tests/unit-test/report-stats.test.ts b/packages/core/tests/unit-test/report-stats.test.ts index 799118f07b..1a10f4bc86 100644 --- a/packages/core/tests/unit-test/report-stats.test.ts +++ b/packages/core/tests/unit-test/report-stats.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { collectReportSummary } from '../../src/report-stats'; import type { AIUsageInfo, diff --git a/packages/core/tests/unit-test/report.test.ts b/packages/core/tests/unit-test/report.test.ts index 3b53277695..d99c600d6f 100644 --- a/packages/core/tests/unit-test/report.test.ts +++ b/packages/core/tests/unit-test/report.test.ts @@ -8,7 +8,7 @@ import { } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { extractAllDumpScriptsSync, generateDumpScriptTag, diff --git a/packages/core/tests/unit-test/run-gherkin-scenario.test.ts b/packages/core/tests/unit-test/run-gherkin-scenario.test.ts index 483acd2394..94ad90cd55 100644 --- a/packages/core/tests/unit-test/run-gherkin-scenario.test.ts +++ b/packages/core/tests/unit-test/run-gherkin-scenario.test.ts @@ -1,11 +1,11 @@ import { Agent } from '@/agent'; import { parseGherkinScenario } from '@/agent/run-gherkin-scenario'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const createAgentStub = () => { const agent = Object.create(Agent.prototype) as Agent; - (agent as any).aiAct = vi.fn(async () => undefined); - (agent as any).aiAssert = vi.fn(async () => undefined); + (agent as any).aiAct = rs.fn(async () => undefined); + (agent as any).aiAssert = rs.fn(async () => undefined); return agent; }; @@ -236,7 +236,7 @@ Then the todo list contains "Buy milk" it('wraps step execution errors with semantic action, line, and step context', async () => { const agent = createAgentStub(); - (agent as any).aiAssert = vi.fn(async () => { + (agent as any).aiAssert = rs.fn(async () => { throw new Error('not visible'); }); @@ -252,7 +252,7 @@ Then the list should be empty it('reports inherited And or But semantics in execution errors', async () => { const agent = createAgentStub(); - (agent as any).aiAssert = vi + (agent as any).aiAssert = rs .fn() .mockResolvedValueOnce(undefined) .mockRejectedValueOnce(new Error('not visible')); diff --git a/packages/core/tests/unit-test/run-markdown.test.ts b/packages/core/tests/unit-test/run-markdown.test.ts index 8c40d4d31e..95018b4834 100644 --- a/packages/core/tests/unit-test/run-markdown.test.ts +++ b/packages/core/tests/unit-test/run-markdown.test.ts @@ -4,13 +4,13 @@ import { dirname, join } from 'node:path'; import { Agent } from '@/agent'; import { markdownToAiActPrompt } from '@/agent/run-markdown'; import { paramStr } from '@/agent/ui-utils'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; let tempDir: string | undefined; const createAgentStub = () => { const agent = Object.create(Agent.prototype) as Agent; - (agent as any).aiAct = vi.fn(async () => 'done'); + (agent as any).aiAct = rs.fn(async () => 'done'); return agent; }; @@ -27,7 +27,7 @@ describe('runMarkdown prompt transform', () => { await rm(tempDir, { recursive: true, force: true }); tempDir = undefined; } - vi.restoreAllMocks(); + rs.restoreAllMocks(); }); it('replaces Markdown images with numbered reference image names', async () => { diff --git a/packages/core/tests/unit-test/screenshot-item.test.ts b/packages/core/tests/unit-test/screenshot-item.test.ts index d20c0246f1..19a9886449 100644 --- a/packages/core/tests/unit-test/screenshot-item.test.ts +++ b/packages/core/tests/unit-test/screenshot-item.test.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; import { ScreenshotItem } from '../../src/screenshot-item'; describe('ScreenshotItem', () => { diff --git a/packages/core/tests/unit-test/screenshot-persistence-preparation.test.ts b/packages/core/tests/unit-test/screenshot-persistence-preparation.test.ts index b4196e97ba..0fd3c9511f 100644 --- a/packages/core/tests/unit-test/screenshot-persistence-preparation.test.ts +++ b/packages/core/tests/unit-test/screenshot-persistence-preparation.test.ts @@ -1,17 +1,17 @@ import { prepareScreenshotForPersistence } from '@/agent/screenshot-preparation'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -const imageMocks = vi.hoisted(() => ({ - convertBase64ImageToJpeg: vi.fn(), - imageInfoOfBase64: vi.fn(), - resizeBase64ImageToJpeg: vi.fn(), +const imageMocks = rs.hoisted(() => ({ + convertBase64ImageToJpeg: rs.fn(), + imageInfoOfBase64: rs.fn(), + resizeBase64ImageToJpeg: rs.fn(), })); -vi.mock('@midscene/shared/img', () => imageMocks); +rs.mock('@midscene/shared/img', () => imageMocks); describe('prepareScreenshotForPersistence', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); imageMocks.convertBase64ImageToJpeg.mockResolvedValue( 'data:image/jpeg;base64,prepared', ); diff --git a/packages/core/tests/unit-test/screenshot-preparation.test.ts b/packages/core/tests/unit-test/screenshot-preparation.test.ts index ae92d12577..57f281e445 100644 --- a/packages/core/tests/unit-test/screenshot-preparation.test.ts +++ b/packages/core/tests/unit-test/screenshot-preparation.test.ts @@ -1,6 +1,6 @@ import { prepareRawScreenshot } from '@/agent/screenshot-preparation'; import { imageInfoOfBase64 } from '@midscene/shared/img'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const pngDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAGCAIAAABxZ0isAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAEUlEQVR4nGMQqbiDFTEMpAQAorNDgTX/VEoAAAAASUVORK5CYII='; diff --git a/packages/core/tests/unit-test/screenshot-store.test.ts b/packages/core/tests/unit-test/screenshot-store.test.ts index bbec2e86f5..f241e726be 100644 --- a/packages/core/tests/unit-test/screenshot-store.test.ts +++ b/packages/core/tests/unit-test/screenshot-store.test.ts @@ -7,7 +7,7 @@ import { } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; import { ScreenshotStore, normalizeImageUrlRef, @@ -111,7 +111,7 @@ describe('ScreenshotStore', () => { it('deduplicates inline reference images by content', async () => { const reportPath = join(tmpRoot, 'reference-inline.html'); - const appendInline = vi.fn(); + const appendInline = rs.fn(); const store = new ScreenshotStore({ mode: 'inline', reportPath, @@ -207,7 +207,7 @@ describe('ScreenshotStore', () => { it('supports inline mode persistence + lazy restore', async () => { const reportPath = join(tmpRoot, 'inline.html'); - const appendInline = vi.fn((id: string, base64: string) => { + const appendInline = rs.fn((id: string, base64: string) => { writeFileSync( reportPath, ``, @@ -229,7 +229,7 @@ describe('ScreenshotStore', () => { it('can ensure shared file copy while preserving inline mode semantics', async () => { const reportPath = join(tmpRoot, 'inline-with-file-copy.html'); const screenshotsDir = join(tmpRoot, 'screenshots'); - const appendInline = vi.fn((id: string, base64: string) => { + const appendInline = rs.fn((id: string, base64: string) => { writeFileSync( reportPath, ``, @@ -257,7 +257,7 @@ describe('ScreenshotStore', () => { it('keeps supporting ensureFileCopy as a deprecated alias', async () => { const reportPath = join(tmpRoot, 'inline-with-deprecated-file-copy.html'); const screenshotsDir = join(tmpRoot, 'screenshots'); - const appendInline = vi.fn((id: string, base64: string) => { + const appendInline = rs.fn((id: string, base64: string) => { writeFileSync( reportPath, ``, diff --git a/packages/core/tests/unit-test/search-area.test.ts b/packages/core/tests/unit-test/search-area.test.ts index a73759432d..40ed0bb00e 100644 --- a/packages/core/tests/unit-test/search-area.test.ts +++ b/packages/core/tests/unit-test/search-area.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { mergePixelBboxesToRect } from '@/ai-model/workflows/grounding/locate-result-rect'; import { expandSearchArea } from '@/ai-model/workflows/grounding/search-area'; diff --git a/packages/core/tests/unit-test/section-locate-protocol.test.ts b/packages/core/tests/unit-test/section-locate-protocol.test.ts index f1f2c40db0..d54c432cce 100644 --- a/packages/core/tests/unit-test/section-locate-protocol.test.ts +++ b/packages/core/tests/unit-test/section-locate-protocol.test.ts @@ -2,18 +2,17 @@ import { ResolvedModelAdapter } from '@/ai-model/model-adapter/resolve'; import { callAI } from '@/ai-model/service-caller/index'; import { AiLocateSection } from '@/ai-model/workflows/grounding'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { createFakeContext } from '../utils'; -vi.mock('@/ai-model/service-caller/index', async () => { - const actual = await vi.importActual< - typeof import('@/ai-model/service-caller/index') - >('@/ai-model/service-caller/index'); - return { - ...actual, - callAI: vi.fn(), - }; -}); +import * as serviceCallerActual from '@/ai-model/service-caller/index' with { + rstest: 'importActual', +}; + +rs.mock('@/ai-model/service-caller/index', () => ({ + ...serviceCallerActual, + callAI: rs.fn(), +})); describe('section locate protocol', () => { const modelConfig: IModelConfig = { @@ -26,21 +25,21 @@ describe('section locate protocol', () => { }; beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(callAI).mockResolvedValue({ + rs.clearAllMocks(); + rs.mocked(callAI).mockResolvedValue({ content: 'custom section response', isStreamed: false, }); }); it('uses the search-area protocol to build and parse the model call', async () => { - const buildResponseInstructions = vi.fn( + const buildResponseInstructions = rs.fn( () => 'Custom search-area response instructions', ); - const buildUserPrompt = vi.fn( + const buildUserPrompt = rs.fn( (description: string) => `Custom search-area task: ${description}`, ); - const parseRawResponse = vi.fn(() => ({ + const parseRawResponse = rs.fn(() => ({ kind: 'located' as const, target: [100, 200, 300, 400], })); @@ -89,12 +88,12 @@ describe('section locate protocol', () => { 'custom section response', adapter.locate.searchArea?.resultCodec.promptSpec, ); - expect(vi.mocked(callAI).mock.calls[0][0][0]).toMatchObject({ + expect(rs.mocked(callAI).mock.calls[0][0][0]).toMatchObject({ content: expect.stringContaining( 'You are an AI assistant that helps identify UI elements.', ), }); - expect(vi.mocked(callAI).mock.calls[0][0][0]).toMatchObject({ + expect(rs.mocked(callAI).mock.calls[0][0][0]).toMatchObject({ content: expect.not.stringContaining('Custom search-area introduction'), }); expect(callAI).toHaveBeenCalledWith( diff --git a/packages/core/tests/unit-test/semantic-retry.test.ts b/packages/core/tests/unit-test/semantic-retry.test.ts index 5f034e654f..b7cf0357e6 100644 --- a/packages/core/tests/unit-test/semantic-retry.test.ts +++ b/packages/core/tests/unit-test/semantic-retry.test.ts @@ -2,7 +2,7 @@ import { callAiAndParseWithRetry, withSemanticRetryFeedback, } from '@/ai-model/service-caller/semantic-retry'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('callAiAndParseWithRetry', () => { it('increments the semantic retry attempt after parsing failures', async () => { diff --git a/packages/core/tests/unit-test/service-caller-empty-content.test.ts b/packages/core/tests/unit-test/service-caller-empty-content.test.ts index 5a73b8d6f7..587546b6fc 100644 --- a/packages/core/tests/unit-test/service-caller-empty-content.test.ts +++ b/packages/core/tests/unit-test/service-caller-empty-content.test.ts @@ -1,10 +1,10 @@ import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -const mockCreate = vi.fn(); +const mockCreate = rs.fn(); -vi.mock('openai', () => ({ - default: vi.fn().mockImplementation(() => ({ +rs.mock('openai', () => ({ + default: rs.fn().mockImplementation(() => ({ chat: { completions: { create: mockCreate, @@ -15,7 +15,7 @@ vi.mock('openai', () => ({ describe('service-caller empty content handling', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('should preserve usage when model returns empty content', async () => { diff --git a/packages/core/tests/unit-test/service-caller-openai-error.test.ts b/packages/core/tests/unit-test/service-caller-openai-error.test.ts index 3d94caf434..83e77cb44a 100644 --- a/packages/core/tests/unit-test/service-caller-openai-error.test.ts +++ b/packages/core/tests/unit-test/service-caller-openai-error.test.ts @@ -1,8 +1,8 @@ import type { IModelConfig } from '@midscene/shared/env'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; -const mockCreate = vi.fn(); -const mockOpenAIConstructor = vi.fn().mockImplementation(() => ({ +const mockCreate = rs.fn(); +const mockOpenAIConstructor = rs.fn().mockImplementation(() => ({ chat: { completions: { create: mockCreate, @@ -10,7 +10,7 @@ const mockOpenAIConstructor = vi.fn().mockImplementation(() => ({ }, })); -vi.mock('openai', () => ({ +rs.mock('openai', () => ({ default: mockOpenAIConstructor, })); @@ -30,13 +30,13 @@ describe('service-caller OpenAI error handling', () => { const originalFetch = globalThis.fetch; beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); globalThis.fetch = originalFetch; }); afterEach(() => { - vi.unmock('@/ai-model/service-caller/model-call-recorder'); - vi.resetModules(); + rs.unmock('@/ai-model/service-caller/model-call-recorder'); + rs.resetModules(); }); it('records non-2xx raw response body without changing the response', async () => { @@ -56,7 +56,7 @@ describe('service-caller OpenAI error handling', () => { 'x-request-id': 'req_123', }, }); - globalThis.fetch = vi.fn().mockResolvedValue(response); + globalThis.fetch = rs.fn().mockResolvedValue(response); const wrappedResponse = await wrapOpenAICompatibleFetch(context)( 'https://example.com/v1/chat/completions', @@ -82,7 +82,7 @@ describe('service-caller OpenAI error handling', () => { status: 200, headers: { 'content-type': 'application/json' }, }); - globalThis.fetch = vi.fn().mockResolvedValue(response); + globalThis.fetch = rs.fn().mockResolvedValue(response); await expect( wrapOpenAICompatibleFetch(context)('https://example.com'), @@ -98,7 +98,7 @@ describe('service-caller OpenAI error handling', () => { const context = { recordEvent: (event: Record) => events.push(event), }; - globalThis.fetch = vi.fn().mockResolvedValue(new Response(null)); + globalThis.fetch = rs.fn().mockResolvedValue(new Response(null)); await wrapOpenAICompatibleFetch(context)('https://example.com', { method: 'POST', @@ -123,7 +123,7 @@ describe('service-caller OpenAI error handling', () => { it('uses x-model-request-id as usage request_id when x-request-id is absent', async () => { const { callAI } = await import('@/ai-model/service-caller'); const { getModelRuntime } = await import('@/ai-model/models'); - globalThis.fetch = vi.fn().mockResolvedValue( + globalThis.fetch = rs.fn().mockResolvedValue( new Response(null, { headers: { 'x-model-request-id': 'model_req_123' }, }), @@ -154,7 +154,7 @@ describe('service-caller OpenAI error handling', () => { it('prefers x-request-id over x-model-request-id', async () => { const { callAI } = await import('@/ai-model/service-caller'); const { getModelRuntime } = await import('@/ai-model/models'); - globalThis.fetch = vi.fn().mockResolvedValue( + globalThis.fetch = rs.fn().mockResolvedValue( new Response(null, { headers: { 'x-request-id': 'req_123', @@ -190,7 +190,7 @@ describe('service-caller OpenAI error handling', () => { '@/ai-model/service-caller/openai-error' ); const context = {}; - globalThis.fetch = vi + globalThis.fetch = rs .fn() .mockResolvedValueOnce(new Response('first body', { status: 500 })) .mockRejectedValueOnce(new Error('network error')) @@ -232,7 +232,7 @@ describe('service-caller OpenAI error handling', () => { const fetchError = Object.assign(new TypeError('fetch failed'), { cause, }); - globalThis.fetch = vi.fn().mockRejectedValue(fetchError); + globalThis.fetch = rs.fn().mockRejectedValue(fetchError); await expect( wrapOpenAICompatibleFetch(context)('https://example.com'), @@ -256,7 +256,7 @@ describe('service-caller OpenAI error handling', () => { const { callAI } = await import('@/ai-model/service-caller'); const { getModelRuntime } = await import('@/ai-model/models'); const actualOpenAI = - await vi.importActual('openai'); + await rs.importActual('openai'); const rawResponseBody = JSON.stringify({ detail: 'model does not exist', trace_id: 'trace_123', @@ -273,7 +273,7 @@ describe('service-caller OpenAI error handling', () => { expect(bareOpenAIError.message).not.toContain('trace_123'); expect(bareOpenAIError.error).toBeUndefined(); - globalThis.fetch = vi.fn().mockResolvedValue( + globalThis.fetch = rs.fn().mockResolvedValue( new Response(rawResponseBody, { status: 422, headers: { @@ -306,8 +306,8 @@ describe('service-caller OpenAI error handling', () => { it('uses the successful retry attempt for the final record', async () => { const events: Array> = []; - vi.resetModules(); - vi.doMock('@/ai-model/service-caller/model-call-recorder', () => ({ + rs.resetModules(); + rs.doMock('@/ai-model/service-caller/model-call-recorder', () => ({ isModelCallRecordingEnabled: () => true, recordModelCallEvent: (event: Record) => { events.push(event); @@ -315,7 +315,7 @@ describe('service-caller OpenAI error handling', () => { })); const { callAI } = await import('@/ai-model/service-caller'); const { getModelRuntime } = await import('@/ai-model/models'); - globalThis.fetch = vi + globalThis.fetch = rs .fn() .mockResolvedValueOnce(new Response('temporary failure', { status: 500 })) .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }))); @@ -353,8 +353,8 @@ describe('service-caller OpenAI error handling', () => { it('records every streaming chunk with its sequence', async () => { const events: Array> = []; - vi.resetModules(); - vi.doMock('@/ai-model/service-caller/model-call-recorder', () => ({ + rs.resetModules(); + rs.doMock('@/ai-model/service-caller/model-call-recorder', () => ({ isModelCallRecordingEnabled: () => true, recordModelCallEvent: (event: Record) => { events.push(event); @@ -362,7 +362,7 @@ describe('service-caller OpenAI error handling', () => { })); const { callAI } = await import('@/ai-model/service-caller'); const { getModelRuntime } = await import('@/ai-model/models'); - globalThis.fetch = vi.fn().mockResolvedValue( + globalThis.fetch = rs.fn().mockResolvedValue( new Response(null, { headers: { 'content-type': 'text/event-stream' }, }), @@ -386,7 +386,7 @@ describe('service-caller OpenAI error handling', () => { await callAI( [{ role: 'user', content: 'hello' }], getModelRuntime(baseConfig()), - { stream: true, onChunk: vi.fn() }, + { stream: true, onChunk: rs.fn() }, ); expect( diff --git a/packages/core/tests/unit-test/service-caller-reasoning-fallback.test.ts b/packages/core/tests/unit-test/service-caller-reasoning-fallback.test.ts index c77de16614..de7eb72a8c 100644 --- a/packages/core/tests/unit-test/service-caller-reasoning-fallback.test.ts +++ b/packages/core/tests/unit-test/service-caller-reasoning-fallback.test.ts @@ -5,22 +5,22 @@ import { callAIWithObjectResponse, } from '@/ai-model/service-caller'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -const { mockDebugLog, mockWarnLog } = vi.hoisted(() => ({ - mockDebugLog: vi.fn(), - mockWarnLog: vi.fn(), +const { mockDebugLog, mockWarnLog } = rs.hoisted(() => ({ + mockDebugLog: rs.fn(), + mockWarnLog: rs.fn(), })); -const mockCreate = vi.fn(); +const mockCreate = rs.fn(); -vi.mock('@midscene/shared/logger', () => ({ - getDebug: vi.fn((_topic, options) => +rs.mock('@midscene/shared/logger', () => ({ + getDebug: rs.fn((_topic, options) => options?.console ? mockWarnLog : mockDebugLog, ), })); -vi.mock('openai', () => ({ - default: vi.fn().mockImplementation(() => ({ +rs.mock('openai', () => ({ + default: rs.fn().mockImplementation(() => ({ chat: { completions: { create: mockCreate, @@ -415,7 +415,7 @@ describe('service-caller reasoning fallback', () => { }); it('uses model retry settings for JSON parsing failures', async () => { - vi.useFakeTimers(); + rs.useFakeTimers(); mockCreate .mockResolvedValueOnce({ choices: [{ message: { content: 'not JSON' } }], @@ -435,16 +435,16 @@ describe('service-caller reasoning fallback', () => { { jsonParserSource: 'locate' }, ); - await vi.advanceTimersByTimeAsync(122); + await rs.advanceTimersByTimeAsync(122); expect(mockCreate).toHaveBeenCalledTimes(1); - await vi.advanceTimersByTimeAsync(1); + await rs.advanceTimersByTimeAsync(1); await expect(responsePromise).resolves.toMatchObject({ content: { bbox: [100, 200, 300, 400] }, }); expect(mockCreate).toHaveBeenCalledTimes(2); } finally { - vi.useRealTimers(); + rs.useRealTimers(); } }); diff --git a/packages/core/tests/unit-test/service-caller-timeout.test.ts b/packages/core/tests/unit-test/service-caller-timeout.test.ts index 4e8a7ad514..a3e84cca09 100644 --- a/packages/core/tests/unit-test/service-caller-timeout.test.ts +++ b/packages/core/tests/unit-test/service-caller-timeout.test.ts @@ -1,10 +1,10 @@ import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; -const mockCreate = vi.fn(); +const mockCreate = rs.fn(); -vi.mock('openai', () => ({ - default: vi.fn().mockImplementation(() => ({ +rs.mock('openai', () => ({ + default: rs.fn().mockImplementation(() => ({ chat: { completions: { create: mockCreate, @@ -27,7 +27,7 @@ const baseConfig = (overrides: Partial = {}): IModelConfig => describe('service-caller request timeout', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('resolves default, custom and disabled timeout values', async () => { @@ -188,7 +188,7 @@ describe('service-caller request timeout', () => { callAI( [{ role: 'user', content: 'hello' }], getModelRuntime(baseConfig({ timeout: 30 })), - { stream: true, onChunk: vi.fn() }, + { stream: true, onChunk: rs.fn() }, ), ).rejects.toThrow(/AI call hard timeout after 30ms/); }); @@ -214,7 +214,7 @@ describe('service-caller request timeout', () => { ); const OpenAI = (await import('openai')).default as unknown as ReturnType< - typeof vi.fn + typeof rs.fn >; const lastCallOptions = OpenAI.mock.calls.at(-1)?.[0]; expect(lastCallOptions?.timeout).toBe(180_000); @@ -250,7 +250,7 @@ describe('service-caller request timeout', () => { await callAI([{ role: 'user', content: 'hello' }], modelRuntime); const OpenAI = (await import('openai')).default as unknown as ReturnType< - typeof vi.fn + typeof rs.fn >; const defaultHeaders = OpenAI.mock.calls.at(-1)?.[0]?.defaultHeaders as | Record @@ -389,7 +389,7 @@ describe('service-caller request timeout', () => { ); const OpenAI = (await import('openai')).default as unknown as ReturnType< - typeof vi.fn + typeof rs.fn >; const lastCallOptions = OpenAI.mock.calls.at(-1)?.[0]; // When timeout is disabled we should NOT forward a timeout to the SDK. diff --git a/packages/core/tests/unit-test/service-caller/model-call-recorder.test.ts b/packages/core/tests/unit-test/service-caller/model-call-recorder.test.ts index 3094ec9351..9340bf92a9 100644 --- a/packages/core/tests/unit-test/service-caller/model-call-recorder.test.ts +++ b/packages/core/tests/unit-test/service-caller/model-call-recorder.test.ts @@ -3,16 +3,20 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { setMidsceneRunDir } from '@midscene/shared/common'; import { MIDSCENE_RECORD_MODEL_CALL } from '@midscene/shared/env/types'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; import { ModelCallRecorder } from '../../../src/ai-model/service-caller/model-call-recorder'; +import * as sharedCommonActual from '@midscene/shared/common' with { + rstest: 'importActual', +}; + const runDirs: string[] = []; afterEach(async () => { - vi.unstubAllEnvs(); - vi.unstubAllGlobals(); - vi.unmock('node:fs/promises'); - vi.unmock('@midscene/shared/common'); + rs.unstubAllEnvs(); + rs.unstubAllGlobals(); + rs.unmock('node:fs/promises'); + rs.unmock('@midscene/shared/common'); setMidsceneRunDir(undefined); await Promise.all( runDirs.splice(0).map((dir) => rm(dir, { recursive: true })), @@ -39,7 +43,7 @@ describe('model call recorder', () => { }); it('uses one JSONL file when its first events are concurrent', async () => { - vi.stubEnv(MIDSCENE_RECORD_MODEL_CALL, 'true'); + rs.stubEnv(MIDSCENE_RECORD_MODEL_CALL, 'true'); const runDir = await createRunDir(); const recorder = new ModelCallRecorder(); @@ -65,13 +69,13 @@ describe('model call recorder', () => { }); it.each([ - ['browser', () => vi.stubGlobal('window', {})], - ['worker', () => vi.stubGlobal('WorkerGlobalScope', class {})], + ['browser', () => rs.stubGlobal('window', {})], + ['worker', () => rs.stubGlobal('WorkerGlobalScope', class {})], ])('does not record in a %s runtime', async (_runtime, setupRuntime) => { - vi.stubEnv(MIDSCENE_RECORD_MODEL_CALL, 'true'); + rs.stubEnv(MIDSCENE_RECORD_MODEL_CALL, 'true'); const runDir = await createRunDir(); setupRuntime(); - vi.resetModules(); + rs.resetModules(); const { ModelCallRecorder: RuntimeRecorder } = await import( '../../../src/ai-model/service-caller/model-call-recorder' ); @@ -85,16 +89,16 @@ describe('model call recorder', () => { }); it('continues recording after a write failure', async () => { - vi.stubEnv(MIDSCENE_RECORD_MODEL_CALL, 'true'); - const appendFile = vi + rs.stubEnv(MIDSCENE_RECORD_MODEL_CALL, 'true'); + const appendFile = rs .fn() .mockRejectedValueOnce(new Error('disk is full')) .mockResolvedValueOnce(undefined); - const mkdir = vi.fn().mockResolvedValue(undefined); - vi.resetModules(); - vi.doMock('node:fs/promises', () => ({ appendFile, mkdir })); - vi.doMock('@midscene/shared/common', async (importOriginal) => ({ - ...(await importOriginal()), + const mkdir = rs.fn().mockResolvedValue(undefined); + rs.resetModules(); + rs.doMock('node:fs/promises', () => ({ appendFile, mkdir })); + rs.doMock('@midscene/shared/common', () => ({ + ...sharedCommonActual, getMidsceneRunBaseDir: () => '/tmp/midscene-model-record-test', })); const { ModelCallRecorder: RuntimeRecorder } = await import( diff --git a/packages/core/tests/unit-test/service-describe.test.ts b/packages/core/tests/unit-test/service-describe.test.ts index 176a1faafe..3b03da5b95 100644 --- a/packages/core/tests/unit-test/service-describe.test.ts +++ b/packages/core/tests/unit-test/service-describe.test.ts @@ -3,43 +3,43 @@ import { elementDescriberInstruction } from '@/ai-model/prompt/describe'; import { AIResponseParseError } from '@/ai-model/service-caller'; import Service from '@/service'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { createFakeContext } from '../utils'; -const { mockCallAIWithObjectResponse } = vi.hoisted(() => ({ - mockCallAIWithObjectResponse: vi.fn(), +import * as serviceCallerActual from '@/ai-model/service-caller' with { + rstest: 'importActual', +}; +import * as imgActual from '@midscene/shared/img' with { + rstest: 'importActual', +}; + +const { mockCallAIWithObjectResponse } = rs.hoisted(() => ({ + mockCallAIWithObjectResponse: rs.fn(), })); const { mockCompositeElementInfoImg, mockCompositePointMarkerImg, mockCropByRect, mockResizeBase64ImageToJpeg, -} = vi.hoisted(() => ({ - mockCompositeElementInfoImg: vi.fn(), - mockCompositePointMarkerImg: vi.fn(), - mockCropByRect: vi.fn(), - mockResizeBase64ImageToJpeg: vi.fn(), +} = rs.hoisted(() => ({ + mockCompositeElementInfoImg: rs.fn(), + mockCompositePointMarkerImg: rs.fn(), + mockCropByRect: rs.fn(), + mockResizeBase64ImageToJpeg: rs.fn(), })); -vi.mock('@/ai-model/service-caller', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - callAIWithObjectResponse: mockCallAIWithObjectResponse, - }; -}); +rs.mock('@/ai-model/service-caller', () => ({ + ...serviceCallerActual, + callAIWithObjectResponse: mockCallAIWithObjectResponse, +})); -vi.mock('@midscene/shared/img', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - compositeElementInfoImg: mockCompositeElementInfoImg, - compositePointMarkerImg: mockCompositePointMarkerImg, - cropByRect: mockCropByRect, - resizeBase64ImageToJpeg: mockResizeBase64ImageToJpeg, - }; -}); +rs.mock('@midscene/shared/img', () => ({ + ...imgActual, + compositeElementInfoImg: mockCompositeElementInfoImg, + compositePointMarkerImg: mockCompositePointMarkerImg, + cropByRect: mockCropByRect, + resizeBase64ImageToJpeg: mockResizeBase64ImageToJpeg, +})); describe('service.describe', () => { const modelConfig: IModelConfig = { diff --git a/packages/core/tests/unit-test/service-locate-deeplocate.test.ts b/packages/core/tests/unit-test/service-locate-deeplocate.test.ts index 4ae643f3f3..2da8d7fa9c 100644 --- a/packages/core/tests/unit-test/service-locate-deeplocate.test.ts +++ b/packages/core/tests/unit-test/service-locate-deeplocate.test.ts @@ -3,15 +3,15 @@ import { getModelRuntime } from '@/ai-model/models'; import Service from '@/service'; import { type AIUsageInfo, ServiceError } from '@/types'; import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { createFakeContext } from '../utils'; -vi.mock('@/ai-model/workflows/grounding', () => ({ +rs.mock('@/ai-model/workflows/grounding', () => ({ AIResponseParseError: class AIResponseParseError extends Error {}, - AiExtractElementInfo: vi.fn(), - AiLocateElement: vi.fn(), - AiLocateSection: vi.fn(), - buildSearchAreaConfig: vi.fn(), + AiExtractElementInfo: rs.fn(), + AiLocateElement: rs.fn(), + AiLocateSection: rs.fn(), + buildSearchAreaConfig: rs.fn(), })); import { @@ -31,9 +31,9 @@ describe('service.locate deepLocate routing', () => { const modelRuntime = getModelRuntime(modelConfig); beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); - vi.mocked(AiLocateElement).mockResolvedValue({ + rs.mocked(AiLocateElement).mockResolvedValue({ parseResult: { element: { center: [120, 220], @@ -50,7 +50,7 @@ describe('service.locate deepLocate routing', () => { reasoning_content: undefined, } as any); - vi.mocked(AiLocateSection).mockResolvedValue({ + rs.mocked(AiLocateSection).mockResolvedValue({ searchAreaConfig: { sourceRect: { left: 10, top: 20, width: 300, height: 200 }, image: { @@ -67,7 +67,7 @@ describe('service.locate deepLocate routing', () => { usage: undefined, }); - vi.mocked(buildSearchAreaConfig).mockResolvedValue({ + rs.mocked(buildSearchAreaConfig).mockResolvedValue({ sourceRect: { left: 20, top: 30, width: 280, height: 180 }, image: { imageBase64: 'data:image/png;base64,BBB', @@ -143,7 +143,7 @@ describe('service.locate deepLocate routing', () => { slot: undefined, request_id: undefined, }; - vi.mocked(AiLocateSection).mockResolvedValue({ + rs.mocked(AiLocateSection).mockResolvedValue({ searchAreaConfig: undefined, error: 'invalid bbox data', rawResponse: '{"bbox":["invalid bbox"]}', diff --git a/packages/core/tests/unit-test/service-utils.test.ts b/packages/core/tests/unit-test/service-utils.test.ts index 3ae2dae674..85e2605959 100644 --- a/packages/core/tests/unit-test/service-utils.test.ts +++ b/packages/core/tests/unit-test/service-utils.test.ts @@ -1,5 +1,5 @@ import { getDescribeDeepContextAreas, getRectInCrop } from '@/service/utils'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('service describe utils', () => { it('uses focused context for point targets on tall screenshots', () => { diff --git a/packages/core/tests/unit-test/shared/action-schema.test.ts b/packages/core/tests/unit-test/shared/action-schema.test.ts index 50d1636eca..d029eac260 100644 --- a/packages/core/tests/unit-test/shared/action-schema.test.ts +++ b/packages/core/tests/unit-test/shared/action-schema.test.ts @@ -1,5 +1,5 @@ import { getZodDefaultValue } from '@/ai-model/shared/action-schema'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; import { z } from 'zod'; describe('getZodDefaultValue', () => { diff --git a/packages/core/tests/unit-test/skill.test.ts b/packages/core/tests/unit-test/skill.test.ts index 4ba8cd79ba..33d9a7abc0 100644 --- a/packages/core/tests/unit-test/skill.test.ts +++ b/packages/core/tests/unit-test/skill.test.ts @@ -1,5 +1,5 @@ import { runSkillCLI } from '@/skill/index'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('runSkillCLI', () => { it('should be a function', () => { diff --git a/packages/core/tests/unit-test/standard-planning-parser.test.ts b/packages/core/tests/unit-test/standard-planning-parser.test.ts index 6b4e359d48..ccd9d0e51f 100644 --- a/packages/core/tests/unit-test/standard-planning-parser.test.ts +++ b/packages/core/tests/unit-test/standard-planning-parser.test.ts @@ -1,5 +1,5 @@ import { parseXMLPlanningResponse } from '@/ai-model/workflows/planning'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('parseXMLPlanningResponse', () => { it('extracts the continuous content between the protocol boundary tags', () => { diff --git a/packages/core/tests/unit-test/task-builder.test.ts b/packages/core/tests/unit-test/task-builder.test.ts index 6c6b5cd9a7..ee36f3e6cd 100644 --- a/packages/core/tests/unit-test/task-builder.test.ts +++ b/packages/core/tests/unit-test/task-builder.test.ts @@ -4,7 +4,7 @@ import { getModelRuntime } from '@/ai-model/models'; import { AbstractInterface, defineActionSleep } from '@/device'; import type Service from '@/service'; import type { DeviceAction, PlanningAction } from '@/types'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; import { z } from 'zod'; class MockInterface extends AbstractInterface { @@ -52,7 +52,7 @@ describe('TaskBuilder', () => { }); afterEach(() => { - vi.useRealTimers(); + rs.useRealTimers(); }); it('normalizes the deprecated locate deepThink alias before task reporting', () => { @@ -77,14 +77,14 @@ describe('TaskBuilder', () => { name: 'Tap', description: 'mock tap action', paramSchema: actionSchema, - call: vi.fn(), + call: rs.fn(), }; const mockInterface = new MockInterface([mockAction, defineActionSleep()]); const insightService = { - contextRetrieverFn: vi.fn(), - locate: vi.fn(), + contextRetrieverFn: rs.fn(), + locate: rs.fn(), } as unknown as Service; const taskBuilder = new TaskBuilder({ @@ -139,7 +139,7 @@ describe('TaskBuilder', () => { name: 'Tap', description: 'mock tap action', paramSchema: actionSchema, - call: vi.fn(), + call: rs.fn(), }; const mockInterface = new MockInterface([mockAction]); const locateDump = { @@ -154,8 +154,8 @@ describe('TaskBuilder', () => { ], }; const insightService = { - contextRetrieverFn: vi.fn(), - locate: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(), + locate: rs.fn(async () => ({ element: { center: [50, 50], rect: { left: 40, top: 40, width: 20, height: 20 }, @@ -205,8 +205,8 @@ describe('TaskBuilder', () => { it('throws when building an executable task for an action outside actionSpace', async () => { const mockInterface = new MockInterface([defineActionSleep()]); const insightService = { - contextRetrieverFn: vi.fn(), - locate: vi.fn(), + contextRetrieverFn: rs.fn(), + locate: rs.fn(), } as unknown as Service; const taskBuilder = new TaskBuilder({ interfaceInstance: mockInterface, @@ -226,11 +226,11 @@ describe('TaskBuilder', () => { }); it('supports fast-path action delays for system actions', async () => { - vi.useFakeTimers(); + rs.useFakeTimers(); - const defaultBeforeHook = vi.fn(async () => undefined); - const defaultAfterHook = vi.fn(async () => undefined); - const defaultActionCall = vi.fn(async () => undefined); + const defaultBeforeHook = rs.fn(async () => undefined); + const defaultAfterHook = rs.fn(async () => undefined); + const defaultActionCall = rs.fn(async () => undefined); const defaultAction: DeviceAction = { name: 'DefaultExit', description: 'default exit action', @@ -241,9 +241,9 @@ describe('TaskBuilder', () => { defaultInterface.beforeInvokeAction = defaultBeforeHook; defaultInterface.afterInvokeAction = defaultAfterHook; - const fastBeforeHook = vi.fn(async () => undefined); - const fastAfterHook = vi.fn(async () => undefined); - const fastActionCall = vi.fn(async () => undefined); + const fastBeforeHook = rs.fn(async () => undefined); + const fastAfterHook = rs.fn(async () => undefined); + const fastActionCall = rs.fn(async () => undefined); const fastAction: DeviceAction = { name: 'FastExit', description: 'fast exit action', @@ -257,8 +257,8 @@ describe('TaskBuilder', () => { fastInterface.afterInvokeAction = fastAfterHook; const insightService = { - contextRetrieverFn: vi.fn(), - locate: vi.fn(), + contextRetrieverFn: rs.fn(), + locate: rs.fn(), } as unknown as Service; const defaultTaskBuilder = new TaskBuilder({ @@ -293,19 +293,19 @@ describe('TaskBuilder', () => { const defaultPromise = defaultTask.executor(defaultTask.param, taskContext); - await vi.advanceTimersByTimeAsync(199); + await rs.advanceTimersByTimeAsync(199); expect(defaultBeforeHook).toHaveBeenCalledTimes(1); expect(defaultActionCall).not.toHaveBeenCalled(); expect(defaultAfterHook).not.toHaveBeenCalled(); - await vi.advanceTimersByTimeAsync(1); + await rs.advanceTimersByTimeAsync(1); expect(defaultActionCall).toHaveBeenCalledTimes(1); expect(defaultAfterHook).not.toHaveBeenCalled(); - await vi.advanceTimersByTimeAsync(299); + await rs.advanceTimersByTimeAsync(299); expect(defaultAfterHook).not.toHaveBeenCalled(); - await vi.advanceTimersByTimeAsync(1); + await rs.advanceTimersByTimeAsync(1); await expect(defaultPromise).resolves.toEqual({ output: undefined }); expect(defaultAfterHook).toHaveBeenCalledTimes(1); @@ -317,7 +317,7 @@ describe('TaskBuilder', () => { }); it('allows actions to attach planning feedback to the running task', async () => { - const actionCall = vi.fn(async () => '0\n'); + const actionCall = rs.fn(async () => '0\n'); const readStateAction: DeviceAction<{ key: string }, string> = { name: 'ReadState', description: 'read state', @@ -334,8 +334,8 @@ describe('TaskBuilder', () => { }; const mockInterface = new MockInterface([readStateAction]); const insightService = { - contextRetrieverFn: vi.fn(), - locate: vi.fn(), + contextRetrieverFn: rs.fn(), + locate: rs.fn(), } as unknown as Service; const taskBuilder = new TaskBuilder({ interfaceInstance: mockInterface, diff --git a/packages/core/tests/unit-test/task-cache-empty-flow.test.ts b/packages/core/tests/unit-test/task-cache-empty-flow.test.ts index 5b6887e409..a60604c3ba 100644 --- a/packages/core/tests/unit-test/task-cache-empty-flow.test.ts +++ b/packages/core/tests/unit-test/task-cache-empty-flow.test.ts @@ -1,6 +1,6 @@ import { TaskCache } from '@/agent'; import { uuid } from '@midscene/shared/utils'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; /** * Access internal cache state for testing diff --git a/packages/core/tests/unit-test/task-cache-poisoning.test.ts b/packages/core/tests/unit-test/task-cache-poisoning.test.ts index 4fcecdc7ef..9125abdb57 100644 --- a/packages/core/tests/unit-test/task-cache-poisoning.test.ts +++ b/packages/core/tests/unit-test/task-cache-poisoning.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { type LocateCache, TaskCache } from '@/agent'; import { uuid } from '@midscene/shared/utils'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; /** * Access internal cache state for testing diff --git a/packages/core/tests/unit-test/task-executor-concurrency.test.ts b/packages/core/tests/unit-test/task-executor-concurrency.test.ts index f669b71187..1a85850f36 100644 --- a/packages/core/tests/unit-test/task-executor-concurrency.test.ts +++ b/packages/core/tests/unit-test/task-executor-concurrency.test.ts @@ -1,13 +1,13 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -vi.mock('@/ai-model/workflows/planning', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - standardPlan: vi.fn(), - }; -}); +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; + +import * as planningActual from '@/ai-model/workflows/planning' with { + rstest: 'importActual', +}; + +rs.mock('@/ai-model/workflows/planning', () => ({ + ...planningActual, + standardPlan: rs.fn(), +})); import { TaskExecutor } from '@/agent/tasks'; import { getModelRuntime } from '@/ai-model/models'; @@ -61,11 +61,11 @@ describe('TaskExecutor concurrency isolation', () => { beforeEach(() => { mockInterface = { interfaceType: 'web', - actionSpace: vi.fn().mockReturnValue(emptyParamActionSpace), + actionSpace: rs.fn().mockReturnValue(emptyParamActionSpace), } as unknown as AbstractInterface; mockService = { - contextRetrieverFn: vi.fn().mockResolvedValue({ + contextRetrieverFn: rs.fn().mockResolvedValue({ screenshot: ScreenshotItem.create(validBase64Image, Date.now()), shotSize: { width: 1920, height: 1080 }, shrunkShotToLogicalRatio: 1, @@ -82,15 +82,15 @@ describe('TaskExecutor concurrency isolation', () => { actionSpace: emptyParamActionSpace, }); - vi.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ + rs.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ tasks: [], yamlFlow: [], } as any); }); afterEach(() => { - vi.restoreAllMocks(); - vi.useRealTimers(); + rs.restoreAllMocks(); + rs.useRealTimers(); }); it.each([ @@ -126,7 +126,7 @@ describe('TaskExecutor concurrency isolation', () => { expectedIncludeLocateInPlanning, expectedImagesIncludeCount, }) => { - vi.mocked(standardPlan).mockResolvedValue({ + rs.mocked(standardPlan).mockResolvedValue({ actions: [], yamlFlow: [], shouldContinuePlanning: false, @@ -164,7 +164,7 @@ describe('TaskExecutor concurrency isolation', () => { ); it('registers aiAct reference images when the execution is created', async () => { - vi.mocked(standardPlan).mockResolvedValue({ + rs.mocked(standardPlan).mockResolvedValue({ actions: [], yamlFlow: [], shouldContinuePlanning: false, @@ -194,7 +194,7 @@ describe('TaskExecutor concurrency isolation', () => { const seenHistories: any[] = []; - vi.mocked(standardPlan).mockImplementation( + rs.mocked(standardPlan).mockImplementation( async (_instruction, opts: any) => { seenHistories.push(opts.conversationHistory); if (seenHistories.length === 2) { @@ -265,7 +265,7 @@ describe('TaskExecutor concurrency isolation', () => { }, }); - vi.mocked(standardPlan).mockImplementation(async (instruction: any) => { + rs.mocked(standardPlan).mockImplementation(async (instruction: any) => { // Gate B's plan until A is executing inside its action batch, so the // two batches are guaranteed to overlap. if (instruction === 'B') { @@ -282,7 +282,7 @@ describe('TaskExecutor concurrency isolation', () => { } as any; }); - vi.spyOn(taskExecutorLocal, 'convertPlanToExecutable').mockImplementation( + rs.spyOn(taskExecutorLocal, 'convertPlanToExecutable').mockImplementation( (async (plans: any[]) => { const type = plans[0]?.type; if (type === 'TapA') { @@ -393,7 +393,7 @@ describe('TaskExecutor concurrency isolation', () => { }, }); - vi.mocked(standardPlan).mockResolvedValue({ + rs.mocked(standardPlan).mockResolvedValue({ actions: [ { type: 'Noop', @@ -407,7 +407,7 @@ describe('TaskExecutor concurrency isolation', () => { finalizeSuccess: true, finalizeMessage: 'Noop done.', }); - vi.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ + rs.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ tasks: [ { type: 'Action Space', @@ -435,7 +435,7 @@ describe('TaskExecutor concurrency isolation', () => { it('should use device-local formatted time for replanning feedback', async () => { const seenPendingFeedback: string[] = []; - mockInterface.getDeviceLocalTimeString = vi + mockInterface.getDeviceLocalTimeString = rs .fn() .mockResolvedValue('2023-10-15 15:37:00 (YYYY-MM-DD HH:mm:ss)'); taskExecutor = new TaskExecutor(mockInterface, mockService, { @@ -443,12 +443,12 @@ describe('TaskExecutor concurrency isolation', () => { actionSpace: emptyParamActionSpace, useDeviceTime: true, }); - vi.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ + rs.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ tasks: [], yamlFlow: [], } as any); - vi.mocked(standardPlan) + rs.mocked(standardPlan) .mockImplementationOnce(async (_instruction, opts: any) => { seenPendingFeedback.push( opts.conversationHistory.pendingFeedbackMessage, @@ -496,7 +496,7 @@ Command: settings get system screen_brightness Stdout: 0`; - vi.spyOn(taskExecutor, 'convertPlanToExecutable') + rs.spyOn(taskExecutor, 'convertPlanToExecutable') .mockResolvedValueOnce({ tasks: [ { @@ -518,7 +518,7 @@ Stdout: yamlFlow: [], } as any); - vi.mocked(standardPlan) + rs.mocked(standardPlan) .mockImplementationOnce(async (_instruction, opts: any) => { seenPendingFeedback.push( opts.conversationHistory.pendingFeedbackMessage, @@ -568,7 +568,7 @@ Stdout: const seenPendingFeedback: string[] = []; const longFeedback = 'x'.repeat(600); - vi.spyOn(taskExecutor, 'convertPlanToExecutable') + rs.spyOn(taskExecutor, 'convertPlanToExecutable') .mockResolvedValueOnce({ tasks: [ { @@ -590,7 +590,7 @@ Stdout: yamlFlow: [], } as any); - vi.mocked(standardPlan) + rs.mocked(standardPlan) .mockImplementationOnce(async (_instruction, opts: any) => { seenPendingFeedback.push( opts.conversationHistory.pendingFeedbackMessage, @@ -641,7 +641,7 @@ Stdout: it('should collect all planning feedback instead of the final task output', async () => { const seenPendingFeedback: string[] = []; - vi.setSystemTime(new Date(2023, 9, 15, 8, 30, 0)); + rs.setSystemTime(new Date(2023, 9, 15, 8, 30, 0)); const firstPlanningFeedback = `RunAdbShell returned stdout. The stdout may indicate success or failure. Command: settings get system screen_brightness Stdout: @@ -656,7 +656,7 @@ Stdout: mCurrentFocus=Window{abc}`; const finalActionOutput = 'tap-output'; - vi.spyOn(taskExecutor, 'convertPlanToExecutable') + rs.spyOn(taskExecutor, 'convertPlanToExecutable') .mockResolvedValueOnce({ tasks: [ { @@ -708,7 +708,7 @@ mCurrentFocus=Window{abc}`; yamlFlow: [], } as any); - vi.mocked(standardPlan) + rs.mocked(standardPlan) .mockImplementationOnce(async (_instruction, opts: any) => { seenPendingFeedback.push( opts.conversationHistory.pendingFeedbackMessage, @@ -781,7 +781,7 @@ ${thirdPlanningFeedback}`); replanningCycleLimit: 1, actionSpace: [], }); - vi.spyOn(taskExecutor, 'convertPlanToExecutable') + rs.spyOn(taskExecutor, 'convertPlanToExecutable') .mockResolvedValueOnce({ tasks: [ { @@ -803,7 +803,7 @@ ${thirdPlanningFeedback}`); yamlFlow: [], } as any); - vi.mocked(standardPlan) + rs.mocked(standardPlan) .mockImplementationOnce(async (_instruction, opts: any) => { seenPendingFeedback.push( opts.conversationHistory.pendingFeedbackMessage, @@ -849,8 +849,8 @@ ${thirdPlanningFeedback}`); }); it('should fall back to runtime time instead of device timestamp when device-local time is unavailable', async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date(2023, 9, 15, 8, 30, 0)); + rs.useFakeTimers(); + rs.setSystemTime(new Date(2023, 9, 15, 8, 30, 0)); const seenPendingFeedback: string[] = []; taskExecutor = new TaskExecutor(mockInterface, mockService, { @@ -858,12 +858,12 @@ ${thirdPlanningFeedback}`); actionSpace: emptyParamActionSpace, useDeviceTime: true, }); - vi.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ + rs.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ tasks: [], yamlFlow: [], } as any); - vi.mocked(standardPlan) + rs.mocked(standardPlan) .mockImplementationOnce(async (_instruction, opts: any) => { seenPendingFeedback.push( opts.conversationHistory.pendingFeedbackMessage, diff --git a/packages/core/tests/unit-test/task-executor-custom-planning.test.ts b/packages/core/tests/unit-test/task-executor-custom-planning.test.ts index 9e4d2396b6..0c58ab91c9 100644 --- a/packages/core/tests/unit-test/task-executor-custom-planning.test.ts +++ b/packages/core/tests/unit-test/task-executor-custom-planning.test.ts @@ -1,6 +1,6 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; -const serviceCallerMock = vi.hoisted(() => { +const serviceCallerMock = rs.hoisted(() => { class AIResponseParseError extends Error { rawResponse?: string; usage?: unknown; @@ -22,15 +22,15 @@ const serviceCallerMock = vi.hoisted(() => { return { AIResponseParseError, - callAIWithStringResponse: vi.fn(), + callAIWithStringResponse: rs.fn(), }; }); -vi.mock('@/ai-model/service-caller/index', () => { +rs.mock('@/ai-model/service-caller/index', () => { return serviceCallerMock; }); -vi.mock('../../src/ai-model/service-caller/index', () => { +rs.mock('../../src/ai-model/service-caller/index', () => { return serviceCallerMock; }); @@ -95,11 +95,11 @@ describe('TaskExecutor custom planning adapters', () => { beforeEach(() => { mockInterface = { interfaceType: 'web', - actionSpace: vi.fn(), + actionSpace: rs.fn(), } as unknown as AbstractInterface; mockService = { - contextRetrieverFn: vi.fn().mockResolvedValue({ + contextRetrieverFn: rs.fn().mockResolvedValue({ screenshot: ScreenshotItem.create(validBase64Image, Date.now()), shotSize: { width: 1920, height: 1080 }, shrunkShotToLogicalRatio: 1, @@ -113,7 +113,7 @@ describe('TaskExecutor custom planning adapters', () => { }); afterEach(() => { - vi.restoreAllMocks(); + rs.restoreAllMocks(); }); it('passes normalized deepLocate through custom planning adapters', async () => { @@ -127,12 +127,12 @@ describe('TaskExecutor custom planning adapters', () => { call: async () => undefined, }, ]; - mockInterface.actionSpace = vi.fn().mockReturnValue(actionSpace); + mockInterface.actionSpace = rs.fn().mockReturnValue(actionSpace); taskExecutor = new TaskExecutor(mockInterface, mockService, { replanningCycleLimit: 1, actionSpace, }); - vi.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ + rs.spyOn(taskExecutor, 'convertPlanToExecutable').mockResolvedValue({ tasks: [], yamlFlow: [], } as any); @@ -150,8 +150,8 @@ describe('TaskExecutor custom planning adapters', () => { }, ]; const customPlanningModel = createCustomPlanningModel(plannedActions); - vi.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: '' }); - const convertSpy = vi.mocked(taskExecutor.convertPlanToExecutable); + rs.mocked(callAIWithStringResponse).mockResolvedValueOnce({ content: '' }); + const convertSpy = rs.mocked(taskExecutor.convertPlanToExecutable); await taskExecutor.action( 'prompt', customPlanningModel, diff --git a/packages/core/tests/unit-test/task-runner-log-time.test.ts b/packages/core/tests/unit-test/task-runner-log-time.test.ts index 05f42c5ae7..1b1d72805f 100644 --- a/packages/core/tests/unit-test/task-runner-log-time.test.ts +++ b/packages/core/tests/unit-test/task-runner-log-time.test.ts @@ -1,7 +1,7 @@ import { ScreenshotItem } from '@/screenshot-item'; import { TaskRunner } from '@/task-runner'; import type { UIContext } from '@/types'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const fakeUIContextBuilder = async () => { const screenshot = ScreenshotItem.create('', Date.now()); diff --git a/packages/core/tests/unit-test/task-runner/index.test.ts b/packages/core/tests/unit-test/task-runner/index.test.ts index 02b21c71fb..a0e4591671 100644 --- a/packages/core/tests/unit-test/task-runner/index.test.ts +++ b/packages/core/tests/unit-test/task-runner/index.test.ts @@ -10,13 +10,12 @@ import type { } from '@/index'; import Service from '@/service'; import { TaskExecutionError } from '@/task-runner'; -import { processError } from '@vitest/runner'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { createFakeContext } from '../../utils'; // Mock AI service caller -vi.mock('@/ai-model/service-caller/index', () => ({ - callAI: vi.fn(), +rs.mock('@/ai-model/service-caller/index', () => ({ + callAI: rs.fn(), AIResponseParseError: class AIResponseParseError extends Error {}, })); @@ -83,7 +82,7 @@ describe( () => { beforeEach(() => { // Setup default mock implementation for AI calls - vi.mocked(callAI).mockResolvedValue({ + rs.mocked(callAI).mockResolvedValue({ content: JSON.stringify({ bbox: [0, 0, 100, 100], errors: [], @@ -99,7 +98,7 @@ describe( action: 'tap', anything: 'acceptable', }; - const tapperFn = vi.fn(); + const tapperFn = rs.fn(); const actionTask: ExecutionTaskActionApply = { type: 'Action Space', param: taskParam, @@ -147,7 +146,7 @@ describe( it('insight - init and append', async () => { const initRunner = new TaskRunner('test', fakeUIContextBuilder); expect(initRunner.status).toBe('init'); - const tapperFn = vi.fn(); + const tapperFn = rs.fn(); const insightTask1 = insightFindTask(); const actionTask: ExecutionTaskActionApply = { @@ -244,7 +243,7 @@ describe( await expect(runner.flush()).rejects.toThrowError(); expect(runner.status).toBe('error'); - const recoveryExecutor = vi.fn().mockResolvedValue({ + const recoveryExecutor = rs.fn().mockResolvedValue({ output: 'recovered', }); const recoveryTask: ExecutionTaskApply< @@ -269,8 +268,8 @@ describe( }); it('reuses UI context before an action and invalidates it after the action settles', async () => { - const now = vi.spyOn(Date, 'now').mockReturnValue(1_000); - const uiContextBuilder = vi.fn(fakeUIContextBuilder); + const now = rs.spyOn(Date, 'now').mockReturnValue(1_000); + const uiContextBuilder = rs.fn(fakeUIContextBuilder); const tasks: ExecutionTaskApply[] = [ { type: 'Planning', @@ -307,8 +306,8 @@ describe( }); it('invalidates UI context when an action throws', async () => { - const now = vi.spyOn(Date, 'now').mockReturnValue(1_000); - const uiContextBuilder = vi.fn(fakeUIContextBuilder); + const now = rs.spyOn(Date, 'now').mockReturnValue(1_000); + const uiContextBuilder = rs.fn(fakeUIContextBuilder); const failedAction: ExecutionTaskActionApply = { type: 'Action Space', executor: async () => { @@ -460,7 +459,7 @@ describe( errorMessage: 'upstream failed', }); - const serializedError = processError(caughtError); + const serializedError = caughtError!.toJSON(); expect(serializedError).toMatchObject({ name: 'TaskExecutionError', code: 'TASK_EXECUTION_FAILED', @@ -515,7 +514,7 @@ describe( expect(caughtError).toBeInstanceOf(TaskExecutionError); expect(caughtError?.cause.stack).toContain('failInExecutor'); - const serializedError = processError(caughtError); + const serializedError = caughtError!.toJSON(); expect(serializedError.stack).toContain('TaskExecutionError'); expect(serializedError.cause).toMatchObject({ name: 'Error', @@ -562,7 +561,7 @@ describe( 'Error without a message', ); - const serializedError = processError(caughtError); + const serializedError = caughtError!.toJSON(); expect(serializedError).toMatchObject({ name: 'TaskExecutionError', code: 'TASK_EXECUTION_FAILED', diff --git a/packages/core/tests/unit-test/task-service-dump.test.ts b/packages/core/tests/unit-test/task-service-dump.test.ts index 259d547c17..54155f4c80 100644 --- a/packages/core/tests/unit-test/task-service-dump.test.ts +++ b/packages/core/tests/unit-test/task-service-dump.test.ts @@ -1,6 +1,6 @@ import { getTaskSearchArea, getTaskServiceDump } from '@/dump'; import type { ExecutionTask, ServiceDump } from '@/types'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; const serviceDump = { type: 'locate', diff --git a/packages/core/tests/unit-test/task-status.test.ts b/packages/core/tests/unit-test/task-status.test.ts index 968f27224b..26508d7351 100644 --- a/packages/core/tests/unit-test/task-status.test.ts +++ b/packages/core/tests/unit-test/task-status.test.ts @@ -1,5 +1,5 @@ import { deriveCaseStatus, deriveTaskStatus } from '@/dump/task-status'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('deriveTaskStatus', () => { it('treats a thrown / failed task as failed', () => { diff --git a/packages/core/tests/unit-test/tasks-null-data.test.ts b/packages/core/tests/unit-test/tasks-null-data.test.ts index 3da5906ebb..66ccc82efc 100644 --- a/packages/core/tests/unit-test/tasks-null-data.test.ts +++ b/packages/core/tests/unit-test/tasks-null-data.test.ts @@ -4,16 +4,16 @@ import { standardPlan } from '@/ai-model/workflows/planning'; import { ScreenshotItem } from '@/screenshot-item'; import type { AIUsageInfo, ServiceDump } from '@/types'; import type { IModelConfig } from '@midscene/shared/env'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; -vi.mock('@/ai-model/workflows/planning', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - standardPlan: vi.fn(), - }; -}); +import * as planningActual from '@/ai-model/workflows/planning' with { + rstest: 'importActual', +}; + +rs.mock('@/ai-model/workflows/planning', () => ({ + ...planningActual, + standardPlan: rs.fn(), +})); // Helper function to create mock UIContext with ScreenshotItem const createMockUIContext = async (screenshotData = 'mock-screenshot') => { @@ -83,8 +83,8 @@ const createMockDump = ( describe('TaskExecutor - Null Data Handling', () => { it('registers insight reference images when the execution is created', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: { answer: 'matched' }, thought: 'matched the reference', dump: createMockDump({ answer: 'matched' }), @@ -119,8 +119,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should handle null data for WaitFor operation', async () => { // Mock service that returns null const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: null, // AI returns null usage: { totalTokens: 100 }, thought: 'Could not determine if condition is true', @@ -166,8 +166,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should handle undefined data for WaitFor operation', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: undefined, // AI returns undefined usage: { totalTokens: 100 }, thought: 'Failed to evaluate condition', @@ -207,8 +207,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should handle null data for Assert operation', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: null, usage: { totalTokens: 100 }, thought: 'Could not verify assertion', @@ -274,8 +274,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should handle valid data for WaitFor operation', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: { StatementIsTruthy: true, }, @@ -346,8 +346,8 @@ describe('TaskExecutor - Null Data Handling', () => { } as ServiceDump; const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: { Boolean: true, }, @@ -393,7 +393,7 @@ describe('TaskExecutor - Null Data Handling', () => { }); it('should preserve planning intent while recording resolved config slot', async () => { - const planSpy = vi.mocked(standardPlan).mockResolvedValue({ + const planSpy = rs.mocked(standardPlan).mockResolvedValue({ actions: [], usage: { prompt_tokens: 20, @@ -408,7 +408,7 @@ describe('TaskExecutor - Null Data Handling', () => { } as any); const mockService = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), onceDumpUpdatedFn: undefined, } as any; @@ -452,7 +452,7 @@ describe('TaskExecutor - Null Data Handling', () => { }); it('should preserve existing intent and warn instead of overwriting it', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const warnSpy = rs.spyOn(console, 'warn').mockImplementation(() => {}); const dump = { ...createMockDump({ Boolean: true }, 'Condition is met'), @@ -471,8 +471,8 @@ describe('TaskExecutor - Null Data Handling', () => { } as ServiceDump; const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: { Boolean: true, }, @@ -520,8 +520,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should handle string data for WaitFor operation', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: 'true', // AI returns plain string instead of structured format usage: { totalTokens: 100 }, thought: 'Condition is met', @@ -561,8 +561,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should handle null data for Query operation', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: null, usage: { totalTokens: 100 }, thought: 'No result found', @@ -600,8 +600,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should handle null data for String type query', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: null, usage: { totalTokens: 100 }, thought: 'Could not extract string', @@ -640,8 +640,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should extract Number type query result from the structured Number field', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: { Number: 42, }, @@ -697,8 +697,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should preserve report fields on Insight task params', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: { Number: 42, }, @@ -743,8 +743,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should handle null data for Number type query', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: null, usage: { totalTokens: 100 }, thought: 'Could not extract number', @@ -795,8 +795,8 @@ describe('TaskExecutor - Null Data Handling', () => { it('should prepend current screenshot guidance for Boolean type query', async () => { const mockInsight = { - contextRetrieverFn: vi.fn(async () => await createMockUIContext()), - extract: vi.fn(async () => ({ + contextRetrieverFn: rs.fn(async () => await createMockUIContext()), + extract: rs.fn(async () => ({ data: { Boolean: true, }, diff --git a/packages/core/tests/unit-test/ui-observer.test.ts b/packages/core/tests/unit-test/ui-observer.test.ts index 0a20017399..329df8d7bc 100644 --- a/packages/core/tests/unit-test/ui-observer.test.ts +++ b/packages/core/tests/unit-test/ui-observer.test.ts @@ -8,7 +8,7 @@ import type { UIContext } from '@/types'; import { UIObservationRecordWriter } from '@midscene/shared/agent-tools/observation-record'; import type { UIObservationRecord } from '@midscene/shared/agent-tools/types'; import { imageInfoOfBase64 } from '@midscene/shared/img'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const createdDirectories: string[] = []; @@ -40,10 +40,10 @@ const fakeRepresentative = (): UIContext => const makeFakeSource = () => { let current: DeviceFrameRef | null = null; - const decode = vi.fn(async (refs: DeviceFrameRef[]) => + const decode = rs.fn(async (refs: DeviceFrameRef[]) => refs.map(() => testPngDataUrl), ); - const stop = vi.fn(); + const stop = rs.fn(); const source: DeviceFrameSource = { latest: () => current, decode, @@ -60,20 +60,20 @@ const makeFakeSource = () => { }; const makeDeps = (fake: ReturnType | null) => { - const screenshot = vi.fn(async () => testPngDataUrl); - const onStopped = vi.fn(); + const screenshot = rs.fn(async () => testPngDataUrl); + const onStopped = rs.fn(); return { deps: { openFrameSource: async () => fake?.source ?? undefined, captureRawScreenshot: screenshot, capturePreparedRepresentative: async () => fakeRepresentative(), createInsight: () => ({ - aiQuery: vi.fn(), - aiBoolean: vi.fn(), - aiNumber: vi.fn(), - aiString: vi.fn(), - aiAsk: vi.fn(), - aiAssert: vi.fn(), + aiQuery: rs.fn(), + aiBoolean: rs.fn(), + aiNumber: rs.fn(), + aiString: rs.fn(), + aiAsk: rs.fn(), + aiAssert: rs.fn(), }), onStopped, observationRecordWriter: recordWriter(), @@ -107,7 +107,7 @@ function fixedRecord(): UIObservationRecord { describe('UIObserver', () => { afterEach(() => { - vi.useRealTimers(); + rs.useRealTimers(); for (const directory of createdDirectories.splice(0)) { rmSync(directory, { recursive: true, force: true }); } @@ -318,14 +318,14 @@ describe('UIObserver', () => { }); it('rejects live DOM options before delegating an observation insight', async () => { - const aiBoolean = vi.fn(); + const aiBoolean = rs.fn(); const observation = new UIObservationImpl(fixedRecord(), { - aiQuery: vi.fn(), + aiQuery: rs.fn(), aiBoolean, - aiNumber: vi.fn(), - aiString: vi.fn(), - aiAsk: vi.fn(), - aiAssert: vi.fn(), + aiNumber: rs.fn(), + aiString: rs.fn(), + aiAsk: rs.fn(), + aiAssert: rs.fn(), }); await expect( @@ -337,22 +337,22 @@ describe('UIObserver', () => { }); it('keeps failed observation cleanup retryable', async () => { - const disposeRecord = vi + const disposeRecord = rs .fn() .mockImplementationOnce(() => { throw new Error('directory is busy'); }) .mockImplementationOnce(() => undefined); - const onDisposed = vi.fn(); + const onDisposed = rs.fn(); const observation = new UIObservationImpl( fixedRecord(), { - aiQuery: vi.fn(), - aiBoolean: vi.fn(), - aiNumber: vi.fn(), - aiString: vi.fn(), - aiAsk: vi.fn(), - aiAssert: vi.fn(), + aiQuery: rs.fn(), + aiBoolean: rs.fn(), + aiNumber: rs.fn(), + aiString: rs.fn(), + aiAsk: rs.fn(), + aiAssert: rs.fn(), }, disposeRecord, onDisposed, @@ -366,7 +366,7 @@ describe('UIObserver', () => { }); it('watchdog auto-stops and can also be disabled', async () => { - vi.useFakeTimers(); + rs.useFakeTimers(); const fake = makeFakeSource(); fake.setLatest('f0', 0); const first = makeDeps(fake); @@ -375,8 +375,8 @@ describe('UIObserver', () => { options({ intervalMs: 200, watchdogMs: 5000 }), ); await observer.start(); - vi.advanceTimersByTime(5000); - await vi.runAllTimersAsync(); + rs.advanceTimersByTime(5000); + await rs.runAllTimersAsync(); await observer.stop(); expect(first.onStopped).toHaveBeenCalledOnce(); @@ -388,7 +388,7 @@ describe('UIObserver', () => { options({ intervalMs: 200, watchdogMs: 0 }), ); await disabled.start(); - vi.advanceTimersByTime(60000); + rs.advanceTimersByTime(60000); await Promise.resolve(); expect(second.onStopped).not.toHaveBeenCalled(); await disabled.stop(); @@ -456,7 +456,7 @@ describe('UIObserver', () => { }); it('falls back when opening the frame source throws', async () => { - const screenshot = vi.fn(async () => testPngDataUrl); + const screenshot = rs.fn(async () => testPngDataUrl); const observer = new UIObserverImpl( { openFrameSource: async () => { @@ -465,12 +465,12 @@ describe('UIObserver', () => { captureRawScreenshot: screenshot, capturePreparedRepresentative: async () => fakeRepresentative(), createInsight: () => ({ - aiQuery: vi.fn(), - aiBoolean: vi.fn(), - aiNumber: vi.fn(), - aiString: vi.fn(), - aiAsk: vi.fn(), - aiAssert: vi.fn(), + aiQuery: rs.fn(), + aiBoolean: rs.fn(), + aiNumber: rs.fn(), + aiString: rs.fn(), + aiAsk: rs.fn(), + aiAssert: rs.fn(), }), observationRecordWriter: recordWriter(), }, diff --git a/packages/core/tests/unit-test/utils.test.ts b/packages/core/tests/unit-test/utils.test.ts index 94eedebcf6..9d855ebb7d 100644 --- a/packages/core/tests/unit-test/utils.test.ts +++ b/packages/core/tests/unit-test/utils.test.ts @@ -5,7 +5,7 @@ import { dumpActionParam, findAllMidsceneLocatorField } from '@/common'; import { getMidsceneLocationSchema } from '@/index'; import { getMidsceneRunSubDir } from '@midscene/shared/common'; import { uuid } from '@midscene/shared/utils'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; import { z } from 'zod'; import { ifPlanLocateParamHasLocatedPixelBbox, @@ -28,18 +28,19 @@ import { } from '../../src/yaml/utils'; import { getGroupedDumpScriptIds } from './test-helpers/report-html'; -const { readFileSyncMock } = vi.hoisted(() => ({ - readFileSyncMock: vi.fn(), +import * as fsActual from 'node:fs' with { rstest: 'importActual' }; + +const { readFileSyncMock } = rs.hoisted(() => ({ + readFileSyncMock: rs.fn(), })); -vi.mock('node:fs', async (importOriginal) => { - const actual = await importOriginal(); - readFileSyncMock.mockImplementation(actual.readFileSync); +rs.mock('node:fs', () => { + readFileSyncMock.mockImplementation(fsActual.readFileSync); return { - ...actual, + ...fsActual, default: { - ...actual, + ...fsActual, readFileSync: readFileSyncMock, }, readFileSync: readFileSyncMock, diff --git a/packages/core/tests/unit-test/vl-model-check.test.ts b/packages/core/tests/unit-test/vl-model-check.test.ts index bb8eb3da80..8d4b85ce35 100644 --- a/packages/core/tests/unit-test/vl-model-check.test.ts +++ b/packages/core/tests/unit-test/vl-model-check.test.ts @@ -1,29 +1,28 @@ import { Agent } from '@/agent/agent'; import type { AbstractInterface } from '@/device'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; + +import * as coreActual from '@midscene/core' with { rstest: 'importActual' }; // Mock dependencies -vi.mock('@midscene/core/utils', () => ({ - writeLogFile: vi.fn(() => null), - reportHTMLContent: vi.fn(() => ''), - stringifyDumpData: vi.fn(() => '{}'), +rs.mock('@midscene/core/utils', () => ({ + writeLogFile: rs.fn(() => null), + reportHTMLContent: rs.fn(() => ''), + stringifyDumpData: rs.fn(() => '{}'), groupedActionDumpFileExt: '.json', getVersion: () => '0.0.0-test', - sleep: vi.fn(() => Promise.resolve()), + sleep: rs.fn(() => Promise.resolve()), })); -vi.mock('@midscene/shared/logger', () => ({ - getDebug: vi.fn(() => vi.fn()), - logMsg: vi.fn(), +rs.mock('@midscene/shared/logger', () => ({ + getDebug: rs.fn(() => rs.fn()), + logMsg: rs.fn(), })); -vi.mock('@midscene/core', async () => { - const actual = await vi.importActual('@midscene/core'); - return { - ...actual, - Insight: vi.fn().mockImplementation(() => ({})), - }; -}); +rs.mock('@midscene/core', () => ({ + ...coreActual, + Insight: rs.fn().mockImplementation(() => ({})), +})); const mockedModelConfig = { MIDSCENE_MODEL_NAME: 'gpt-4o', @@ -36,9 +35,9 @@ const createMockInterface = ( ) => ({ interfaceType, - destroy: vi.fn(), - size: vi.fn().mockResolvedValue({}), - actionSpace: vi.fn(() => []), + destroy: rs.fn(), + size: rs.fn().mockResolvedValue({}), + actionSpace: rs.fn(() => []), }) as unknown as AbstractInterface; describe('VL Model Check for Different Interface Types', () => { diff --git a/packages/core/tests/unit-test/workflows/planning/planning-action-log.test.ts b/packages/core/tests/unit-test/workflows/planning/planning-action-log.test.ts index 0ced4c477e..2ee6eac2d9 100644 --- a/packages/core/tests/unit-test/workflows/planning/planning-action-log.test.ts +++ b/packages/core/tests/unit-test/workflows/planning/planning-action-log.test.ts @@ -5,7 +5,7 @@ import { actionScrollParamSchema, } from '@/device'; import type { DeviceAction, PlanningAction } from '@/types'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, rs } from '@rstest/core'; const actionDefinition = ( name: string, @@ -13,7 +13,7 @@ const actionDefinition = ( ): DeviceAction => ({ name, paramSchema, - call: vi.fn(), + call: rs.fn(), }); describe('buildPlanningActionLog', () => { diff --git a/packages/core/tests/unit-test/workflows/recorder-generation/markdown.test.ts b/packages/core/tests/unit-test/workflows/recorder-generation/markdown.test.ts index 0a6d687f6f..7b4f4870ab 100644 --- a/packages/core/tests/unit-test/workflows/recorder-generation/markdown.test.ts +++ b/packages/core/tests/unit-test/workflows/recorder-generation/markdown.test.ts @@ -1,7 +1,7 @@ import type { IModelConfig } from '@midscene/shared/env'; import { imageInfoOfBase64 } from '@midscene/shared/img'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import sharp from 'sharp'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; import { callAIWithStringResponse } from '../../../../src/ai-model/service-caller'; import type { ChromeRecordedEvent } from '../../../../src/ai-model/workflows/recorder-generation/common'; import { @@ -9,24 +9,24 @@ import { generateRecorderMarkdownReplay, } from '../../../../src/ai-model/workflows/recorder-generation/markdown'; -const { mockDebugMarkdownReplay } = vi.hoisted(() => ({ - mockDebugMarkdownReplay: vi.fn(), +import * as loggerActual from '@midscene/shared/logger' with { + rstest: 'importActual', +}; + +const { mockDebugMarkdownReplay } = rs.hoisted(() => ({ + mockDebugMarkdownReplay: rs.fn(), })); -vi.mock('@midscene/shared/logger', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - getDebug: vi.fn(() => mockDebugMarkdownReplay), - }; -}); +rs.mock('@midscene/shared/logger', () => ({ + ...loggerActual, + getDebug: rs.fn(() => mockDebugMarkdownReplay), +})); -vi.mock('../../../../src/ai-model/service-caller', () => ({ - callAIWithStringResponse: vi.fn(), +rs.mock('../../../../src/ai-model/service-caller', () => ({ + callAIWithStringResponse: rs.fn(), })); -const mockCallAIWithStringResponse = vi.mocked(callAIWithStringResponse); +const mockCallAIWithStringResponse = rs.mocked(callAIWithStringResponse); const mockedModelConfig = { modelName: 'mock', @@ -64,7 +64,7 @@ const mockEvents: ChromeRecordedEvent[] = [ describe('markdown-generator', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('compresses oversized screenshots to JPEG before model generation', async () => { diff --git a/packages/core/tests/unit-test/workflows/recorder-generation/metadata.test.ts b/packages/core/tests/unit-test/workflows/recorder-generation/metadata.test.ts index a2fd1bcd9e..88c21913c1 100644 --- a/packages/core/tests/unit-test/workflows/recorder-generation/metadata.test.ts +++ b/packages/core/tests/unit-test/workflows/recorder-generation/metadata.test.ts @@ -1,13 +1,13 @@ import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { callAIWithObjectResponse } from '../../../../src/ai-model/service-caller'; import { generateRecorderSessionMetadata } from '../../../../src/ai-model/workflows/recorder-generation/metadata'; -vi.mock('../../../../src/ai-model/service-caller', () => ({ - callAIWithObjectResponse: vi.fn(), +rs.mock('../../../../src/ai-model/service-caller', () => ({ + callAIWithObjectResponse: rs.fn(), })); -const mockCallAIWithObjectResponse = vi.mocked(callAIWithObjectResponse); +const mockCallAIWithObjectResponse = rs.mocked(callAIWithObjectResponse); const mockedModelConfig = { modelName: 'mock', @@ -18,7 +18,7 @@ const mockedModelConfig = { describe('recorder-metadata-generator', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); mockCallAIWithObjectResponse.mockResolvedValue({ content: { title: 'Example Recording', diff --git a/packages/core/tests/unit-test/workflows/recorder-generation/playwright.test.ts b/packages/core/tests/unit-test/workflows/recorder-generation/playwright.test.ts index e36a3f8e68..61a676e0f3 100644 --- a/packages/core/tests/unit-test/workflows/recorder-generation/playwright.test.ts +++ b/packages/core/tests/unit-test/workflows/recorder-generation/playwright.test.ts @@ -1,5 +1,5 @@ import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { beforeEach, describe, expect, rs, test } from '@rstest/core'; import { callAIWithStringResponse } from '../../../../src/ai-model/service-caller'; import { type ChromeRecordedEvent, @@ -16,15 +16,15 @@ import { } from '../../../../src/ai-model/workflows/recorder-generation/playwright'; // Mock the callAi function -vi.mock('../../../../src/ai-model/service-caller', () => ({ - callAIWithStringResponse: vi.fn(), +rs.mock('../../../../src/ai-model/service-caller', () => ({ + callAIWithStringResponse: rs.fn(), })); -const mockCallAiWithStringResponse = vi.mocked(callAIWithStringResponse); +const mockCallAiWithStringResponse = rs.mocked(callAIWithStringResponse); describe('playwright-generator', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); const mockEvents: ChromeRecordedEvent[] = [ diff --git a/packages/core/tests/unit-test/workflows/recorder-generation/yaml.test.ts b/packages/core/tests/unit-test/workflows/recorder-generation/yaml.test.ts index 253cfba2af..28da623029 100644 --- a/packages/core/tests/unit-test/workflows/recorder-generation/yaml.test.ts +++ b/packages/core/tests/unit-test/workflows/recorder-generation/yaml.test.ts @@ -1,5 +1,5 @@ import type { IModelConfig } from '@midscene/shared/env'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, rs } from '@rstest/core'; import { callAI, callAIWithStringResponse, @@ -12,13 +12,13 @@ import { generateYamlTestStream, } from '../../../../src/ai-model/workflows/recorder-generation/yaml'; -vi.mock('../../../../src/ai-model/service-caller', () => ({ - callAI: vi.fn(), - callAIWithStringResponse: vi.fn(), +rs.mock('../../../../src/ai-model/service-caller', () => ({ + callAI: rs.fn(), + callAIWithStringResponse: rs.fn(), })); -const mockCallAI = vi.mocked(callAI); -const mockCallAIWithStringResponse = vi.mocked(callAIWithStringResponse); +const mockCallAI = rs.mocked(callAI); +const mockCallAIWithStringResponse = rs.mocked(callAIWithStringResponse); const mockEvents: ChromeRecordedEvent[] = [ { @@ -51,7 +51,7 @@ const mockedModelConfig = { describe('yaml-generator', () => { beforeEach(() => { - vi.clearAllMocks(); + rs.clearAllMocks(); }); it('adds a language instruction when generating YAML', async () => { @@ -76,7 +76,7 @@ describe('yaml-generator', () => { }); it('uses the same language instruction for streaming YAML generation', async () => { - const onChunk = vi.fn(); + const onChunk = rs.fn(); mockCallAI.mockResolvedValue({ content: 'yaml-content', usage: undefined, @@ -206,7 +206,7 @@ describe('yaml-generator', () => { }); it('preserves platform-aware prompt for streaming recorder YAML generation', async () => { - const onChunk = vi.fn(); + const onChunk = rs.fn(); mockCallAI.mockResolvedValue({ content: 'android:\n deviceId: "emulator-5554"\n', usage: undefined, diff --git a/packages/core/tests/unit-test/xml-parser.test.ts b/packages/core/tests/unit-test/xml-parser.test.ts index 26dc696440..30e61514bb 100644 --- a/packages/core/tests/unit-test/xml-parser.test.ts +++ b/packages/core/tests/unit-test/xml-parser.test.ts @@ -1,5 +1,5 @@ import { extractXMLTag } from '@/ai-model/shared/xml'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from '@rstest/core'; describe('extractXMLTag', () => { it('should extract simple tag content', () => { diff --git a/packages/core/tests/unit-test/yaml-cleanup.test.ts b/packages/core/tests/unit-test/yaml-cleanup.test.ts index d21253b3ca..55bd972afc 100644 --- a/packages/core/tests/unit-test/yaml-cleanup.test.ts +++ b/packages/core/tests/unit-test/yaml-cleanup.test.ts @@ -1,7 +1,7 @@ import type { Agent } from '@/agent/agent'; import { runFreeFnCleanup } from '@/yaml/cleanup'; import { ScriptPlayer } from '@/yaml/player'; -import { describe, expect, test, vi } from 'vitest'; +import { describe, expect, rstest as rs, test } from '@rstest/core'; describe('YAML resource cleanup', () => { test('runs cleanup functions in declared order and attempts all of them', async () => { @@ -41,7 +41,7 @@ describe('YAML resource cleanup', () => { test('ScriptPlayer rejects when resource cleanup fails', async () => { const cleanupError = new Error('cleanup failed'); const agent = { - getActionSpace: vi.fn().mockResolvedValue([]), + getActionSpace: rs.fn().mockResolvedValue([]), } as unknown as Agent; const player = new ScriptPlayer({ tasks: [] }, async () => ({ agent, diff --git a/packages/core/tests/unit-test/yaml-doc-usage.test.ts b/packages/core/tests/unit-test/yaml-doc-usage.test.ts index 6bc4af5763..3b2bb5054e 100644 --- a/packages/core/tests/unit-test/yaml-doc-usage.test.ts +++ b/packages/core/tests/unit-test/yaml-doc-usage.test.ts @@ -1,39 +1,39 @@ import { Agent } from '@/agent'; import { ScriptPlayer } from '@/yaml/player'; import { interpolateEnvVars, parseYamlScript } from '@/yaml/utils'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, rs } from '@rstest/core'; const createDocAgent = (overrides: Record = {}) => { const agent = { reportFile: '/tmp/doc-report.html', dump: { executions: [] }, onTaskStartTip: undefined, - aiAct: vi.fn(async () => undefined), - aiTap: vi.fn(async () => undefined), - aiScroll: vi.fn(async () => undefined), - aiQuery: vi.fn(async () => ({ id: 'SKU-123', title: 'doc item' })), - aiNumber: vi.fn(async () => 42), - aiString: vi.fn(async () => 'SKU-123'), - aiBoolean: vi.fn(async () => true), - aiAsk: vi.fn(async () => 'answer'), - aiLocate: vi.fn(async () => ({ + aiAct: rs.fn(async () => undefined), + aiTap: rs.fn(async () => undefined), + aiScroll: rs.fn(async () => undefined), + aiQuery: rs.fn(async () => ({ id: 'SKU-123', title: 'doc item' })), + aiNumber: rs.fn(async () => 42), + aiString: rs.fn(async () => 'SKU-123'), + aiBoolean: rs.fn(async () => true), + aiAsk: rs.fn(async () => 'answer'), + aiLocate: rs.fn(async () => ({ rect: { x: 1, y: 2, width: 3, height: 4 }, })), - aiWaitFor: vi.fn(async () => undefined), - aiAssert: vi.fn(async () => ({ + aiWaitFor: rs.fn(async () => undefined), + aiAssert: rs.fn(async () => ({ pass: true, thought: 'ok', message: 'passed', })), - runGherkinScenario: vi.fn(async () => ({ + runGherkinScenario: rs.fn(async () => ({ steps: [], })), - evaluateJavaScript: vi.fn(async () => 'js-result'), - recordToReport: vi.fn(async () => undefined), - recordErrorToReport: vi.fn(async () => undefined), - runAdbShell: vi.fn(async () => 'adb-result'), - callActionInActionSpace: vi.fn(async () => 'action-result'), - getActionSpace: vi.fn(async () => [ + evaluateJavaScript: rs.fn(async () => 'js-result'), + recordToReport: rs.fn(async () => undefined), + recordErrorToReport: rs.fn(async () => undefined), + runAdbShell: rs.fn(async () => 'adb-result'), + callActionInActionSpace: rs.fn(async () => 'action-result'), + getActionSpace: rs.fn(async () => [ { name: 'Hover', interfaceAlias: 'aiHover' }, { name: 'DoubleClick', interfaceAlias: 'aiDoubleClick' }, { name: 'RightClick', interfaceAlias: 'aiRightClick' }, @@ -42,7 +42,7 @@ const createDocAgent = (overrides: Record = {}) => { { name: 'RunAdbShell', interfaceAlias: 'runAdbShell' }, { name: 'RunWdaRequest', interfaceAlias: 'runWdaRequest' }, ]), - _unstableLogContent: vi.fn(() => ({ logs: [] })), + _unstableLogContent: rs.fn(() => ({ logs: [] })), ...overrides, }; @@ -51,7 +51,7 @@ const createDocAgent = (overrides: Record = {}) => { describe('YAML docs usage coverage', () => { afterEach(() => { - vi.restoreAllMocks(); + rs.restoreAllMocks(); Reflect.deleteProperty(process.env, 'DOC_ENABLED'); Reflect.deleteProperty(process.env, 'DOC_HOST'); Reflect.deleteProperty(process.env, 'DOC_TOPIC'); @@ -464,7 +464,7 @@ tasks: name: title `); const agent = createDocAgent({ - aiAssert: vi.fn(async () => ({ + aiAssert: rs.fn(async () => ({ pass: false, thought: 'failed', message: 'doc failure', @@ -495,7 +495,7 @@ tasks: name: gate `); const agent = createDocAgent({ - evaluateJavaScript: vi.fn(async () => { + evaluateJavaScript: rs.fn(async () => { throw error; }), }); @@ -530,7 +530,7 @@ tasks: name: gate `); const agent = createDocAgent({ - aiAct: vi.fn(async () => { + aiAct: rs.fn(async () => { agent.dump.executions.push({ id: 'recovered-agent-action', logTime: Date.now(), @@ -546,7 +546,7 @@ tasks: ], }); }), - evaluateJavaScript: vi.fn(async () => { + evaluateJavaScript: rs.fn(async () => { throw error; }), }); @@ -582,7 +582,7 @@ tasks: - aiAct: Click the broken button `); const agent = createDocAgent({ - aiAct: vi.fn(async () => { + aiAct: rs.fn(async () => { agent.dump.executions.push({ id: 'failed-agent-action', logTime: Date.now(), @@ -760,8 +760,8 @@ tasks: }); const agent = createDocAgent({ - aiString: vi.fn(async () => 'RUNTIME-123'), - aiQuery: vi.fn(async () => 'search-result'), + aiString: rs.fn(async () => 'RUNTIME-123'), + aiQuery: rs.fn(async () => 'search-result'), }); const player = new ScriptPlayer( script, diff --git a/packages/core/tests/unit-test/yaml-player-output.test.ts b/packages/core/tests/unit-test/yaml-player-output.test.ts index 370f490537..25adf8fd05 100644 --- a/packages/core/tests/unit-test/yaml-player-output.test.ts +++ b/packages/core/tests/unit-test/yaml-player-output.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { ScriptPlayer } from '@/yaml/player'; import { parseYamlScript } from '@/yaml/utils'; -import { describe, expect, test, vi } from 'vitest'; +import { describe, expect, rs, test } from '@rstest/core'; describe('YAML player output', () => { test.each([ @@ -12,7 +12,7 @@ describe('YAML player output', () => { ])( 'resolves configured output paths for the %s environment without launching an agent', (_label, environmentKey, fileName) => { - const setupAgent = vi.fn(); + const setupAgent = rs.fn(); const relativeOutput = `./midscene_run/output/${fileName}`; const script = parseYamlScript(` ${environmentKey}: @@ -37,8 +37,8 @@ tasks: [] message: 'Expected assertion failure', }; const agent = { - aiAssert: vi.fn().mockResolvedValue(assertionResult), - getActionSpace: vi.fn().mockResolvedValue([]), + aiAssert: rs.fn().mockResolvedValue(assertionResult), + getActionSpace: rs.fn().mockResolvedValue([]), onTaskStartTip: undefined, reportFile: null, }; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index d7b4dfc5e8..5b5fa93ead 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../shared/tsconfig.base.json", "compilerOptions": { + "allowJs": false, "sourceMap": true, "paths": { "@/*": ["./src/*"] diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts deleted file mode 100644 index 004f68b8e6..0000000000 --- a/packages/core/vitest.config.ts +++ /dev/null @@ -1,46 +0,0 @@ -import path from 'node:path'; -import dotenv from 'dotenv'; -import { defineConfig } from 'vitest/config'; -import { createCoverageConfig } from '../../scripts/vitest-coverage'; -import { version } from './package.json'; - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -dotenv.config({ - path: path.join(__dirname, '../../.env'), - override: true, - debug: true, -}); - -const enableAiTest = Boolean(process.env.AITEST); -const basicTest = ['tests/unit-test/**/*.test.ts']; - -export default defineConfig({ - test: { - coverage: createCoverageConfig(__dirname), - include: enableAiTest ? ['tests/ai/**/**.test.ts'] : basicTest, - retry: process.env.CI ? 1 : 0, - // Keep CI model request concurrency comparable to the former 4-core hosted - // runner. Set here (CI-only) instead of as CLI flags in ai-unit-test.yml - // because rstest rejects bare --minWorkers/--maxWorkers. Local AI runs - // keep the default worker count. - ...(enableAiTest && process.env.CI ? { minWorkers: 1, maxWorkers: 4 } : {}), - }, - define: { - __VERSION__: `'${version}'`, - __MIDSCENE_REPORT_BUILD__: 'false', - __DEV_REPORT_PATH__: JSON.stringify( - path.resolve(__dirname, '../../apps/report/dist/index.html'), - ), - }, - resolve: { - alias: { - '@': path.resolve(__dirname, 'src'), - }, - }, - ssr: { - external: ['@silvia-odwyer/photon'], - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 492f7dc682..ae0fd49a87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1042,6 +1042,9 @@ importers: '@rslib/core': specifier: ^0.18.3 version: 0.18.3(@microsoft/api-extractor@7.52.10(@types/node@18.19.62))(typescript@5.8.3) + '@rstest/core': + specifier: 0.11.5 + version: 0.11.5(core-js@3.47.0)(jsdom@29.0.2) '@types/js-yaml': specifier: 4.0.9 version: 4.0.9 @@ -1054,9 +1057,6 @@ importers: '@types/semver': specifier: 7.7.0 version: 7.7.0 - '@vitest/runner': - specifier: 3.0.5 - version: 3.0.5 langsmith: specifier: ^0.3.74 version: 0.3.74(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.210.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(openai@6.3.0(ws@8.20.0)(zod@3.25.76)) @@ -1066,9 +1066,6 @@ importers: typescript: specifier: ^5.8.3 version: 5.8.3 - vitest: - specifier: 3.0.5 - version: 3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) packages/harmony: dependencies: @@ -15454,14 +15451,6 @@ snapshots: optionalDependencies: vite: 5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - '@vitest/mocker@3.0.5(vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1))': - dependencies: - '@vitest/spy': 3.0.5 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - vite: 5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - '@vitest/pretty-format@3.0.5': dependencies: tinyrainbow: 2.0.0 @@ -23559,24 +23548,6 @@ snapshots: - supports-color - terser - vite-node@3.0.5(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): - dependencies: - cac: 6.7.14 - debug: 4.4.0 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - vite@5.4.10(@types/node@18.19.130)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): dependencies: esbuild: 0.21.5 @@ -23590,19 +23561,6 @@ snapshots: sass-embedded: 1.86.3 terser: 5.46.1 - vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.24.3 - optionalDependencies: - '@types/node': 18.19.62 - fsevents: 2.3.3 - less: 4.3.0 - lightningcss: 1.30.1 - sass-embedded: 1.86.3 - terser: 5.46.1 - vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.130)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): dependencies: '@vitest/expect': 3.0.5 @@ -23640,43 +23598,6 @@ snapshots: - supports-color - terser - vitest@3.0.5(@types/debug@4.1.12)(@types/node@18.19.62)(jsdom@29.0.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1): - dependencies: - '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1)) - '@vitest/pretty-format': 3.1.1 - '@vitest/runner': 3.0.5 - '@vitest/snapshot': 3.0.5 - '@vitest/spy': 3.0.5 - '@vitest/utils': 3.0.5 - chai: 5.2.0 - debug: 4.4.0 - expect-type: 1.2.1 - magic-string: 0.30.17 - pathe: 2.0.3 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 5.4.10(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - vite-node: 3.0.5(@types/node@18.19.62)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.86.3)(terser@5.46.1) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 18.19.62 - jsdom: 29.0.2 - transitivePeerDependencies: - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - vm-browserify@1.1.2: {} w-json@1.3.10: {}