mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
* Update bug report template to enhance clarity and requirements - Changed the required checklist item from a screenshot to a log file generated via the "Copilot: Create Log File" command. - Made the screenshot of the note and Copilot chat pane optional. - Clarified the statement regarding bug report requirements for better understanding. * feat: Implement embedded note processing in ContextProcessor - Added support for processing embedded notes within markdown content, allowing for structured `<embedded_note>` blocks. - Introduced new methods for building and formatting embedded note blocks, including error handling for missing notes. - Enhanced logging to replace console statements with appropriate logging functions for better error tracking. - Updated the `buildMarkdownContextContent` method to streamline content processing for markdown files. - Added a new constant `EMBEDDED_NOTE_TAG` to manage embedded note structures.
203 lines
5.8 KiB
TypeScript
203 lines
5.8 KiB
TypeScript
jest.mock("@/chainFactory", () => ({
|
|
ChainType: {
|
|
LLM_CHAIN: "llm_chain",
|
|
COPILOT_PLUS_CHAIN: "copilot_plus",
|
|
PROJECT_CHAIN: "project_chain",
|
|
},
|
|
}));
|
|
|
|
import { ContextProcessor } from "@/contextProcessor";
|
|
import { EMBEDDED_NOTE_TAG } from "@/constants";
|
|
import { ChainType } from "@/chainFactory";
|
|
import { TFile, Vault } from "obsidian";
|
|
|
|
type FileCacheMap = Record<string, any>;
|
|
type FileContentMap = Record<string, string>;
|
|
|
|
const createMockFile = (path: string): TFile => new (TFile as any)(path);
|
|
|
|
describe("ContextProcessor - Embedded Notes", () => {
|
|
let contextProcessor: ContextProcessor;
|
|
let vault: Vault;
|
|
let fileParserManager: any;
|
|
let fileCaches: FileCacheMap;
|
|
let fileContents: FileContentMap;
|
|
let fileIndex: Map<string, TFile>;
|
|
|
|
beforeEach(() => {
|
|
contextProcessor = ContextProcessor.getInstance();
|
|
|
|
fileCaches = {};
|
|
fileContents = {};
|
|
fileIndex = new Map<string, TFile>();
|
|
|
|
const metadataCacheMock = {
|
|
getFirstLinkpathDest: jest.fn((link: string, _sourcePath: string) => {
|
|
const normalized = link.endsWith(".md") ? link : `${link}.md`;
|
|
return fileIndex.get(normalized) ?? null;
|
|
}),
|
|
getFileCache: jest.fn((file: TFile) => fileCaches[file.path] ?? {}),
|
|
};
|
|
|
|
(global as any).app = {
|
|
metadataCache: metadataCacheMock,
|
|
};
|
|
|
|
vault = {
|
|
adapter: {
|
|
stat: jest.fn().mockResolvedValue({ ctime: 0, mtime: 0 }),
|
|
},
|
|
} as unknown as Vault;
|
|
|
|
(vault as any).getAbstractFileByPath = jest.fn();
|
|
|
|
fileParserManager = {
|
|
supportsExtension: jest.fn(
|
|
(extension: string) => extension === "md" || extension === "canvas"
|
|
),
|
|
parseFile: jest.fn(async (file: TFile) => {
|
|
const content = fileContents[file.path];
|
|
if (content === undefined) {
|
|
throw new Error(`Missing mock content for ${file.path}`);
|
|
}
|
|
return content;
|
|
}),
|
|
};
|
|
});
|
|
|
|
const registerFile = (file: TFile, content: string, cache: any = {}): void => {
|
|
fileIndex.set(file.path, file);
|
|
fileContents[file.path] = content;
|
|
fileCaches[file.path] = cache;
|
|
};
|
|
|
|
it("should include embedded note content in the context payload", async () => {
|
|
const source = createMockFile("Source.md");
|
|
const embedded = createMockFile("Embedded.md");
|
|
|
|
registerFile(source, "Introduction\n![[Embedded]]\nConclusion");
|
|
registerFile(embedded, "Embedded note body");
|
|
|
|
const result = await contextProcessor.processContextNotes(
|
|
new Set(),
|
|
fileParserManager,
|
|
vault,
|
|
[source],
|
|
false,
|
|
null,
|
|
ChainType.LLM_CHAIN
|
|
);
|
|
|
|
expect(result).toContain(`<${EMBEDDED_NOTE_TAG}>`);
|
|
expect(result).toContain("Embedded note body");
|
|
});
|
|
|
|
it("should extract a heading section when the embedded note targets a heading", async () => {
|
|
const source = createMockFile("Source.md");
|
|
const embedded = createMockFile("Embedded.md");
|
|
const embeddedContent = "## Section\nImportant details\n\n## Other\nOther details";
|
|
|
|
registerFile(source, "Root\n![[Embedded#Section]]\nTail");
|
|
registerFile(embedded, embeddedContent, {
|
|
headings: [
|
|
{
|
|
heading: "Section",
|
|
level: 2,
|
|
position: { start: { offset: 0 } },
|
|
},
|
|
{
|
|
heading: "Other",
|
|
level: 2,
|
|
position: { start: { offset: embeddedContent.indexOf("## Other") } },
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await contextProcessor.processContextNotes(
|
|
new Set(),
|
|
fileParserManager,
|
|
vault,
|
|
[source],
|
|
false,
|
|
null,
|
|
ChainType.LLM_CHAIN
|
|
);
|
|
|
|
expect(result).toContain("<heading>Section</heading>");
|
|
expect(result).toContain("Important details");
|
|
expect(result).not.toContain("Other details");
|
|
});
|
|
|
|
it("should extract block reference content when embedding a block", async () => {
|
|
const source = createMockFile("Source.md");
|
|
const embedded = createMockFile("Embedded.md");
|
|
const embeddedContent = "Paragraph 1\nParagraph 2 ^block-ref\nParagraph 3\n";
|
|
const blockStart = embeddedContent.indexOf("Paragraph 2");
|
|
const blockEnd = embeddedContent.indexOf("Paragraph 3");
|
|
|
|
registerFile(source, "![[Embedded#^block-ref]]");
|
|
registerFile(embedded, embeddedContent, {
|
|
blocks: {
|
|
"block-ref": {
|
|
position: {
|
|
start: { offset: blockStart },
|
|
end: { offset: blockEnd },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await contextProcessor.processContextNotes(
|
|
new Set(),
|
|
fileParserManager,
|
|
vault,
|
|
[source],
|
|
false,
|
|
null,
|
|
ChainType.LLM_CHAIN
|
|
);
|
|
|
|
expect(result).toContain("<block_id>block-ref</block_id>");
|
|
expect(result).toContain("Paragraph 2 ^block-ref");
|
|
expect(result).not.toContain("Paragraph 3");
|
|
});
|
|
|
|
it("should leave nested embeds untouched for recursive references", async () => {
|
|
const source = createMockFile("Source.md");
|
|
const embedded = createMockFile("Embedded.md");
|
|
|
|
registerFile(source, "Parent\n![[Embedded]]", {});
|
|
registerFile(embedded, "Child\n![[Source]]", {});
|
|
|
|
const result = await contextProcessor.processContextNotes(
|
|
new Set(),
|
|
fileParserManager,
|
|
vault,
|
|
[source],
|
|
false,
|
|
null,
|
|
ChainType.LLM_CHAIN
|
|
);
|
|
|
|
expect(result).toContain("<content>");
|
|
expect(result).toContain("![[Source]]");
|
|
});
|
|
|
|
it("should surface an error when the embedded note cannot be resolved", async () => {
|
|
const source = createMockFile("Source.md");
|
|
|
|
registerFile(source, "Missing\n![[Absent]]", {});
|
|
|
|
const result = await contextProcessor.processContextNotes(
|
|
new Set(),
|
|
fileParserManager,
|
|
vault,
|
|
[source],
|
|
false,
|
|
null,
|
|
ChainType.LLM_CHAIN
|
|
);
|
|
|
|
expect(result).toContain("<error>Embedded note not found</error>");
|
|
});
|
|
});
|