UNPKG

ai-pp3

Version:

CLI tool combining multimodal AI analysis with RawTherapee's engine to generate optimized PP3 profiles for RAW photography

397 lines (388 loc) 15.7 kB
import { describe, expect, it, vi, beforeEach } from "vitest"; import { splitContentBySections, splitPP3ContentBySections, splitContentIntoSections, } from "../pp3-sections/section-parser.js"; import { generateMultiPP3FromRawImage } from "../agent.js"; import { convertDngToImage, convertDngToImageWithPP3, } from "../raw-therapee-wrap.js"; import { generateText } from "ai"; import fs from "node:fs"; import { validateFileAccess } from "../utils/validation.js"; vi.mock("../raw-therapee-wrap.js"); vi.mock("ai"); vi.mock("../utils/validation.js", () => ({ validateFileAccess: vi.fn(), handleFileError: vi.fn(), })); vi.mock("node:fs", () => ({ default: { promises: { readFile: vi.fn(), writeFile: vi.fn(), unlink: vi.fn(), access: vi.fn(), }, }, })); describe("Agent Section Parsing", () => { describe("splitContentBySections", () => { it("should split content into sections correctly", () => { const content = `[Section1] param1=value1 param2=value2 [Section2] param3=value3 param4=value4`; const result = splitContentBySections(content); expect(result.sections).toHaveLength(2); expect(result.sectionOrders).toEqual(["Section1", "Section2"]); expect(result.sections[0]).toBe("[Section1]\nparam1=value1\nparam2=value2"); expect(result.sections[1]).toBe("[Section2]\nparam3=value3\nparam4=value4"); }); it("should handle empty content", () => { const result = splitContentBySections(""); expect(result.sections).toHaveLength(0); expect(result.sectionOrders).toHaveLength(0); }); it("should handle content with no sections", () => { const content = "param1=value1\nparam2=value2"; const result = splitContentBySections(content); expect(result.sections).toHaveLength(0); expect(result.sectionOrders).toHaveLength(0); }); it("should call processSection callback for each section", () => { const content = `[Section1] param1=value1 [Section2] param2=value2`; const processSectionMock = vi.fn(); splitContentBySections(content, processSectionMock); expect(processSectionMock).toHaveBeenCalledTimes(2); expect(processSectionMock).toHaveBeenCalledWith("[Section1]\nparam1=value1", "Section1", 0); expect(processSectionMock).toHaveBeenCalledWith("[Section2]\nparam2=value2", "Section2", 1); }); }); describe("splitPP3ContentBySections", () => { it("should categorize sections based on provided section names", () => { const content = `[Section1] param1=value1 [Section2] param2=value2 [Section3] param3=value3`; const sectionNames = ["Section1", "Section3"]; const result = splitPP3ContentBySections(content, sectionNames); expect(result.includedSections).toHaveLength(2); expect(result.excludedSections).toHaveLength(1); expect(result.includedSections[0]).toBe("[Section1]\nparam1=value1"); expect(result.includedSections[1]).toBe("[Section3]\nparam3=value3"); expect(result.excludedSections[0]).toBe("[Section2]\nparam2=value2"); expect(result.sectionOrders).toEqual([ "Section1", "Section2", "Section3", ]); }); it("should handle empty content", () => { const result = splitPP3ContentBySections("", ["Section1"]); expect(result.includedSections).toHaveLength(0); expect(result.excludedSections).toHaveLength(0); expect(result.sectionOrders).toHaveLength(0); }); it("should handle content with no matching sections", () => { const content = `[Section1] param1=value1 [Section2] param2=value2`; const sectionNames = ["Section3", "Section4"]; const result = splitPP3ContentBySections(content, sectionNames); expect(result.includedSections).toHaveLength(0); expect(result.excludedSections).toHaveLength(2); expect(result.sectionOrders).toEqual(["Section1", "Section2"]); }); }); describe("splitContentIntoSections", () => { it("should split content into sections", () => { const content = `[Section1] param1=value1 [Section2] param2=value2`; const result = splitContentIntoSections(content); expect(result.sections).toHaveLength(2); expect(result.sectionOrders).toEqual(["Section1", "Section2"]); }); it("should handle empty content", () => { const result = splitContentIntoSections(""); expect(result.sections).toHaveLength(0); expect(result.sectionOrders).toHaveLength(0); }); it("should handle content with no sections", () => { const content = "param1=value1\nparam2=value2"; const result = splitContentIntoSections(content); expect(result.sections).toHaveLength(0); expect(result.sectionOrders).toHaveLength(0); }); }); describe("splitContentIntoSections additional tests", () => { it("should handle single section", () => { const content = `[Section1] param1=value1 param2=value2`; const result = splitContentIntoSections(content); expect(result.sections).toHaveLength(1); expect(result.sectionOrders).toEqual(["Section1"]); expect(result.sections[0]).toBe("[Section1]\nparam1=value1\nparam2=value2"); }); it("should handle whitespace-only content", () => { const content = " \n \t \n "; const result = splitContentIntoSections(content); expect(result.sections).toHaveLength(0); expect(result.sectionOrders).toHaveLength(0); }); it("should handle sections with empty lines", () => { const content = `[Section1] param1=value1 param2=value2 [Section2] param3=value3`; const result = splitContentIntoSections(content); expect(result.sections).toHaveLength(2); expect(result.sections[0]).toContain("param1=value1"); expect(result.sections[0]).toContain("param2=value2"); expect(result.sections[1]).toContain("param3=value3"); }); }); }); describe("Multi-generation PP3 Processing", () => { const TEST_INPUT_FILE = "/path/to/test.dng"; beforeEach(() => { vi.clearAllMocks(); // Mock successful file operations vi.mocked(fs.promises.readFile).mockImplementation((path, encoding) => { return encoding === "utf8" ? Promise.resolve("[Exposure]\nExposure=0.5\n[ColorToning]\nEnabled=true") : Promise.resolve(Buffer.from("mock image data")); }); vi.mocked(fs.promises.writeFile).mockResolvedValue(); vi.mocked(fs.promises.unlink).mockResolvedValue(); vi.mocked(fs.promises.access).mockResolvedValue(); // Mock successful validation vi.mocked(validateFileAccess).mockResolvedValue(); // Mock successful RawTherapee operations vi.mocked(convertDngToImage).mockResolvedValue(); vi.mocked(convertDngToImageWithPP3).mockResolvedValue(); // Mock AI responses vi.mocked(generateText).mockResolvedValue({ text: "<<<<<<< SEARCH\n[Exposure]\nExposure=0.5\n=======\n[Exposure]\nExposure=1.0\n>>>>>>> REPLACE", reasoning: undefined, files: [], reasoningDetails: [], sources: [], experimental_output: undefined, toolCalls: [], toolResults: [], finishReason: "stop", usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, warnings: undefined, steps: [], request: {}, response: { id: "mock-id", timestamp: new Date(), modelId: "mock-model", messages: [], }, logprobs: undefined, providerMetadata: undefined, experimental_providerMetadata: undefined, }); }); it("should make exactly 5 RawTherapee CLI calls for 3 generations (1 preview + 3 eval + 1 final)", async () => { // Mock evaluation response vi.mocked(generateText) .mockResolvedValueOnce({ text: "<<<<<<< SEARCH\n[Exposure]\nExposure=0.5\n=======\n[Exposure]\nExposure=1.0\n>>>>>>> REPLACE", reasoning: undefined, files: [], reasoningDetails: [], sources: [], experimental_output: undefined, toolCalls: [], toolResults: [], finishReason: "stop", usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, warnings: undefined, steps: [], request: {}, response: { id: "mock-id-1", timestamp: new Date(), modelId: "mock-model", messages: [], }, logprobs: undefined, providerMetadata: undefined, experimental_providerMetadata: undefined, }) .mockResolvedValueOnce({ text: "<<<<<<< SEARCH\n[Exposure]\nExposure=0.5\n=======\n[Exposure]\nExposure=1.2\n>>>>>>> REPLACE", reasoning: undefined, files: [], reasoningDetails: [], sources: [], experimental_output: undefined, toolCalls: [], toolResults: [], finishReason: "stop", usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, warnings: undefined, steps: [], request: {}, response: { id: "mock-id-2", timestamp: new Date(), modelId: "mock-model", messages: [], }, logprobs: undefined, providerMetadata: undefined, experimental_providerMetadata: undefined, }) .mockResolvedValueOnce({ text: "<<<<<<< SEARCH\n[Exposure]\nExposure=0.5\n=======\n[Exposure]\nExposure=0.8\n>>>>>>> REPLACE", reasoning: undefined, files: [], reasoningDetails: [], sources: [], experimental_output: undefined, toolCalls: [], toolResults: [], finishReason: "stop", usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, warnings: undefined, steps: [], request: {}, response: { id: "mock-id-3", timestamp: new Date(), modelId: "mock-model", messages: [], }, logprobs: undefined, providerMetadata: undefined, experimental_providerMetadata: undefined, }) .mockResolvedValueOnce({ text: "BEST_GENERATION: 2", reasoning: undefined, files: [], reasoningDetails: [], sources: [], experimental_output: undefined, toolCalls: [], toolResults: [], finishReason: "stop", usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, warnings: undefined, steps: [], request: {}, response: { id: "mock-id-4", timestamp: new Date(), modelId: "mock-model", messages: [], }, logprobs: undefined, providerMetadata: undefined, experimental_providerMetadata: undefined, }); const result = await generateMultiPP3FromRawImage({ inputPath: TEST_INPUT_FILE, generations: 3, verbose: true, }); // Verify the correct number of RawTherapee CLI calls expect(convertDngToImage).toHaveBeenCalledTimes(1); // 1 preview expect(convertDngToImageWithPP3).toHaveBeenCalledTimes(4); // 3 eval + 1 final // Verify AI calls: 3 generations + 1 evaluation expect(generateText).toHaveBeenCalledTimes(4); // Verify result structure expect(result.bestResult).toBeDefined(); expect(result.allResults).toHaveLength(3); expect(result.finalOutputPath).toBeDefined(); expect(result.finalOutputPath).toContain("_final."); }); it("should use same format/quality for preview and evaluation images", async () => { vi.mocked(generateText) .mockResolvedValueOnce({ text: "<<<<<<< SEARCH\n[Exposure]\nExposure=0.5\n=======\n[Exposure]\nExposure=1.0\n>>>>>>> REPLACE", reasoning: undefined, files: [], reasoningDetails: [], sources: [], experimental_output: undefined, toolCalls: [], toolResults: [], finishReason: "stop", usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, warnings: undefined, steps: [], request: {}, response: { id: "mock-id-5", timestamp: new Date(), modelId: "mock-model", messages: [], }, logprobs: undefined, providerMetadata: undefined, experimental_providerMetadata: undefined, }) .mockResolvedValueOnce({ text: "BEST_GENERATION: 1", reasoning: undefined, files: [], reasoningDetails: [], sources: [], experimental_output: undefined, toolCalls: [], toolResults: [], finishReason: "stop", usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, warnings: undefined, steps: [], request: {}, response: { id: "mock-id-6", timestamp: new Date(), modelId: "mock-model", messages: [], }, logprobs: undefined, providerMetadata: undefined, experimental_providerMetadata: undefined, }); await generateMultiPP3FromRawImage({ inputPath: TEST_INPUT_FILE, generations: 1, previewFormat: "png", previewQuality: 85, outputFormat: "tiff", outputQuality: 100, }); // Verify total calls: 1 eval + 1 final = 2 calls expect(vi.mocked(convertDngToImageWithPP3)).toHaveBeenCalledTimes(2); // Check that evaluation images use preview format/quality const evaluationCalls = vi .mocked(convertDngToImageWithPP3) .mock.calls.filter((call) => call[0].output.includes("_eval.")); expect(evaluationCalls).toHaveLength(1); expect(evaluationCalls[0][0].format).toBe("png"); expect(evaluationCalls[0][0].quality).toBe(85); // Check that final output uses desired format/quality const finalCalls = vi .mocked(convertDngToImageWithPP3) .mock.calls.filter((call) => call[0].output.includes("_final.")); expect(finalCalls).toHaveLength(1); expect(finalCalls[0][0].format).toBe("tiff"); expect(finalCalls[0][0].quality).toBe(100); }); }); //# sourceMappingURL=agent.test.js.map