Skip to main content
Glama
planUtils.test.tsβ€’11.7 kB
import { expect } from "chai"; import fs from "fs/promises"; import path from "path"; import os from "os"; import { exportPlanFromLogs, importPlanFromYaml, executePlan } from "../src/utils/planUtils"; import { Plan } from "../src/models/Plan"; describe("Plan Utils", () => { let tempDir: string; beforeEach(async () => { tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "plan-test-")); }); afterEach(async () => { await fs.rm(tempDir, { recursive: true, force: true }); }); describe("exportPlanFromLogs", () => { it("should export a plan from valid log files", async () => { // Create mock log entries const logEntries = [ { timestamp: "2023-01-01T10:00:00.000Z", tool: "launchApp", params: { packageName: "com.example.app" }, result: { success: true } }, { timestamp: "2023-01-01T10:00:01.000Z", tool: "observe", params: { withViewHierarchy: true }, result: { success: true } }, { timestamp: "2023-01-01T10:00:02.000Z", tool: "tapOnText", params: { text: "Login" }, result: { success: true } }, { timestamp: "2023-01-01T10:00:03.000Z", tool: "observe", params: { withViewHierarchy: true }, result: { success: true } } ]; // Write log entries to files const logFile = path.join(tempDir, "test.json"); const logContent = logEntries.map(entry => JSON.stringify(entry)).join("\n"); await fs.writeFile(logFile, logContent); // Export plan const outputPath = path.join(tempDir, "test-plan.yaml"); const result = await exportPlanFromLogs(tempDir, "Test Plan", outputPath); expect(result.success).to.be.true; expect(result.stepCount).to.equal(3); // launchApp, tapOnText, last observe expect(result.planContent).to.include("Test Plan"); expect(result.planContent).to.include("launchApp"); expect(result.planContent).to.include("tapOnText"); }); it("should omit emulator tools from plans", async () => { const logEntries = [ { timestamp: "2023-01-01T10:00:00.000Z", tool: "startDevice", params: { avdName: "test" }, result: { success: true } }, { timestamp: "2023-01-01T10:00:01.000Z", tool: "launchApp", params: { packageName: "com.example.app" }, result: { success: true } } ]; const logFile = path.join(tempDir, "test.json"); const logContent = logEntries.map(entry => JSON.stringify(entry)).join("\n"); await fs.writeFile(logFile, logContent); const outputPath = path.join(tempDir, "test-plan.yaml"); const result = await exportPlanFromLogs(tempDir, "Test Plan", outputPath); expect(result.success).to.be.true; expect(result.stepCount).to.equal(1); // Only launchApp expect(result.planContent).to.not.include("startDevice"); expect(result.planContent).to.include("launchApp"); }); it("should only include the last observe call", async () => { const logEntries = [ { timestamp: "2023-01-01T10:00:00.000Z", tool: "observe", params: { withViewHierarchy: true }, result: { success: true } }, { timestamp: "2023-01-01T10:00:01.000Z", tool: "tapOnText", params: { text: "Login" }, result: { success: true } }, { timestamp: "2023-01-01T10:00:02.000Z", tool: "observe", params: { withViewHierarchy: false }, result: { success: true } } ]; const logFile = path.join(tempDir, "test.json"); const logContent = logEntries.map(entry => JSON.stringify(entry)).join("\n"); await fs.writeFile(logFile, logContent); const outputPath = path.join(tempDir, "test-plan.yaml"); const result = await exportPlanFromLogs(tempDir, "Test Plan", outputPath); expect(result.success).to.be.true; expect(result.stepCount).to.equal(2); // tapOnText + last observe // Check that the last observe has the correct params const planLines = result.planContent!.split("\n"); const observeLines = planLines.filter(line => line.includes("withViewHierarchy")); expect(observeLines.length).to.equal(1); expect(observeLines[0]).to.include("false"); }); it("should handle empty log directory", async () => { const outputPath = path.join(tempDir, "test-plan.yaml"); const result = await exportPlanFromLogs(tempDir, "Test Plan", outputPath); expect(result.success).to.be.false; expect(result.error).to.include("No log files found"); }); it("should save to specified output path", async () => { const logEntries = [ { timestamp: "2023-01-01T10:00:00.000Z", tool: "launchApp", params: { packageName: "com.example.app" }, result: { success: true } } ]; const logFile = path.join(tempDir, "test.json"); const logContent = logEntries.map(entry => JSON.stringify(entry)).join("\n"); await fs.writeFile(logFile, logContent); const outputPath = path.join(tempDir, "plan.yaml"); const result = await exportPlanFromLogs(tempDir, "Test Plan", outputPath); expect(result.success).to.be.true; expect(result.planPath).to.equal(outputPath); const savedContent = await fs.readFile(outputPath, "utf-8"); expect(savedContent).to.include("Test Plan"); expect(savedContent).to.include("launchApp"); }); }); describe("importPlanFromYaml", () => { it("should import a valid YAML plan", () => { const yamlContent = ` name: Test Plan description: A test plan steps: - tool: launchApp appId: com.example.app - tool: tapOnText text: Login metadata: createdAt: "2023-01-01T10:00:00.000Z" version: "0.0.1" `; const plan = importPlanFromYaml(yamlContent); expect(plan.name).to.equal("Test Plan"); expect(plan.description).to.equal("A test plan"); expect(plan.steps).to.have.length(2); expect(plan.steps[0].tool).to.equal("launchApp"); expect(plan.steps[0].params.appId).to.equal("com.example.app"); expect(plan.steps[1].tool).to.equal("tapOnText"); expect(plan.steps[1].params.text).to.equal("Login"); }); it("should throw error for invalid YAML", () => { const invalidYaml = "invalid: yaml: content: ["; expect(() => importPlanFromYaml(invalidYaml)).to.throw(); }); it("should throw error for missing required fields", () => { const yamlContent = ` description: A plan without name steps: [] `; expect(() => importPlanFromYaml(yamlContent)).to.throw("Invalid plan structure"); }); it("should throw error for invalid step structure", () => { const yamlContent = ` name: Test Plan steps: - invalid_step: true `; expect(() => importPlanFromYaml(yamlContent)).to.throw("Invalid step"); }); }); describe("executePlan", () => { it("should return success for empty plan", async () => { const plan: Plan = { name: "Empty Plan", steps: [] }; const result = await executePlan(plan, 0); expect(result.success).to.be.true; expect(result.executedSteps).to.equal(0); expect(result.totalSteps).to.equal(0); }); it("should start from step 0 when no startStep is provided", async () => { const plan: Plan = { name: "Test Plan", steps: [] }; const result = await executePlan(plan, 0); expect(result.success).to.be.true; expect(result.executedSteps).to.equal(0); expect(result.totalSteps).to.equal(0); }); it("should start from step 0 when startStep is negative", async () => { const plan: Plan = { name: "Test Plan", steps: [] }; const result = await executePlan(plan, -5); expect(result.success).to.be.true; expect(result.executedSteps).to.equal(0); expect(result.totalSteps).to.equal(0); }); it("should start from step 0 when startStep is not a number", async () => { const plan: Plan = { name: "Test Plan", steps: [] }; // @ts-ignore - Intentionally passing non-number for testing const result = await executePlan(plan, "invalid"); expect(result.success).to.be.true; expect(result.executedSteps).to.equal(0); expect(result.totalSteps).to.equal(0); }); it("should throw error when startStep is greater than total steps", async () => { const plan: Plan = { name: "Test Plan", steps: [ { tool: "observe", params: { withViewHierarchy: false } }, { tool: "observe", params: { withViewHierarchy: true } } ] }; const result = await executePlan(plan, 5); expect(result.success).to.be.false; expect(result.failedStep?.error).to.include("Start step index 5 is out of bounds"); expect(result.failedStep?.error).to.include("valid range: 0-1"); }); it("should throw error when startStep equals total steps", async () => { const plan: Plan = { name: "Test Plan", steps: [ { tool: "observe", params: { withViewHierarchy: false } } ] }; const result = await executePlan(plan, 1); expect(result.success).to.be.false; expect(result.failedStep?.error).to.include("Start step index 1 is out of bounds"); expect(result.failedStep?.error).to.include("valid range: 0-0"); }); it("should execute all steps when startStep is 0", async () => { const plan: Plan = { name: "Test Plan", steps: [ { tool: "unknownTool1", params: {} }, { tool: "unknownTool2", params: {} } ] }; const result = await executePlan(plan, 0); // This will fail because the tools don't exist, but it should show we tried to start from step 0 expect(result.success).to.be.false; expect(result.totalSteps).to.equal(2); expect(result.executedSteps).to.equal(0); expect(result.failedStep?.stepIndex).to.equal(0); }); it("should skip initial steps when startStep is greater than 0", async () => { const plan: Plan = { name: "Test Plan", steps: [ { tool: "skippedTool", params: {} }, { tool: "unknownTool", params: {} } ] }; const result = await executePlan(plan, 1); // This will fail because unknownTool doesn't exist, but it should show we started from step 1 expect(result.success).to.be.false; expect(result.totalSteps).to.equal(2); expect(result.executedSteps).to.equal(0); expect(result.failedStep?.stepIndex).to.equal(1); expect(result.failedStep?.tool).to.equal("unknownTool"); }); it("should handle valid startStep for single step plan", async () => { const plan: Plan = { name: "Single Step Plan", steps: [ { tool: "unknownTool", params: {} } ] }; const result = await executePlan(plan, 0); expect(result.success).to.be.false; expect(result.totalSteps).to.equal(1); expect(result.executedSteps).to.equal(0); expect(result.failedStep?.stepIndex).to.equal(0); }); // Note: Full execution tests would require mocking the ToolRegistry // and actual tool implementations, which is beyond the scope of this unit test }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zillow/auto-mobile'

If you have feedback or need assistance with the MCP directory API, please join our Discord server