diff --git a/README.md b/README.md index 689881de..b8044a06 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,22 @@ cp .env.example .env pnpm run spark "what is the current temperature in London?" ``` +## Usage + +Spark accepts up to three arguments: + +```bash +pnpm run spark "" [url] [data] +``` + +- **task**: Natural language description of what you want to do (required) +- **url**: Starting URL to begin the task from (optional) +- **data**: JSON object with contextual data for the task (optional) + ## Examples +### Basic Usage + ```bash # Get current weather pnpm run spark "is it raining in Tokyo" @@ -53,6 +67,45 @@ pnpm run spark "what is the current price of AAPL stock" pnpm run spark "what is the top headline on Reuters" ``` +### With Starting URL + +```bash +# Start from a specific website +pnpm run spark "find the latest news" "https://news.ycombinator.com" + +# Check weather on a specific weather site +pnpm run spark "get weather for San Francisco" "https://weather.com" + +# Book a flight from NYC to LAX on December 25th for 2 passengers +pnpm run spark "book a flight from NYC to LAX on December 25th for 2 passengers" "https://airline.com" +``` + +### With Contextual Data + +You can provide details either in the task description OR in the data object. Using the data object helps keep the task description clean and provides structured information the AI can easily reference. + +```bash +# Same task as above, but with structured data instead of in the prompt +pnpm run spark "book a flight" "https://airline.com" '{"departure":"NYC","destination":"LAX","date":"2024-12-25","passengers":2}' + +# Fill out a form with provided information +pnpm run spark "submit contact form" "https://example.com/contact" '{"name":"John Doe","email":"john@example.com","message":"Hello world"}' + +# Compare: details in prompt vs. data object +pnpm run spark "find hotels in Paris from Dec 20-22 for 2 guests" "https://booking.com" +# vs. +pnpm run spark "find hotels" "https://booking.com" '{"location":"Paris","checkIn":"2024-12-20","checkOut":"2024-12-22","guests":2}' +``` + +**When to use data objects:** + +- Complex information with multiple fields +- Structured data like dates, IDs, or specifications +- When you want to keep the task description simple and focused +- For reusable templates where only the data changes + +The data object is passed as JSON and becomes available to the AI throughout the entire task execution, allowing it to reference the information when filling forms, making selections, or completing complex workflows. + ## Development Built with TypeScript, Playwright, and OpenAI's GPT-4. diff --git a/src/index.ts b/src/index.ts index f43d51bf..0788bda1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,25 +8,44 @@ import { PlaywrightBrowser } from "./browser/playwrightBrowser.js"; // Load environment variables from .env file config(); -// Get the task and optional URL from the args +// Get the task, optional URL, and optional data from the args const task = process.argv[2]; const startingUrl = process.argv[3]; +const dataArg = process.argv[4]; if (!task) { console.error(chalk.red.bold("❌ Error: Missing task argument")); console.log(""); console.log(chalk.white.bold("Usage:")); - console.log(` ${chalk.cyan("spark")} ${chalk.green("")} ${chalk.yellow("[url]")}`); + console.log( + ` ${chalk.cyan("spark")} ${chalk.green("")} ${chalk.yellow("[url]")} ${chalk.magenta("[data]")}`, + ); console.log(""); console.log(chalk.white.bold("Examples:")); console.log(` ${chalk.cyan("spark")} ${chalk.green('"search for flights to Paris"')}`); console.log( ` ${chalk.cyan("spark")} ${chalk.green('"find the latest news"')} ${chalk.yellow("https://news.ycombinator.com")}`, ); + console.log( + ` ${chalk.cyan("spark")} ${chalk.green('"book a flight"')} ${chalk.yellow("https://airline.com")} ${chalk.magenta('\'{"departure":"NYC","destination":"LAX","date":"2024-12-25"}\'')}`, + ); console.log(""); process.exit(1); } +// Parse data argument if provided +let data = null; +if (dataArg) { + try { + data = JSON.parse(dataArg); + } catch (error) { + console.error(chalk.red.bold("❌ Error: Invalid JSON in data argument")); + console.log(chalk.gray(`Data argument: ${dataArg}`)); + console.log(chalk.gray(`Error: ${error instanceof Error ? error.message : String(error)}`)); + process.exit(1); + } +} + // Debug flag - set to true to see page snapshots const DEBUG = false; @@ -48,7 +67,7 @@ const DEBUG = false; }); // Execute the task - await webAgent.execute(task, startingUrl); + await webAgent.execute(task, startingUrl, data); // Close the browser when done await webAgent.close(); diff --git a/src/prompts.ts b/src/prompts.ts index 07e30f48..6da47e50 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -109,15 +109,27 @@ Task: {{task}} Explanation: {{explanation}} Plan: {{plan}} Today's Date: {{currentDate}} +{{#if data}} +Input Data: +\`\`\`json +{{data}} +\`\`\` +{{/if}} `.trim(), ); -export const buildTaskAndPlanPrompt = (task: string, explanation: string, plan: string) => +export const buildTaskAndPlanPrompt = ( + task: string, + explanation: string, + plan: string, + data?: any, +) => taskAndPlanTemplate({ task, explanation, plan, currentDate: getCurrentFormattedDate(), + data: data ? JSON.stringify(data, null, 2) : null, }); const pageSnapshotTemplate = buildPromptTemplate( diff --git a/src/webAgent.ts b/src/webAgent.ts index 051fb7e9..866155b6 100644 --- a/src/webAgent.ts +++ b/src/webAgent.ts @@ -42,6 +42,7 @@ export class WebAgent { private provider: LanguageModel; private DEBUG = false; private taskExplanation: string = ""; + private data: any = null; private readonly FILTERED_PREFIXES = ["/url:"]; private readonly ARIA_TRANSFORMATIONS: Array<[RegExp, string]> = [ [/^listitem/g, "li"], @@ -106,7 +107,7 @@ export class WebAgent { }, { role: "user", - content: buildTaskAndPlanPrompt(task, this.taskExplanation, this.plan), + content: buildTaskAndPlanPrompt(task, this.taskExplanation, this.plan, this.data), }, ]; return this.messages; @@ -377,6 +378,7 @@ export class WebAgent { this.plan = ""; this.url = ""; this.messages = []; + this.data = null; this.currentPage = { url: "", title: "" }; } @@ -405,7 +407,7 @@ export class WebAgent { return response.object; } - async execute(task: string, startingUrl?: string) { + async execute(task: string, startingUrl?: string, data?: any) { if (!task) { throw new Error("No task provided."); } @@ -413,6 +415,11 @@ export class WebAgent { // Reset state for new task this.resetState(); + // Store the data if provided + if (data) { + this.data = data; + } + // If a starting URL is provided, use it directly if (startingUrl) { this.url = startingUrl; diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 00000000..f72eca46 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,346 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { spawn } from "child_process"; +import path from "path"; + +// Mock child_process spawn +vi.mock("child_process", () => ({ + spawn: vi.fn(), +})); + +const mockSpawn = vi.mocked(spawn); + +describe("CLI index", () => { + let mockProcess: any; + + beforeEach(() => { + // Mock process object + mockProcess = { + stdout: { + on: vi.fn(), + pipe: vi.fn(), + }, + stderr: { + on: vi.fn(), + pipe: vi.fn(), + }, + on: vi.fn(), + kill: vi.fn(), + }; + + mockSpawn.mockReturnValue(mockProcess); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("Data argument parsing", () => { + const runCLI = (args: string[]) => { + // Simulate running the CLI with given arguments + const originalArgv = process.argv; + process.argv = ["node", "index.js", ...args]; + + // Mock console methods + const originalConsoleError = console.error; + const originalConsoleLog = console.log; + const consoleErrorSpy = vi.fn(); + const consoleLogSpy = vi.fn(); + console.error = consoleErrorSpy; + console.log = consoleLogSpy; + + try { + // Import and run the CLI module + // We can't directly test the CLI execution due to process.exit, + // but we can test the argument parsing logic + return { consoleErrorSpy, consoleLogSpy }; + } finally { + process.argv = originalArgv; + console.error = originalConsoleError; + console.log = originalConsoleLog; + } + }; + + it("should handle valid JSON data argument", () => { + const validJSON = '{"departure":"NYC","destination":"LAX","date":"2024-12-25"}'; + + // Test JSON parsing directly + expect(() => JSON.parse(validJSON)).not.toThrow(); + + const parsed = JSON.parse(validJSON); + expect(parsed).toEqual({ + departure: "NYC", + destination: "LAX", + date: "2024-12-25", + }); + }); + + it("should handle complex nested JSON data", () => { + const complexJSON = JSON.stringify({ + booking: { + flight: { + departure: { city: "NYC", time: "9:00 AM" }, + arrival: { city: "LAX", time: "12:00 PM" }, + }, + hotel: { + name: "Grand Hotel", + checkIn: "2024-12-25", + checkOut: "2024-12-27", + }, + }, + travelers: [ + { name: "John Doe", age: 30 }, + { name: "Jane Doe", age: 28 }, + ], + }); + + expect(() => JSON.parse(complexJSON)).not.toThrow(); + + const parsed = JSON.parse(complexJSON); + expect(parsed.booking.flight.departure.city).toBe("NYC"); + expect(parsed.travelers).toHaveLength(2); + expect(parsed.travelers[0].name).toBe("John Doe"); + }); + + it("should handle JSON with special characters", () => { + const specialCharsJSON = JSON.stringify({ + message: 'Hello "world" & ', + symbols: "!@#$%^&*()", + unicode: "café naïve résumé", + }); + + expect(() => JSON.parse(specialCharsJSON)).not.toThrow(); + + const parsed = JSON.parse(specialCharsJSON); + expect(parsed.message).toBe('Hello "world" & '); + expect(parsed.symbols).toBe("!@#$%^&*()"); + expect(parsed.unicode).toBe("café naïve résumé"); + }); + + it("should handle empty JSON object", () => { + const emptyJSON = "{}"; + + expect(() => JSON.parse(emptyJSON)).not.toThrow(); + + const parsed = JSON.parse(emptyJSON); + expect(parsed).toEqual({}); + }); + + it("should handle JSON arrays", () => { + const arrayJSON = JSON.stringify([ + { name: "Item 1", value: 100 }, + { name: "Item 2", value: 200 }, + ]); + + expect(() => JSON.parse(arrayJSON)).not.toThrow(); + + const parsed = JSON.parse(arrayJSON); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed).toHaveLength(2); + expect(parsed[0].name).toBe("Item 1"); + }); + + it("should reject invalid JSON", () => { + const invalidJSONs = [ + '{"invalid": json}', // Missing quotes + '{departure: "NYC"}', // Missing quotes on key + '{"incomplete":', // Incomplete JSON + "not json at all", // Not JSON + "", // Empty string + "null", // Null as string (valid JSON but might not be intended) + ]; + + invalidJSONs.forEach((invalidJSON, index) => { + if (index === invalidJSONs.length - 1) { + // 'null' is valid JSON, should not throw + expect(() => JSON.parse(invalidJSON)).not.toThrow(); + } else { + expect(() => JSON.parse(invalidJSON)).toThrow(); + } + }); + }); + + it("should handle JSON with boolean and numeric values", () => { + const mixedJSON = JSON.stringify({ + isActive: true, + count: 42, + price: 99.99, + isAvailable: false, + nullValue: null, + }); + + expect(() => JSON.parse(mixedJSON)).not.toThrow(); + + const parsed = JSON.parse(mixedJSON); + expect(parsed.isActive).toBe(true); + expect(parsed.count).toBe(42); + expect(parsed.price).toBe(99.99); + expect(parsed.isAvailable).toBe(false); + expect(parsed.nullValue).toBe(null); + }); + + it("should preserve data types in JSON parsing", () => { + const typedJSON = JSON.stringify({ + string: "text", + number: 123, + boolean: true, + array: [1, 2, 3], + object: { nested: "value" }, + nullValue: null, + }); + + const parsed = JSON.parse(typedJSON); + + expect(typeof parsed.string).toBe("string"); + expect(typeof parsed.number).toBe("number"); + expect(typeof parsed.boolean).toBe("boolean"); + expect(Array.isArray(parsed.array)).toBe(true); + expect(typeof parsed.object).toBe("object"); + expect(parsed.nullValue).toBe(null); + }); + + it("should handle deeply nested JSON", () => { + const deepJSON = JSON.stringify({ + level1: { + level2: { + level3: { + level4: { + level5: { + value: "deep value", + }, + }, + }, + }, + }, + }); + + expect(() => JSON.parse(deepJSON)).not.toThrow(); + + const parsed = JSON.parse(deepJSON); + expect(parsed.level1.level2.level3.level4.level5.value).toBe("deep value"); + }); + + it("should handle JSON with unicode characters", () => { + const unicodeJSON = JSON.stringify({ + emoji: "🚀✈️🏨", + chinese: "你好世界", + arabic: "مرحبا بالعالم", + russian: "Привет мир", + }); + + expect(() => JSON.parse(unicodeJSON)).not.toThrow(); + + const parsed = JSON.parse(unicodeJSON); + expect(parsed.emoji).toBe("🚀✈️🏨"); + expect(parsed.chinese).toBe("你好世界"); + expect(parsed.arabic).toBe("مرحبا بالعالم"); + expect(parsed.russian).toBe("Привет мир"); + }); + }); + + describe("Argument validation", () => { + it("should validate argument combinations", () => { + // Test various argument combinations + const validCombinations = [ + ["task only"], + ["task", "https://example.com"], + ["task", "https://example.com", '{"data": "value"}'], + ]; + + validCombinations.forEach((args) => { + expect(args.length).toBeGreaterThan(0); + if (args.length >= 3) { + // If data is provided, it should be valid JSON + expect(() => JSON.parse(args[2])).not.toThrow(); + } + }); + }); + + it("should handle URL validation patterns", () => { + const validUrls = [ + "https://example.com", + "http://test.org", + "https://subdomain.example.com/path", + "https://example.com:8080/path?query=value", + ]; + + validUrls.forEach((url) => { + expect(url).toMatch(/^https?:\/\/.+/); + }); + }); + + it("should identify potential invalid URLs", () => { + const invalidUrls = [ + "not-a-url", + "ftp://example.com", // Wrong protocol + "example.com", // Missing protocol + "", // Empty string + ]; + + invalidUrls.forEach((url) => { + if (url) { + expect(url).not.toMatch(/^https?:\/\/.+/); + } + }); + }); + }); + + describe("Error handling", () => { + it("should handle JSON parsing errors gracefully", () => { + const malformedJSONs = ['{"incomplete":', '{invalid: "json"}', "not json", "{trailing,}"]; + + malformedJSONs.forEach((json) => { + expect(() => JSON.parse(json)).toThrow(); + }); + }); + + it("should provide meaningful error messages for JSON parsing", () => { + try { + JSON.parse('{"invalid": json}'); + } catch (error) { + expect(error).toBeInstanceOf(SyntaxError); + expect(error.message).toContain("JSON"); + } + }); + }); + + describe("Usage examples", () => { + it("should demonstrate valid usage patterns", () => { + const examples = [ + { + args: ["search for flights to Paris"], + description: "Simple task without URL or data", + }, + { + args: ["find the latest news", "https://news.ycombinator.com"], + description: "Task with URL but no data", + }, + { + args: [ + "book a flight", + "https://airline.com", + JSON.stringify({ + departure: "NYC", + destination: "LAX", + date: "2024-12-25", + }), + ], + description: "Complete task with URL and data", + }, + ]; + + examples.forEach((example) => { + expect(example.args[0]).toBeTruthy(); // Task should exist + + if (example.args.length >= 2) { + // URL should be valid format if provided + expect(example.args[1]).toMatch(/^https?:\/\/.+/); + } + + if (example.args.length >= 3) { + // Data should be valid JSON if provided + expect(() => JSON.parse(example.args[2])).not.toThrow(); + } + }); + }); + }); +}); diff --git a/test/prompts.test.ts b/test/prompts.test.ts index 54162a2f..ee4ce3bf 100644 --- a/test/prompts.test.ts +++ b/test/prompts.test.ts @@ -183,6 +183,121 @@ describe("prompts", () => { expect(prompt).toContain("Plan: "); expect(prompt).toContain("Today's Date: Jan 15, 2024"); }); + + it("should include data when provided", () => { + const task = "Book a flight"; + const explanation = "Reserve airline tickets"; + const plan = "1. Search flights\n2. Select flight\n3. Book"; + const data = { + departure: "NYC", + destination: "LAX", + date: "2024-12-25", + passengers: 2, + }; + + const prompt = buildTaskAndPlanPrompt(task, explanation, plan, data); + + expect(prompt).toContain("Input Data:"); + expect(prompt).toContain("```json"); + expect(prompt).toContain('"departure": "NYC"'); + expect(prompt).toContain('"destination": "LAX"'); + expect(prompt).toContain('"date": "2024-12-25"'); + expect(prompt).toContain('"passengers": 2'); + expect(prompt).toContain("```"); + }); + + it("should not include data section when data is null", () => { + const task = "Search for hotels"; + const explanation = "Find accommodation"; + const plan = "1. Search\n2. Compare\n3. Select"; + + const prompt = buildTaskAndPlanPrompt(task, explanation, plan, null); + + expect(prompt).not.toContain("Input Data:"); + expect(prompt).not.toContain("```json"); + }); + + it("should not include data section when data is undefined", () => { + const task = "Search for hotels"; + const explanation = "Find accommodation"; + const plan = "1. Search\n2. Compare\n3. Select"; + + const prompt = buildTaskAndPlanPrompt(task, explanation, plan); + + expect(prompt).not.toContain("Input Data:"); + expect(prompt).not.toContain("```json"); + }); + + it("should handle complex nested data objects", () => { + const task = "Complete booking"; + const explanation = "Finalize reservation"; + const plan = "1. Review\n2. Pay\n3. Confirm"; + const data = { + booking: { + flight: { + departure: { city: "NYC", time: "9:00 AM" }, + arrival: { city: "LAX", time: "12:00 PM" }, + }, + hotel: { + name: "Grand Hotel", + checkIn: "2024-12-25", + checkOut: "2024-12-27", + }, + }, + travelers: [ + { name: "John Doe", age: 30 }, + { name: "Jane Doe", age: 28 }, + ], + }; + + const prompt = buildTaskAndPlanPrompt(task, explanation, plan, data); + + expect(prompt).toContain("Input Data:"); + expect(prompt).toContain("```json"); + expect(prompt).toContain('"departure"'); + expect(prompt).toContain('"NYC"'); + expect(prompt).toContain('"Grand Hotel"'); + expect(prompt).toContain('"John Doe"'); + expect(prompt).toContain('"travelers"'); + }); + + it("should format data with proper JSON indentation", () => { + const task = "Test task"; + const explanation = "Test explanation"; + const plan = "Test plan"; + const data = { + level1: { + level2: { + value: "nested", + }, + }, + }; + + const prompt = buildTaskAndPlanPrompt(task, explanation, plan, data); + + // Check that JSON is properly indented (2 spaces) + expect(prompt).toContain( + '{\n "level1": {\n "level2": {\n "value": "nested"\n }\n }\n}', + ); + }); + + it("should handle data with special characters", () => { + const task = "Special chars test"; + const explanation = "Test special characters"; + const plan = "Handle special chars"; + const data = { + message: 'Hello "world" & ', + symbols: "!@#$%^&*()", + unicode: "café naïve résumé", + }; + + const prompt = buildTaskAndPlanPrompt(task, explanation, plan, data); + + expect(prompt).toContain("Input Data:"); + expect(prompt).toContain('"Hello \\"world\\" & "'); + expect(prompt).toContain('"!@#$%^&*()"'); + expect(prompt).toContain('"café naïve résumé"'); + }); }); describe("buildPageSnapshotPrompt", () => { diff --git a/test/webAgent.test.ts b/test/webAgent.test.ts index d2998496..2561d27d 100644 --- a/test/webAgent.test.ts +++ b/test/webAgent.test.ts @@ -593,6 +593,228 @@ describe("WebAgent", () => { }); }); + describe("Data handling", () => { + it("should handle data parameter in execute method", async () => { + const planResponse = { + object: { + explanation: "test explanation", + plan: "test plan", + }, + }; + + const doneResponse = { + object: { + currentStep: "Completing task", + observation: "Task finished", + extractedData: "Final data", + thought: "Task is done", + action: { + action: PageAction.Done, + value: "Task completed with data", + }, + }, + }; + + const validationResponse = { + object: { + isValid: true, + }, + }; + + mockGenerateObject + .mockResolvedValueOnce(planResponse) + .mockResolvedValueOnce(doneResponse) + .mockResolvedValueOnce(validationResponse); + + const testData = { + departure: "NYC", + destination: "LAX", + date: "2024-12-25", + }; + + const result = await webAgent.execute("test task", "https://example.com", testData); + expect(result).toBe("Task completed with data"); + }); + + it("should include data in setupMessages when provided", async () => { + const testData = { + departure: "NYC", + destination: "LAX", + }; + + // Mock all required responses for a complete execution + const planResponse = { + object: { + explanation: "test explanation", + plan: "test plan", + }, + }; + + const doneResponse = { + object: { + currentStep: "Completing task", + observation: "Task finished", + extractedData: "Final data", + thought: "Task is done", + action: { + action: PageAction.Done, + value: "Task completed with data included", + }, + }, + }; + + const validationResponse = { + object: { + isValid: true, + }, + }; + + mockGenerateObject + .mockResolvedValueOnce(planResponse) + .mockResolvedValueOnce(doneResponse) + .mockResolvedValueOnce(validationResponse); + + const result = await webAgent.execute("test task", "https://example.com", testData); + + expect(result).toBe("Task completed with data included"); + expect(mockGenerateObject).toHaveBeenCalledTimes(3); + }); + + it("should reset data on resetState", () => { + // Set up some state including data + const testData = { test: "value" }; + + // We can't directly test private properties, but we can test the behavior + webAgent.resetState(); + + // After reset, setupMessages should work without data + const messages = webAgent.setupMessages("test task"); + expect(messages).toHaveLength(2); + expect(messages[1].content).not.toContain("Input Data:"); + }); + + it("should handle null data parameter", async () => { + const planResponse = { + object: { + explanation: "test explanation", + plan: "test plan", + }, + }; + + const doneResponse = { + object: { + currentStep: "Completing task", + observation: "Task finished", + extractedData: "Final data", + thought: "Task is done", + action: { + action: PageAction.Done, + value: "Task completed without data", + }, + }, + }; + + const validationResponse = { + object: { + isValid: true, + }, + }; + + mockGenerateObject + .mockResolvedValueOnce(planResponse) + .mockResolvedValueOnce(doneResponse) + .mockResolvedValueOnce(validationResponse); + + const result = await webAgent.execute("test task", "https://example.com", null); + expect(result).toBe("Task completed without data"); + }); + + it("should handle undefined data parameter", async () => { + const planResponse = { + object: { + explanation: "test explanation", + plan: "test plan", + }, + }; + + const doneResponse = { + object: { + currentStep: "Completing task", + observation: "Task finished", + extractedData: "Final data", + thought: "Task is done", + action: { + action: PageAction.Done, + value: "Task completed without data", + }, + }, + }; + + const validationResponse = { + object: { + isValid: true, + }, + }; + + mockGenerateObject + .mockResolvedValueOnce(planResponse) + .mockResolvedValueOnce(doneResponse) + .mockResolvedValueOnce(validationResponse); + + const result = await webAgent.execute("test task", "https://example.com"); + expect(result).toBe("Task completed without data"); + }); + + it("should handle complex data objects", async () => { + const planResponse = { + object: { + explanation: "test explanation", + plan: "test plan", + }, + }; + + const doneResponse = { + object: { + currentStep: "Completing task", + observation: "Task finished", + extractedData: "Final data", + thought: "Task is done", + action: { + action: PageAction.Done, + value: "Complex data task completed", + }, + }, + }; + + const validationResponse = { + object: { + isValid: true, + }, + }; + + mockGenerateObject + .mockResolvedValueOnce(planResponse) + .mockResolvedValueOnce(doneResponse) + .mockResolvedValueOnce(validationResponse); + + const complexData = { + booking: { + flight: { + departure: { city: "NYC", time: "9:00 AM" }, + arrival: { city: "LAX", time: "12:00 PM" }, + }, + }, + travelers: [ + { name: "John Doe", age: 30 }, + { name: "Jane Doe", age: 28 }, + ], + }; + + const result = await webAgent.execute("test task", "https://example.com", complexData); + expect(result).toBe("Complex data task completed"); + }); + }); + describe("Task validation", () => { it("should validate successful task completion", async () => { const planResponse = {