Skip to content

Commit

Permalink
Merge pull request #43 from wix-incubator/add-disable-view-hierarchy-…
Browse files Browse the repository at this point in the history
…cache-flag

feat: add disable view hierarchy cache option.
  • Loading branch information
asafkorem authored Jan 18, 2025
2 parents cf40546 + 0a1bd91 commit 0912725
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 22 deletions.
3 changes: 2 additions & 1 deletion src/Copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export class Copilot {
this.codeEvaluator,
this.snapshotManager,
config.promptHandler,
this.cacheHandler
this.cacheHandler,
config.options?.cacheMode
);

}
Expand Down
68 changes: 67 additions & 1 deletion src/actions/StepPerformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {PromptCreator} from '@/utils/PromptCreator';
import {CodeEvaluator} from '@/utils/CodeEvaluator';
import {SnapshotManager} from '@/utils/SnapshotManager';
import {CacheHandler} from '@/utils/CacheHandler';
import {PromptHandler, TestingFrameworkAPICatalog} from '@/types';
import {CacheMode, PromptHandler, TestingFrameworkAPICatalog} from '@/types';
import * as crypto from 'crypto';
import {dummyContext, dummyBarContext1, dummyBarContext2} from "../test-utils/APICatalogTestUtils";

Expand All @@ -26,9 +26,13 @@ describe('StepPerformer', () => {
let mockSnapshotManager: jest.Mocked<SnapshotManager>;
let mockPromptHandler: jest.Mocked<PromptHandler>;
let mockCacheHandler: jest.Mocked<CacheHandler>;
let uuidCounter = 0;

beforeEach(() => {
jest.resetAllMocks();
uuidCounter = 0;

(crypto.randomUUID as jest.Mock).mockImplementation(() => `uuid-${uuidCounter++}`);

const apiCatalog: TestingFrameworkAPICatalog = {
context: {},
Expand Down Expand Up @@ -318,4 +322,66 @@ describe('StepPerformer', () => {

});
});

describe('cache modes', () => {
const testCacheModes = async (cacheMode: CacheMode) => {
const generatedKeys: string[] = [];
mockCacheHandler.addToTemporaryCache.mockImplementation((key: string) => {
generatedKeys.push(key);
});

stepPerformer = new StepPerformer(
mockContext,
mockPromptCreator,
mockCodeEvaluator,
mockSnapshotManager,
mockPromptHandler,
mockCacheHandler,
cacheMode
);

setupMocks({
promptResult: '```\nconst code = true;\n```',
codeEvaluationResult: 'success'
});
await stepPerformer.perform(INTENT);
return generatedKeys[0];
};

it('should include view hierarchy hash in cache key when mode is full', async () => {
const cacheKey = await testCacheModes('full');
const parsedKey = JSON.parse(cacheKey);
expect(parsedKey).toHaveProperty('viewHierarchyHash');
expect(parsedKey.viewHierarchyHash).toBe('hash');
});

it('should not include view hierarchy hash in cache key when mode is lightweight', async () => {
const cacheKey = await testCacheModes('lightweight');
const parsedKey = JSON.parse(cacheKey);
expect(parsedKey).not.toHaveProperty('viewHierarchyHash');
});

it('should generate unique cache keys when mode is disabled', async () => {
const firstKey = await testCacheModes('disabled');
const secondKey = await testCacheModes('disabled');
expect(firstKey).not.toBe(secondKey);
});

it('should not use cache when mode is disabled', async () => {
stepPerformer = new StepPerformer(
mockContext,
mockPromptCreator,
mockCodeEvaluator,
mockSnapshotManager,
mockPromptHandler,
mockCacheHandler,
'disabled'
);

setupMocks({ cacheExists: true });
await stepPerformer.perform(INTENT);

expect(mockPromptHandler.runPrompt).toHaveBeenCalled();
});
});
});
21 changes: 18 additions & 3 deletions src/actions/StepPerformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@ import {PromptCreator} from '@/utils/PromptCreator';
import {CodeEvaluator} from '@/utils/CodeEvaluator';
import {SnapshotManager} from '@/utils/SnapshotManager';
import {CacheHandler} from '@/utils/CacheHandler';
import {CodeEvaluationResult, PreviousStep, PromptHandler} from '@/types';
import {CacheMode, CodeEvaluationResult, PreviousStep, PromptHandler} from '@/types';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import {extractCodeBlock} from '@/utils/extractCodeBlock';

export class StepPerformer {
private readonly cacheMode: CacheMode;

constructor(
private context: any,
private promptCreator: PromptCreator,
private codeEvaluator: CodeEvaluator,
private snapshotManager: SnapshotManager,
private promptHandler: PromptHandler,
private cacheHandler: CacheHandler,
cacheMode: CacheMode = 'full',
) {
this.cacheMode = cacheMode;
}

extendJSContext(newContext: any): void {
Expand All @@ -30,8 +34,19 @@ export class StepPerformer {
}

private generateCacheKey(step: string, previous: PreviousStep[], viewHierarchy: string): string {
const viewHierarchyHash = crypto.createHash('md5').update(viewHierarchy).digest('hex');
return JSON.stringify({step, previous, viewHierarchyHash});
if (this.cacheMode === 'disabled') {
// Return a unique key that won't match any cached value
return crypto.randomUUID();
}

const cacheKeyData: any = {step, previous};

if (this.cacheMode === 'full') {
const viewHierarchyHash = crypto.createHash('md5').update(viewHierarchy).digest('hex');
cacheKeyData.viewHierarchyHash = viewHierarchyHash;
}

return JSON.stringify(cacheKeyData);
}

private async captureSnapshotAndViewHierarchy() {
Expand Down
59 changes: 59 additions & 0 deletions src/integration tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,63 @@ describe('Copilot Integration Tests', () => {
expect(spyStepPerformer).toHaveBeenCalledTimes(1);
});
});

describe('Cache Modes', () => {
beforeEach(() => {
mockPromptHandler.runPrompt.mockResolvedValue('// No operation');
});

it('should use full cache mode by default', async () => {
copilot.init({
frameworkDriver: mockFrameworkDriver,
promptHandler: mockPromptHandler
});
copilot.start();

await copilot.perform('Tap on the login button');
copilot.end();

expect(Object.keys(mockedCacheFile || {})[0]).toContain('viewHierarchyHash');
});

it('should not include view hierarchy in cache key when using lightweight mode', async () => {
copilot.init({
frameworkDriver: mockFrameworkDriver,
promptHandler: mockPromptHandler,
options: {
cacheMode: 'lightweight'
}
});
copilot.start();

await copilot.perform('Tap on the login button');
copilot.end();

const cacheKeys = Object.keys(mockedCacheFile || {});
expect(cacheKeys[0]).not.toContain('viewHierarchyHash');
});

it('should not use cache when cache mode is disabled', async () => {
copilot.init({
frameworkDriver: mockFrameworkDriver,
promptHandler: mockPromptHandler,
options: {
cacheMode: 'disabled'
}
});
copilot.start();

// First call
await copilot.perform('Tap on the login button');
copilot.end();

// Second call with same intent
copilot.start();
await copilot.perform('Tap on the login button');
copilot.end();

// Should call runPrompt twice since cache is disabled
expect(mockPromptHandler.runPrompt).toHaveBeenCalledTimes(2);
});
});
});
28 changes: 28 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,31 @@ export interface PromptHandler {
isSnapshotImageSupported: () => boolean;
}

/**
* The cache mode for the Copilot.
* - 'full': Cache is used with the screen state (default)
* - 'lightweight': Cache is used but only based on steps (without screen state)
* - 'disabled': No caching is used
* @default 'full'
*/
export type CacheMode = 'full' | 'lightweight' | 'disabled';

/**
* Configuration options for the Copilot behavior.
*/
export interface CopilotOptions {
/**
* The cache mode to use.
* @default 'full'
*/
cacheMode?: CacheMode;
}

/**
* Configuration options for Copilot.
* @property frameworkDriver The testing driver to use for interacting with the underlying testing framework.
* @property promptHandler The prompt handler to use for interacting with the AI service
* @property options Additional options for configuring Copilot behavior
*/
export interface Config {
/**
Expand All @@ -148,6 +171,11 @@ export interface Config {
* The prompt handler to use for interacting with the AI service
*/
promptHandler: PromptHandler;

/**
* Additional options for configuring Copilot behavior
*/
options?: CopilotOptions;
}

/**
Expand Down
Loading

0 comments on commit 0912725

Please sign in to comment.