- Introduced profile selection modal for creating notes from interview profiles. - Added settings for interview configuration path, auto-starting interviews, and intercepting unresolved link clicks. - Updated package files to include YAML dependency for configuration handling. - Enhanced CSS for profile selection and interview wizard UI elements.
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");
|
||
});
|
||
});
|