/** * Tests for template matching profiles (discovery vs decisioning) using real configuration files. */ import { describe, it, expect } from "vitest"; import { matchTemplates } from "../../analysis/templateMatching"; import { createVaultAppFromFixtures } from "../helpers/vaultHelper"; import { loadChainRolesFromFixtures, loadChainTemplatesFromFixtures } from "../helpers/configHelper"; import { buildNoteIndex } from "../../analysis/graphIndex"; import type { TFile } from "obsidian"; import type { IndexedEdge } from "../../analysis/graphIndex"; import type { EdgeVocabulary } from "../../vocab/types"; import type { ChainTemplatesConfig, TemplateMatchingProfile } from "../../dictionary/types"; describe("templateMatching profiles", () => { const mockEdgeVocabulary: EdgeVocabulary = { byCanonical: new Map(), aliasToCanonical: new Map(), }; it("should emit missing_slot findings with discovery profile (low thresholds)", async () => { const app = createVaultAppFromFixtures(); const chainRoles = loadChainRolesFromFixtures(); const baseTemplates = loadChainTemplatesFromFixtures(); if (!chainRoles || !baseTemplates) { console.warn("Skipping test: real config files not found in fixtures"); return; } // Create templates config with discovery profile const templatesConfig: ChainTemplatesConfig = { ...baseTemplates, defaults: { ...baseTemplates.defaults, profiles: { discovery: { required_links: false, min_slots_filled_for_gap_findings: 1, min_score_for_gap_findings: 0, }, }, }, }; // Load only trigger note (missing transformation -> outcome edge) const currentFile = app.vault.getAbstractFileByPath("Tests/01_experience_trigger.md"); if (!currentFile || !("extension" in currentFile) || currentFile.extension !== "md") { throw new Error("Test file not found"); } const { edges: currentEdges } = await buildNoteIndex(app, currentFile as TFile); // Only include current edges (missing outcome) const allEdges: IndexedEdge[] = [...currentEdges]; const discoveryProfile: TemplateMatchingProfile = { required_links: false, min_slots_filled_for_gap_findings: 1, min_score_for_gap_findings: 0, }; const matches = await matchTemplates( app, { file: "Tests/01_experience_trigger.md", heading: "Kontext" }, allEdges, templatesConfig, chainRoles, mockEdgeVocabulary, { includeNoteLinks: true, includeCandidates: false }, discoveryProfile ); expect(matches.length).toBeGreaterThan(0); const match = matches[0]; if (!match) return; expect(match.templateName).toBe("trigger_transformation_outcome"); // With discovery profile (low thresholds), should detect missing slots if (match.missingSlots.length > 0) { const slotsFilled = Object.keys(match.slotAssignments).length; expect(slotsFilled).toBeGreaterThanOrEqual(1); // At least trigger slot filled expect(match.score).toBeGreaterThanOrEqual(0); // Score meets threshold } }); it("should NOT emit missing_slot findings with decisioning profile (high thresholds)", async () => { const app = createVaultAppFromFixtures(); const chainRoles = loadChainRolesFromFixtures(); const baseTemplates = loadChainTemplatesFromFixtures(); if (!chainRoles || !baseTemplates) { console.warn("Skipping test: real config files not found in fixtures"); return; } // Create templates config with decisioning profile const templatesConfig: ChainTemplatesConfig = { ...baseTemplates, defaults: { ...baseTemplates.defaults, profiles: { decisioning: { required_links: true, min_slots_filled_for_gap_findings: 3, min_score_for_gap_findings: 20, }, }, }, }; // Load only trigger note (missing transformation -> outcome edge) const currentFile = app.vault.getAbstractFileByPath("Tests/01_experience_trigger.md"); if (!currentFile || !("extension" in currentFile) || currentFile.extension !== "md") { throw new Error("Test file not found"); } const { edges: currentEdges } = await buildNoteIndex(app, currentFile as TFile); const allEdges: IndexedEdge[] = [...currentEdges]; const decisioningProfile: TemplateMatchingProfile = { required_links: true, min_slots_filled_for_gap_findings: 3, min_score_for_gap_findings: 20, }; const matches = await matchTemplates( app, { file: "Tests/01_experience_trigger.md", heading: "Kontext" }, allEdges, templatesConfig, chainRoles, mockEdgeVocabulary, { includeNoteLinks: true, includeCandidates: false }, decisioningProfile ); expect(matches.length).toBeGreaterThan(0); const match = matches[0]; if (!match) return; // With decisioning profile (high thresholds), partial matches may not meet thresholds // This is expected behavior - the test verifies the profile is applied expect(match.templateName).toBe("trigger_transformation_outcome"); }); it("should not penalize score for missing links when required_links=false", async () => { const app = createVaultAppFromFixtures(); const chainRoles = loadChainRolesFromFixtures(); const baseTemplates = loadChainTemplatesFromFixtures(); if (!chainRoles || !baseTemplates) { console.warn("Skipping test: real config files not found in fixtures"); return; } const templatesConfig: ChainTemplatesConfig = { ...baseTemplates, defaults: { ...baseTemplates.defaults, profiles: { discovery: { required_links: false, min_slots_filled_for_gap_findings: 1, min_score_for_gap_findings: 0, }, }, }, }; const currentFile = app.vault.getAbstractFileByPath("Tests/01_experience_trigger.md"); if (!currentFile || !("extension" in currentFile) || currentFile.extension !== "md") { throw new Error("Test file not found"); } const { edges: currentEdges } = await buildNoteIndex(app, currentFile as TFile); const allEdges: IndexedEdge[] = [...currentEdges]; const profile: TemplateMatchingProfile = { required_links: false, }; const matches = await matchTemplates( app, { file: "Tests/01_experience_trigger.md", heading: "Kontext" }, allEdges, templatesConfig, chainRoles, mockEdgeVocabulary, { includeNoteLinks: true, includeCandidates: false }, profile ); if (matches.length > 0) { const match = matches[0]; if (!match) return; // With required_links=false, missing links should not heavily penalize score // Score should be based on slots filled, not missing links expect(match.score).toBeGreaterThan(-10); // Not heavily penalized } }); it("should penalize score for missing links when required_links=true", async () => { const app = createVaultAppFromFixtures(); const chainRoles = loadChainRolesFromFixtures(); const baseTemplates = loadChainTemplatesFromFixtures(); if (!chainRoles || !baseTemplates) { console.warn("Skipping test: real config files not found in fixtures"); return; } const templatesConfig: ChainTemplatesConfig = { ...baseTemplates, defaults: { ...baseTemplates.defaults, profiles: { decisioning: { required_links: true, min_slots_filled_for_gap_findings: 3, min_score_for_gap_findings: 20, }, }, }, }; const currentFile = app.vault.getAbstractFileByPath("Tests/01_experience_trigger.md"); if (!currentFile || !("extension" in currentFile) || currentFile.extension !== "md") { throw new Error("Test file not found"); } const { edges: currentEdges } = await buildNoteIndex(app, currentFile as TFile); const allEdges: IndexedEdge[] = [...currentEdges]; const profile: TemplateMatchingProfile = { required_links: true, }; const matches = await matchTemplates( app, { file: "Tests/01_experience_trigger.md", heading: "Kontext" }, allEdges, templatesConfig, chainRoles, mockEdgeVocabulary, { includeNoteLinks: true, includeCandidates: false }, profile ); if (matches.length > 0) { const match = matches[0]; if (!match) return; // With required_links=true, missing links should penalize score (-5 per missing link) if (match.requiredLinks > match.satisfiedLinks) { // Score should reflect penalty for missing required links expect(match.score).toBeLessThan(10); // Penalized } } }); });