349 lines
9.8 KiB
TypeScript
349 lines
9.8 KiB
TypeScript
|
|
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");
|
|||
|
|
});
|
|||
|
|
});
|