mindnet_obsidian/src/tests/analysis/templateMatching.test.ts
Lars 90ccec5f7d
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run
Implement findings fixing and template matching features in Mindnet plugin
- Added a new command to fix findings in the current section of a markdown file, enhancing user experience by automating issue resolution.
- Introduced settings for configuring actions related to missing notes and headings, allowing for customizable behavior during the fixing process.
- Enhanced the chain inspector to support template matching, providing users with insights into template utilization and potential gaps in their content.
- Updated the analysis report to include detailed metadata about edges and role matches, improving the clarity and usefulness of inspection results.
- Improved error handling and user notifications for fixing findings and template matching processes, ensuring better feedback during execution.
2026-01-18 21:10:33 +01:00

386 lines
12 KiB
TypeScript

/**
* Tests for template matching.
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import type { App, TFile } from "obsidian";
import { matchTemplates } from "../../analysis/templateMatching";
import type { ChainTemplatesConfig, ChainRolesConfig } from "../../dictionary/types";
import type { IndexedEdge } from "../../analysis/graphIndex";
import type { EdgeVocabulary } from "../../vocab/types";
describe("templateMatching", () => {
let mockApp: App;
let mockChainRoles: ChainRolesConfig;
let mockEdgeVocabulary: EdgeVocabulary | null;
beforeEach(() => {
// Mock App
mockApp = {
vault: {
getAbstractFileByPath: vi.fn(),
cachedRead: vi.fn(),
},
metadataCache: {
getFileCache: vi.fn(),
getFirstLinkpathDest: vi.fn(),
},
} as any;
// Mock Chain Roles
mockChainRoles = {
roles: {
causal: {
edge_types: ["causes", "caused_by", "resulted_in"],
},
influences: {
edge_types: ["influences", "influenced_by", "wirkt_auf"],
},
structural: {
edge_types: ["part_of", "contains"],
},
},
};
mockEdgeVocabulary = null;
});
it("should match template with rich format and all slots filled (including 1-hop outgoing neighbors)", async () => {
const templatesConfig: ChainTemplatesConfig = {
templates: [
{
name: "trigger_transformation_outcome",
description: "Test template",
slots: [
{ id: "trigger", allowed_node_types: ["experience"] },
{ id: "transformation", allowed_node_types: ["insight"] },
{ id: "outcome", allowed_node_types: ["decision"] },
],
links: [
{
from: "trigger",
to: "transformation",
allowed_edge_roles: ["causal", "influences"],
},
{
from: "transformation",
to: "outcome",
allowed_edge_roles: ["causal"],
},
],
},
],
};
// Mock edges: A -> B -> C
// A is current context, B is outgoing neighbor, C is 1-hop from B
const allEdges: IndexedEdge[] = [
{
rawEdgeType: "wirkt_auf",
source: { file: "Tests/01_experience_trigger.md", sectionHeading: "Kontext" },
target: { file: "03_insight_transformation", heading: "Kern" },
scope: "section",
evidence: {
file: "Tests/01_experience_trigger.md",
sectionHeading: "Kontext",
lineRange: { start: 5, end: 6 },
},
},
{
rawEdgeType: "resulted_in",
source: { file: "Tests/03_insight_transformation.md", sectionHeading: "Kern" },
target: { file: "04_decision_outcome", heading: "Entscheidung" },
scope: "section",
evidence: {
file: "Tests/03_insight_transformation.md",
sectionHeading: "Kern",
lineRange: { start: 3, end: 4 },
},
},
];
// Mock metadataCache for link resolution (basename -> full path)
// Note: buildCandidateNodes resolves from currentContext.file, not from edge source
(mockApp.metadataCache.getFirstLinkpathDest as any) = vi.fn((link: string, source: string) => {
if (link === "03_insight_transformation" && source === "Tests/01_experience_trigger.md") {
return { path: "Tests/03_insight_transformation.md", extension: "md" } as TFile;
}
if (link === "04_decision_outcome") {
// Can be resolved from any source in Tests/ folder
if (source.startsWith("Tests/")) {
return { path: "Tests/04_decision_outcome.md", extension: "md" } as TFile;
}
}
return null;
});
// Mock file reads
(mockApp.vault.getAbstractFileByPath as any).mockImplementation((path: string) => {
if (path === "Tests/01_experience_trigger.md" ||
path === "Tests/03_insight_transformation.md" ||
path === "Tests/04_decision_outcome.md") {
return { extension: "md", path } as TFile;
}
return null;
});
(mockApp.vault.cachedRead as any).mockImplementation((file: TFile) => {
if (file.path === "Tests/01_experience_trigger.md") {
return Promise.resolve(`---
type: experience
---
## Kontext
`);
}
if (file.path === "Tests/03_insight_transformation.md") {
return Promise.resolve(`---
type: insight
---
## Kern
`);
}
if (file.path === "Tests/04_decision_outcome.md") {
return Promise.resolve(`---
type: decision
---
## Entscheidung
`);
}
return Promise.resolve(`---
type: unknown
---
`);
});
const matches = await matchTemplates(
mockApp,
{ file: "Tests/01_experience_trigger.md", heading: "Kontext" },
allEdges,
templatesConfig,
mockChainRoles,
mockEdgeVocabulary,
{ includeNoteLinks: true, includeCandidates: false }
);
expect(matches.length).toBeGreaterThan(0);
const match = matches[0];
expect(match?.templateName).toBe("trigger_transformation_outcome");
expect(match?.missingSlots).toEqual([]);
expect(match?.slotAssignments["trigger"]?.file).toBe("Tests/01_experience_trigger.md");
expect(match?.slotAssignments["transformation"]?.file).toBe("Tests/03_insight_transformation.md");
expect(match?.slotAssignments["outcome"]?.file).toBe("Tests/04_decision_outcome.md");
expect(match?.satisfiedLinks).toBe(2);
expect(match?.requiredLinks).toBe(2);
});
it("should detect missing slot when edge is missing", async () => {
const templatesConfig: ChainTemplatesConfig = {
templates: [
{
name: "trigger_transformation_outcome",
slots: [
{ id: "trigger", allowed_node_types: ["experience"] },
{ id: "transformation", allowed_node_types: ["insight"] },
{ id: "outcome", allowed_node_types: ["decision"] },
],
links: [
{
from: "trigger",
to: "transformation",
allowed_edge_roles: ["causal"],
},
{
from: "transformation",
to: "outcome",
allowed_edge_roles: ["causal"],
},
],
},
],
};
// Mock edges: A -> B (missing B -> C)
const allEdges: IndexedEdge[] = [
{
rawEdgeType: "causes",
source: { file: "NoteA.md", sectionHeading: "Kontext" },
target: { file: "NoteB.md", heading: null },
scope: "section",
evidence: {
file: "NoteA.md",
sectionHeading: "Kontext",
lineRange: { start: 5, end: 6 },
},
},
];
// Mock metadataCache for link resolution
(mockApp.metadataCache.getFirstLinkpathDest as any) = vi.fn((link: string, source: string) => {
if (link === "03_insight_transformation" && source === "Tests/01_experience_trigger.md") {
return { path: "Tests/03_insight_transformation.md", extension: "md" } as TFile;
}
return null;
});
(mockApp.vault.getAbstractFileByPath as any).mockImplementation((path: string) => {
if (path === "Tests/01_experience_trigger.md" || path === "Tests/03_insight_transformation.md") {
return { extension: "md", path } as TFile;
}
return null;
});
(mockApp.vault.cachedRead as any).mockImplementation((file: TFile) => {
if (file.path === "Tests/01_experience_trigger.md") {
return Promise.resolve(`---
type: experience
---
## Kontext
`);
}
if (file.path === "Tests/03_insight_transformation.md") {
return Promise.resolve(`---
type: insight
---
`);
}
return Promise.resolve(`---
type: unknown
---
`);
});
const matches = await matchTemplates(
mockApp,
{ file: "Tests/01_experience_trigger.md", heading: "Kontext" },
allEdges,
templatesConfig,
mockChainRoles,
mockEdgeVocabulary,
{ includeNoteLinks: true, includeCandidates: false }
);
expect(matches.length).toBeGreaterThan(0);
const match = matches[0];
expect(match?.templateName).toBe("trigger_transformation_outcome");
// Should have missing slots (outcome slot cannot be filled)
expect(match?.missingSlots.length).toBeGreaterThan(0);
});
it("should produce deterministic results regardless of edge order", async () => {
const templatesConfig: ChainTemplatesConfig = {
templates: [
{
name: "simple_chain",
slots: [
{ id: "start", allowed_node_types: ["experience"] },
{ id: "end", allowed_node_types: ["decision"] },
],
links: [
{
from: "start",
to: "end",
allowed_edge_roles: ["causal"],
},
],
},
],
};
// Create edges in different orders
const edges1: IndexedEdge[] = [
{
rawEdgeType: "causes",
source: { file: "Tests/01_experience_trigger.md", sectionHeading: "Kontext" },
target: { file: "02_decision_outcome", heading: null },
scope: "section",
evidence: {
file: "Tests/01_experience_trigger.md",
sectionHeading: "Kontext",
lineRange: { start: 5, end: 6 },
},
},
];
const edges2: IndexedEdge[] = [
{
rawEdgeType: "causes",
source: { file: "Tests/01_experience_trigger.md", sectionHeading: "Kontext" },
target: { file: "02_decision_outcome", heading: null },
scope: "section",
evidence: {
file: "Tests/01_experience_trigger.md",
sectionHeading: "Kontext",
lineRange: { start: 5, end: 6 },
},
},
];
// Mock metadataCache for link resolution
(mockApp.metadataCache.getFirstLinkpathDest as any) = vi.fn((link: string, source: string) => {
if (link === "02_decision_outcome" && source === "Tests/01_experience_trigger.md") {
return { path: "Tests/02_decision_outcome.md", extension: "md" } as TFile;
}
return null;
});
(mockApp.vault.getAbstractFileByPath as any).mockImplementation((path: string) => {
if (path === "Tests/01_experience_trigger.md" || path === "Tests/02_decision_outcome.md") {
return { extension: "md", path } as TFile;
}
return null;
});
(mockApp.vault.cachedRead as any).mockImplementation((file: TFile) => {
if (file.path === "Tests/01_experience_trigger.md") {
return Promise.resolve(`---
type: experience
---
## Kontext
`);
}
if (file.path === "Tests/02_decision_outcome.md") {
return Promise.resolve(`---
type: decision
---
`);
}
return Promise.resolve(`---
type: unknown
---
`);
});
const matches1 = await matchTemplates(
mockApp,
{ file: "Tests/01_experience_trigger.md", heading: "Kontext" },
edges1,
templatesConfig,
mockChainRoles,
mockEdgeVocabulary,
{ includeNoteLinks: true, includeCandidates: false }
);
const matches2 = await matchTemplates(
mockApp,
{ file: "Tests/01_experience_trigger.md", heading: "Kontext" },
edges2,
templatesConfig,
mockChainRoles,
mockEdgeVocabulary,
{ includeNoteLinks: true, includeCandidates: false }
);
expect(matches1.length).toBe(matches2.length);
if (matches1.length > 0 && matches2.length > 0) {
expect(matches1[0]?.templateName).toBe(matches2[0]?.templateName);
expect(matches1[0]?.score).toBe(matches2[0]?.score);
expect(matches1[0]?.missingSlots).toEqual(matches2[0]?.missingSlots);
}
});
});