yuyutaotao b261ed7f2a
feat(web): use xpath and yaml as cache (#711)
* feat(web-integration): use xpath for cache instead of id

* feat(web-integration): enhance TaskCache to support xpaths for cache matching and add new test cases

* feat(web-integration): add debug log for unknown page types in TaskCache

* feat(web-integration): update caching logic and cache hit conditions for Plan and Locate tasks

* chore(core): update debug log

* feat(web-integration): update rspress.config and enhance TaskCache structure with new properties

* feat(web-integration): recalculate id when hit cache

* fix(web-integration): update mock implementation in task-cache test to use evaluate method

* feat(web-integration): enhance element caching by adding XPath support and improving cache hit logic

* chore(core): lint

* feat(web-integration): improve XPath handling in web-extractor

* test(web-integration): fix tests

* feat(core, web-integration): add attributes to LocateResultElement and enhance element handling

* fix(core): lint

* feat(web-integration): add midsceneVersion to TaskCache and update cache validation logic

* fix(core): test

* fix(web-integration): update cache validation logic to prevent reading outdated midscene cache files

* feat(web-integration): enhance TaskCache to track used cache items and improve cache retrieval logic

* fix(core): xpath logic (#710)

* feat(core): resue context for locate

* feat(core): build yamlFlow from aiAction

* feat(core): refine task-cache

* feat(core): update cache

* feat(core): refine task-cache

* feat(core): refine task-cache

* feat(core): remove unused checkElementExistsByXPath

* feat(core): use yaml file as cache

* chore(core): fix lint

* chore(core): print warning for previous cache

* refactor(core): remove quickAnswer references and improve element matching logic

* fix(core): update import path for buildYamlFlowFromPlans

* chore(web-integration): update output image and skip task error test

* fix(web-integration): update test snapshots to handle beta versions

* fix(web-integration): adjust test snapshots for version consistency

* fix(web-integration): track original cache length and adjust matching logic in tests

* fix(web-integration): update test URLs to reflect new target site and enable previously skipped test

* chore(core): update cache docs

* fix(core): test

* feat(core): try to match element from plan

* fix(web-integration): cache id stable when retry in palywright

* fix(web-integration): typo

* style(web-integration): lint

* fix(web-integration): stable cacheid in tests

* fix(web-integration): cache id

---------

Co-authored-by: quanruzhuoxiu <quanruzhuoxiu@gmail.com>
2025-05-16 17:16:56 +08:00

229 lines
6.6 KiB
TypeScript

import { existsSync, readFileSync } from 'node:fs';
import {
type LocateCache,
type PlanningCache,
TaskCache,
} from '@/common/task-cache';
import { uuid } from '@midscene/shared/utils';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
vi.mock('../../package.json', () => {
return {
version: '0.16.11',
};
});
const prepareCache = (
caches: (PlanningCache | LocateCache)[],
cacheId?: string,
) => {
const cache = new TaskCache(cacheId ?? uuid(), true);
caches.map((data: PlanningCache | LocateCache) => {
cache.appendCache(data);
});
return cache.cacheFilePath;
};
describe(
'TaskCache',
() => {
beforeAll(() => {
vi.resetModules();
});
afterAll(() => {
vi.restoreAllMocks();
});
it('should create cache file', () => {
const cacheId = uuid();
const cache = new TaskCache(cacheId, true);
expect(cache.cacheFilePath).toBeDefined();
cache.appendCache({
type: 'plan',
prompt: 'test',
yamlWorkflow: 'test',
});
expect(existsSync(cache.cacheFilePath!)).toBe(true);
const cacheContent = readFileSync(cache.cacheFilePath!, 'utf-8').replace(
cacheId,
'cacheId',
);
expect(cacheContent).toMatchSnapshot();
expect(cache.isCacheResultUsed).toBe(true);
});
it('update or append cache record - should not match cache added in same run', () => {
const cacheId = uuid();
const cache = new TaskCache(cacheId, true);
cache.appendCache({
type: 'plan',
prompt: 'test-prompt',
yamlWorkflow: 'test-yaml-workflow',
});
const existingRecord = cache.matchPlanCache('test-prompt');
expect(existingRecord).toBeUndefined();
cache.updateOrAppendCacheRecord(
{
type: 'plan',
prompt: 'test-prompt',
yamlWorkflow: 'test-yaml-workflow-2',
},
existingRecord,
);
expect(cache.cache.caches.length).toBe(2);
expect(cache.cache.caches).toMatchSnapshot();
});
it('one cache record can only be matched once - when loaded from file', () => {
const cacheFilePath = prepareCache([
{
type: 'plan',
prompt: 'test-prompt',
yamlWorkflow: 'test-yaml-workflow',
},
]);
const newCache = new TaskCache(uuid(), true, cacheFilePath);
// should be able to match cache record
expect(newCache.matchPlanCache('test-prompt')).toBeDefined();
// should return undefined when matching the same record again
expect(newCache.matchPlanCache('test-prompt')).toBeUndefined();
});
it('same prompt with same type cache record can be matched twice - when loaded from file', () => {
const cacheFilePath = prepareCache([
{
type: 'plan',
prompt: 'test-prompt',
yamlWorkflow: 'test-yaml-workflow-1',
},
{
type: 'plan',
prompt: 'test-prompt',
yamlWorkflow: 'test-yaml-workflow-2',
},
]);
const newCache = new TaskCache(uuid(), true, cacheFilePath);
// should be able to match the first record
const firstMatch = newCache.matchPlanCache('test-prompt');
expect(firstMatch).toBeDefined();
expect(firstMatch?.cacheContent.yamlWorkflow).toBe(
'test-yaml-workflow-1',
);
// should be able to match the second record
const secondMatch = newCache.matchPlanCache('test-prompt');
expect(secondMatch).toBeDefined();
expect(secondMatch?.cacheContent.yamlWorkflow).toBe(
'test-yaml-workflow-2',
);
// should return undefined when matching the same record again
expect(newCache.matchPlanCache('test-prompt')).toBeUndefined();
});
it('should not match cache records added in the same run', () => {
const cacheId = uuid();
const cache = new TaskCache(cacheId, true);
// cache is empty, cacheOriginalLength should be 0
expect(cache.cacheOriginalLength).toBe(0);
// add a cache record
cache.appendCache({
type: 'plan',
prompt: 'test-prompt-1',
yamlWorkflow: 'test-yaml-workflow-1',
});
// add another cache record
cache.appendCache({
type: 'plan',
prompt: 'test-prompt-2',
yamlWorkflow: 'test-yaml-workflow-2',
});
// cache has two records
expect(cache.cache.caches.length).toBe(2);
// cacheOriginalLength should be 0
expect(cache.cacheOriginalLength).toBe(0);
// should not be able to match any record
expect(cache.matchPlanCache('test-prompt-1')).toBeUndefined();
expect(cache.matchPlanCache('test-prompt-2')).toBeUndefined();
});
it('save and retrieve cache from file', () => {
const cacheId = uuid();
const planningCachedPrompt = 'test';
const planningCachedYamlWorkflow = 'test-yaml-workflow';
const locateCachedPrompt = 'test-locate';
const locateCachedXpaths = ['test-xpath-1', 'test-xpath-2'];
const cacheFilePath = prepareCache(
[
{
type: 'plan',
prompt: planningCachedPrompt,
yamlWorkflow: planningCachedYamlWorkflow,
},
{
type: 'locate',
prompt: locateCachedPrompt,
xpaths: locateCachedXpaths,
},
],
cacheId,
);
const newTaskCache = new TaskCache(cacheId, true, cacheFilePath);
// should be able to match all cache records
const cachedPlanCache = newTaskCache.matchPlanCache(planningCachedPrompt);
const { cacheContent: cachedPlanCacheContent } = cachedPlanCache!;
expect(cachedPlanCacheContent.prompt).toBe(planningCachedPrompt);
expect(cachedPlanCacheContent.yamlWorkflow).toBe(
planningCachedYamlWorkflow,
);
const cachedLocateCache =
newTaskCache.matchLocateCache(locateCachedPrompt);
const {
cacheContent: cachedLocateCacheContent,
updateFn: cachedLocateCacheUpdateFn,
} = cachedLocateCache!;
expect(cachedLocateCacheContent.prompt).toBe(locateCachedPrompt);
expect(cachedLocateCacheContent.xpaths).toEqual(locateCachedXpaths);
expect(newTaskCache.cache.caches).toMatchSnapshot();
// test update cache
cachedLocateCacheUpdateFn((cache) => {
cache.xpaths = ['test-xpath-3', 'test-xpath-4'];
});
expect(newTaskCache.cache.caches).toMatchSnapshot();
const cacheFileContent = readFileSync(
newTaskCache.cacheFilePath!,
'utf-8',
).replace(newTaskCache.cacheId, 'cacheId');
expect(cacheFileContent).toMatchSnapshot();
});
},
{ timeout: 20000 },
);