Skip to content

Commit

Permalink
merge with master
Browse files Browse the repository at this point in the history
  • Loading branch information
tzvielwix committed Jan 20, 2025
2 parents ffe974d + 4e2df2f commit 9ff9022
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 44 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "detox-copilot",
"version": "0.0.28",
"version": "0.0.31",
"description": "A flexible plugin that drives your tests with human-written commands, enhanced by the power of large language models (LLMs)",
"keywords": [
"detox",
Expand Down
3 changes: 2 additions & 1 deletion src/Copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export class Copilot {
this.codeEvaluator,
this.snapshotManager,
config.promptHandler,
this.cacheHandler
this.cacheHandler,
config.options?.cacheMode
);
this.pilotPerformer = new PilotPerformer(this.pilotPromptCreator, this.copilotStepPerformer, config.promptHandler, () => this.screenCapturer.capture());
}
Expand Down
22 changes: 19 additions & 3 deletions src/actions/CopilotStepPerformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@ import {PromptCreator} from '@/utils/PromptCreator';
import {CodeEvaluator} from '@/utils/CodeEvaluator';
import {SnapshotManager} from '@/utils/SnapshotManager';
import {CacheHandler} from '@/utils/CacheHandler';
import {CodeEvaluationResult, PreviousStep, PromptHandler, CaptureResult} from '@/types';
import {CacheMode, CodeEvaluationResult, PreviousStep, PromptHandler, CaptureResult} from '@/types';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import {extractCodeBlock} from '@/utils/extractCodeBlock';

export class CopilotStepPerformer {
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 +35,19 @@ export class CopilotStepPerformer {
}

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 shouldOverrideCache() {
Expand Down
60 changes: 59 additions & 1 deletion src/integration tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,62 @@ describe('Copilot Integration Tests', () => {
expect(spyPilotPerformerPerform).toHaveBeenCalledWith(goal);
});
});
});
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);
});
});
});
2 changes: 2 additions & 0 deletions src/test-utils/APICatalogTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const dummyBarContext2 = {bar: jest.fn()};

export const promptCreatorConstructorMockAPI: TestingFrameworkAPICatalog = {
context: {},
name: 'Test Framework',
description: 'A testing framework for unit testing purposes',
categories: [
{
title: 'Actions',
Expand Down
32 changes: 32 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,14 @@ export interface TestingFrameworkDriver {

/**
* Represents the available API of the testing framework that can be used by Copilot.
* @property name Optional name of the testing framework (e.g. "Detox", "Jest", etc.).
* @property description Optional description of the testing framework's purpose and capabilities.
* @property context The available variables of the testing framework (i.e. exposes the matching function, expect, etc.).
* @property categories The available categories of the testing framework API.
*/
export type TestingFrameworkAPICatalog = {
name?: string;
description?: string;
context: any;
categories: TestingFrameworkAPICatalogCategory[];
}
Expand Down Expand Up @@ -145,8 +149,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 @@ -158,6 +185,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
4 changes: 2 additions & 2 deletions src/utils/PromptCreator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ describe('PromptCreator', () => {
{
step: 'navigate to login screen',
code: 'await element(by.id("login")).tap();',
result: undefined
result: 'success'
},
{
step: 'enter username',
code: 'await element(by.id("username")).typeText("john_doe");',
result: undefined
result: 'john doe'
}
];

Expand Down
22 changes: 21 additions & 1 deletion src/utils/PromptCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,30 @@ export class PromptCreator {
}

private createBasePrompt(): string[] {
return [
const basePrompt = [
"# Test Code Generation",
"",
"You are an AI assistant tasked with generating test code for an application using the provided UI testing framework API.",
"Please generate the minimal executable code to perform the desired intent based on the given information and context.",
""
];

if (this.apiCatalog.name || this.apiCatalog.description) {
basePrompt.push("## Testing Framework");
basePrompt.push("");

if (this.apiCatalog.name) {
basePrompt.push(`Framework: ${this.apiCatalog.name}`);
basePrompt.push("");
}

if (this.apiCatalog.description) {
basePrompt.push(`Description: ${this.apiCatalog.description}`);
basePrompt.push("");
}
}

return basePrompt;
}

private createContext(
Expand Down Expand Up @@ -107,6 +124,7 @@ export class PromptCreator {
"```",
previousStep.code,
"```",
...(previousStep.result ? [`- Result: ${previousStep.result}`] : []),
""
]).flat(),
""
Expand Down Expand Up @@ -216,6 +234,8 @@ export class PromptCreator {
}
steps.push(
"If you cannot generate the relevant code due to ambiguity or invalid intent, return code that throws an informative error explaining the problem in one sentence.",
"Each step must be completely independent - do not rely on any variables or assignments from previous steps. Even if a variable was declared or assigned in a previous step, you must redeclare and reassign it in your current step.",
"Use the provided framework APIs as much as possible - prefer using the documented API methods over creating custom implementations.",
"Wrap the generated code with backticks, without any additional formatting.",
"Do not provide any additional code beyond the minimal executable code required to perform the intent."
);
Expand Down
44 changes: 32 additions & 12 deletions src/utils/__snapshots__/PromptCreator.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ exports[`PromptCreator constructor should merge redundant categories 1`] = `
You are an AI assistant tasked with generating test code for an application using the provided UI testing framework API.
Please generate the minimal executable code to perform the desired intent based on the given information and context.
## Testing Framework
Framework: Test Framework
Description: A testing framework for unit testing purposes
## Context
### Intent to perform
Expand Down Expand Up @@ -123,8 +129,10 @@ Please follow these steps carefully:
1. Analyze the provided intent and the view hierarchy to understand the required action.
2. Generate the minimal executable code required to perform the intent using the available API.
3. If you cannot generate the relevant code due to ambiguity or invalid intent, return code that throws an informative error explaining the problem in one sentence.
4. Wrap the generated code with backticks, without any additional formatting.
5. Do not provide any additional code beyond the minimal executable code required to perform the intent.
4. Each step must be completely independent - do not rely on any variables or assignments from previous steps. Even if a variable was declared or assigned in a previous step, you must redeclare and reassign it in your current step.
5. Use the provided framework APIs as much as possible - prefer using the documented API methods over creating custom implementations.
6. Wrap the generated code with backticks, without any additional formatting.
7. Do not provide any additional code beyond the minimal executable code required to perform the intent.
### Verify the prompt
Expand Down Expand Up @@ -251,8 +259,10 @@ Please follow these steps carefully:
1. Analyze the provided intent and the view hierarchy to understand the required action.
2. Generate the minimal executable code required to perform the intent using the available API.
3. If you cannot generate the relevant code due to ambiguity or invalid intent, return code that throws an informative error explaining the problem in one sentence.
4. Wrap the generated code with backticks, without any additional formatting.
5. Do not provide any additional code beyond the minimal executable code required to perform the intent.
4. Each step must be completely independent - do not rely on any variables or assignments from previous steps. Even if a variable was declared or assigned in a previous step, you must redeclare and reassign it in your current step.
5. Use the provided framework APIs as much as possible - prefer using the documented API methods over creating custom implementations.
6. Wrap the generated code with backticks, without any additional formatting.
7. Do not provide any additional code beyond the minimal executable code required to perform the intent.
### Verify the prompt
Expand Down Expand Up @@ -393,8 +403,10 @@ Please follow these steps carefully:
1. Analyze the provided intent and the view hierarchy to understand the required action.
2. Generate the minimal executable code required to perform the intent using the available API.
3. If you cannot generate the relevant code due to ambiguity or invalid intent, return code that throws an informative error explaining the problem in one sentence.
4. Wrap the generated code with backticks, without any additional formatting.
5. Do not provide any additional code beyond the minimal executable code required to perform the intent.
4. Each step must be completely independent - do not rely on any variables or assignments from previous steps. Even if a variable was declared or assigned in a previous step, you must redeclare and reassign it in your current step.
5. Use the provided framework APIs as much as possible - prefer using the documented API methods over creating custom implementations.
6. Wrap the generated code with backticks, without any additional formatting.
7. Do not provide any additional code beyond the minimal executable code required to perform the intent.
### Verify the prompt
Expand Down Expand Up @@ -509,8 +521,10 @@ Please follow these steps carefully:
5. If the visual assertion fails, return code that throws an informative error explaining the failure.
6. If visual validation is not possible, proceed to generate the minimal executable code required to perform the intent.
7. If you cannot generate the relevant code due to ambiguity or invalid intent, return code that throws an informative error explaining the problem in one sentence.
8. Wrap the generated code with backticks, without any additional formatting.
9. Do not provide any additional code beyond the minimal executable code required to perform the intent.
8. Each step must be completely independent - do not rely on any variables or assignments from previous steps. Even if a variable was declared or assigned in a previous step, you must redeclare and reassign it in your current step.
9. Use the provided framework APIs as much as possible - prefer using the documented API methods over creating custom implementations.
10. Wrap the generated code with backticks, without any additional formatting.
11. Do not provide any additional code beyond the minimal executable code required to perform the intent.
### Verify the prompt
Expand Down Expand Up @@ -626,8 +640,10 @@ Please follow these steps carefully:
1. Analyze the provided intent and the view hierarchy to understand the required action.
2. Generate the minimal executable code required to perform the intent using the available API.
3. If you cannot generate the relevant code due to ambiguity or invalid intent, return code that throws an informative error explaining the problem in one sentence.
4. Wrap the generated code with backticks, without any additional formatting.
5. Do not provide any additional code beyond the minimal executable code required to perform the intent.
4. Each step must be completely independent - do not rely on any variables or assignments from previous steps. Even if a variable was declared or assigned in a previous step, you must redeclare and reassign it in your current step.
5. Use the provided framework APIs as much as possible - prefer using the documented API methods over creating custom implementations.
6. Wrap the generated code with backticks, without any additional formatting.
7. Do not provide any additional code beyond the minimal executable code required to perform the intent.
### Verify the prompt
Expand Down Expand Up @@ -673,13 +689,15 @@ No snapshot image is attached for this intent.
\`\`\`
await element(by.id("login")).tap();
\`\`\`
- Result: success
#### Step 2
- Intent: "enter username"
- Generated code:
\`\`\`
await element(by.id("username")).typeText("john_doe");
\`\`\`
- Result: john doe
## Available Testing Framework API
Expand Down Expand Up @@ -755,8 +773,10 @@ Please follow these steps carefully:
1. Analyze the provided intent and the view hierarchy to understand the required action.
2. Generate the minimal executable code required to perform the intent using the available API.
3. If you cannot generate the relevant code due to ambiguity or invalid intent, return code that throws an informative error explaining the problem in one sentence.
4. Wrap the generated code with backticks, without any additional formatting.
5. Do not provide any additional code beyond the minimal executable code required to perform the intent.
4. Each step must be completely independent - do not rely on any variables or assignments from previous steps. Even if a variable was declared or assigned in a previous step, you must redeclare and reassign it in your current step.
5. Use the provided framework APIs as much as possible - prefer using the documented API methods over creating custom implementations.
6. Wrap the generated code with backticks, without any additional formatting.
7. Do not provide any additional code beyond the minimal executable code required to perform the intent.
### Verify the prompt
Expand Down
Loading

0 comments on commit 9ff9022

Please sign in to comment.