import { describe, it, expect } from "vitest"; import { parseInterviewConfig, validateConfig } from "../../interview/parseInterviewConfig"; import type { InterviewConfig } from "../../interview/types"; describe("parseInterviewConfig", () => { const fixtureYaml = ` version: "2.0" frontmatterWhitelist: - title - id - date - tags profiles: - key: profile1 label: "Test Profile 1" note_type: "note" group: "test" defaults: template: "default" steps: - type: loop key: items_loop label: "Items Loop" items: - type: capture_frontmatter key: title_field label: "Title" field: title required: true - type: capture_text key: content_field label: "Content" required: false - type: llm_dialog key: llm_step label: "LLM Dialog" prompt: "What is the main topic?" system_prompt: "You are a helpful assistant." model: "gpt-4" temperature: 0.7 max_tokens: 500 - key: profile2 label: "Test Profile 2" note_type: "interview" steps: - type: capture_frontmatter key: date_field field: date required: false - type: llm_dialog key: analysis prompt: "Analyze the content" `; it("parses YAML correctly", () => { const result = parseInterviewConfig(fixtureYaml); expect(result.errors.length).toBe(0); expect(result.config.version).toBe("2.0"); expect(result.config.frontmatterWhitelist).toEqual(["title", "id", "date", "tags"]); expect(result.config.profiles.length).toBe(2); }); it("parses profile structure correctly", () => { const result = parseInterviewConfig(fixtureYaml); const profile1 = result.config.profiles[0]; if (!profile1) throw new Error("Expected profile1"); expect(profile1.key).toBe("profile1"); expect(profile1.label).toBe("Test Profile 1"); expect(profile1.note_type).toBe("note"); expect(profile1.group).toBe("test"); expect(profile1.defaults).toEqual({ template: "default" }); expect(profile1.steps.length).toBe(2); }); it("parses loop step correctly", () => { const result = parseInterviewConfig(fixtureYaml); const profile1 = result.config.profiles[0]; if (!profile1) throw new Error("Expected profile1"); const loopStep = profile1.steps[0]; if (!loopStep || loopStep.type !== "loop") { throw new Error("Expected loop step"); } expect(loopStep.key).toBe("items_loop"); expect(loopStep.label).toBe("Items Loop"); expect(loopStep.items.length).toBe(2); const firstItem = loopStep.items[0]; if (!firstItem || firstItem.type !== "capture_frontmatter") { throw new Error("Expected capture_frontmatter step"); } expect(firstItem.key).toBe("title_field"); expect(firstItem.field).toBe("title"); expect(firstItem.required).toBe(true); }); it("parses llm_dialog step correctly", () => { const result = parseInterviewConfig(fixtureYaml); const profile1 = result.config.profiles[0]; if (!profile1) throw new Error("Expected profile1"); const llmStep = profile1.steps[1]; if (!llmStep || llmStep.type !== "llm_dialog") { throw new Error("Expected llm_dialog step"); } expect(llmStep.key).toBe("llm_step"); expect(llmStep.prompt).toBe("What is the main topic?"); expect(llmStep.system_prompt).toBe("You are a helpful assistant."); expect(llmStep.model).toBe("gpt-4"); expect(llmStep.temperature).toBe(0.7); expect(llmStep.max_tokens).toBe(500); }); it("parses capture_frontmatter step correctly", () => { const result = parseInterviewConfig(fixtureYaml); const profile2 = result.config.profiles[1]; if (!profile2) throw new Error("Expected profile2"); const captureStep = profile2.steps[0]; if (!captureStep || captureStep.type !== "capture_frontmatter") { throw new Error("Expected capture_frontmatter step"); } expect(captureStep.key).toBe("date_field"); expect(captureStep.field).toBe("date"); expect(captureStep.required).toBe(false); }); it("parses capture_text step correctly", () => { const result = parseInterviewConfig(fixtureYaml); const profile1 = result.config.profiles[0]; if (!profile1) throw new Error("Expected profile1"); const loopStep = profile1.steps[0]; if (!loopStep || loopStep.type !== "loop") { throw new Error("Expected loop step"); } const captureTextStep = loopStep.items[1]; if (!captureTextStep || captureTextStep.type !== "capture_text") { throw new Error("Expected capture_text step"); } expect(captureTextStep.key).toBe("content_field"); expect(captureTextStep.required).toBe(false); }); it("validates unique profile keys", () => { const yamlWithDuplicate = ` version: "2.0" frontmatterWhitelist: [] profiles: - key: profile1 label: "Profile 1" note_type: "note" steps: [] - key: profile1 label: "Profile 1 Duplicate" note_type: "note" steps: [] `; const result = parseInterviewConfig(yamlWithDuplicate); const validationErrors = validateConfig(result.config); expect(validationErrors.some(e => e.includes("Duplicate profile key"))).toBe(true); }); it("validates capture_frontmatter.field in whitelist", () => { const yamlWithInvalidField = ` version: "2.0" frontmatterWhitelist: - title - id profiles: - key: profile1 label: "Profile 1" note_type: "note" steps: - type: capture_frontmatter key: invalid_field field: invalid_field_name `; const result = parseInterviewConfig(yamlWithInvalidField); const validationErrors = validateConfig(result.config); expect( validationErrors.some(e => e.includes("capture_frontmatter field 'invalid_field_name' not in frontmatterWhitelist") ) ).toBe(true); }); it("validates capture_frontmatter.field in nested loop", () => { const yamlWithNestedInvalidField = ` version: "2.0" frontmatterWhitelist: - title profiles: - key: profile1 label: "Profile 1" note_type: "note" steps: - type: loop key: items items: - type: capture_frontmatter key: invalid field: invalid_field `; const result = parseInterviewConfig(yamlWithNestedInvalidField); const validationErrors = validateConfig(result.config); expect( validationErrors.some(e => e.includes("capture_frontmatter field 'invalid_field' not in frontmatterWhitelist") ) ).toBe(true); }); it("handles invalid YAML gracefully", () => { const invalidYaml = "invalid: yaml: content: ["; const result = parseInterviewConfig(invalidYaml); expect(result.errors.length).toBeGreaterThan(0); expect(result.config.profiles.length).toBe(0); }); it("handles missing required fields", () => { const incompleteYaml = ` version: "2.0" frontmatterWhitelist: [] profiles: - key: profile1 label: "Profile 1" # missing note_type steps: [] `; const result = parseInterviewConfig(incompleteYaml); // Profile should be skipped due to missing note_type expect(result.config.profiles.length).toBe(0); expect(result.errors.length).toBeGreaterThan(0); }); it("parses YAML with 'kind' and 'id' fields (real-world format)", () => { const yamlWithKindAndId = ` version: 2 frontmatter_whitelist: - id - title - type profiles: - key: experience_hub group: experience label: "Experience – Hub" note_type: experience defaults: status: active steps: - id: title kind: capture_frontmatter field: title label: "Hub Titel" required: true - id: items kind: loop label: "Erlebnisse" steps: - id: item_text kind: capture_text label: "Erlebnis" required: true - id: review kind: review label: "Review & Apply" `; const result = parseInterviewConfig(yamlWithKindAndId); // Log errors for debugging if (result.errors.length > 0) { console.log("Parse errors:", result.errors); } expect(result.errors.length).toBe(0); expect(result.config.profiles.length).toBe(1); const profile = result.config.profiles[0]; if (!profile) throw new Error("Expected profile"); expect(profile.key).toBe("experience_hub"); expect(profile.label).toBe("Experience – Hub"); expect(profile.note_type).toBe("experience"); expect(profile.group).toBe("experience"); expect(profile.steps.length).toBe(3); // First step: capture_frontmatter const titleStep = profile.steps[0]; if (!titleStep || titleStep.type !== "capture_frontmatter") { throw new Error("Expected capture_frontmatter step"); } expect(titleStep.key).toBe("title"); expect(titleStep.field).toBe("title"); expect(titleStep.label).toBe("Hub Titel"); expect(titleStep.required).toBe(true); // Second step: loop const loopStep = profile.steps[1]; if (!loopStep || loopStep.type !== "loop") { throw new Error("Expected loop step"); } expect(loopStep.key).toBe("items"); expect(loopStep.label).toBe("Erlebnisse"); expect(loopStep.items.length).toBe(1); const nestedStep = loopStep.items[0]; if (!nestedStep || nestedStep.type !== "capture_text") { throw new Error("Expected capture_text step in loop"); } expect(nestedStep.key).toBe("item_text"); expect(nestedStep.label).toBe("Erlebnis"); expect(nestedStep.required).toBe(true); // Third step: review const reviewStep = profile.steps[2]; if (!reviewStep || reviewStep.type !== "review") { throw new Error("Expected review step"); } expect(reviewStep.key).toBe("review"); expect(reviewStep.label).toBe("Review & Apply"); }); });