mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
* chore(lint): enforce no-global-app via no-restricted-globals; fix all violations
Flip on `no-restricted-globals` with an `app` entry (error) for production
`src/**/*.{ts,tsx}`, exempting test files (they consume the `window.app` mock).
Sweep the whole codebase to 0 violations by threading `app` explicitly:
useApp() in React, this.app / this.chainManager.app in classes, params in plain
modules, a seed pattern for no-arg getInstance() singletons, setters for eager
module-level singletons, and factory functions for app-using built-in tools.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(agent): pass full mock app to initializeBuiltinTools
The initializer's app branch dereferences app.vault.getRoot() via
registerFileTreeTool, so passing the bare vault crashed setup before any
tools registered. Pass the full mock app instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
443 lines
14 KiB
TypeScript
443 lines
14 KiB
TypeScript
import { ContextProcessor } from "@/contextProcessor";
|
|
import { DATAVIEW_BLOCK_TAG } from "@/constants";
|
|
|
|
// Typed helper to access plugins on the mocked global app
|
|
function getPlugins(): Record<string, unknown> {
|
|
return (window.app as unknown as { plugins: { plugins: Record<string, unknown> } }).plugins
|
|
.plugins;
|
|
}
|
|
|
|
function setPlugins(plugins: Record<string, unknown>): void {
|
|
(window.app as unknown as { plugins: { plugins: Record<string, unknown> } }).plugins.plugins =
|
|
plugins;
|
|
}
|
|
|
|
// Mock the global app object for Dataview plugin access
|
|
window.app = {
|
|
plugins: {
|
|
plugins: {},
|
|
},
|
|
} as unknown as typeof window.app;
|
|
|
|
describe("ContextProcessor - Dataview Integration", () => {
|
|
let contextProcessor: ContextProcessor;
|
|
let originalConsoleError: typeof console.error;
|
|
|
|
beforeEach(() => {
|
|
contextProcessor = ContextProcessor.getInstance(window.app);
|
|
// Reset plugins for each test
|
|
setPlugins({});
|
|
// Save and mock console.error to suppress expected error messages
|
|
originalConsoleError = console.error;
|
|
console.error = jest.fn();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore original console.error
|
|
console.error = originalConsoleError;
|
|
});
|
|
|
|
describe("processDataviewBlocks - Plugin Availability", () => {
|
|
it("should return content unchanged when Dataview plugin is not installed", async () => {
|
|
const content = "```dataview\nLIST\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
expect(result).toBe(content);
|
|
});
|
|
|
|
it("should return content unchanged when Dataview API is not available", async () => {
|
|
getPlugins().dataview = {};
|
|
const content = "```dataview\nLIST\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
expect(result).toBe(content);
|
|
});
|
|
});
|
|
|
|
describe("processDataviewBlocks - Regex Pattern Matching", () => {
|
|
beforeEach(() => {
|
|
// Mock successful Dataview plugin with API
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: true,
|
|
value: {
|
|
type: "list",
|
|
values: [{ path: "note1.md" }, { path: "note2.md" }],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
});
|
|
|
|
it("should match dataview blocks with exact newline", async () => {
|
|
const content = "```dataview\nLIST\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
expect(result).toContain(`<${DATAVIEW_BLOCK_TAG}>`);
|
|
expect(result).toContain("<query_type>dataview</query_type>");
|
|
});
|
|
|
|
it("should match dataview blocks with trailing spaces", async () => {
|
|
const content = "```dataview \nLIST\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
expect(result).toContain(`<${DATAVIEW_BLOCK_TAG}>`);
|
|
expect(result).toContain("<query_type>dataview</query_type>");
|
|
});
|
|
|
|
it("should match dataview blocks with tabs", async () => {
|
|
const content = "```dataview\t\nLIST\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
expect(result).toContain(`<${DATAVIEW_BLOCK_TAG}>`);
|
|
expect(result).toContain("<query_type>dataview</query_type>");
|
|
});
|
|
|
|
it("should match dataview blocks with Windows CRLF line endings", async () => {
|
|
const content = "```dataview\r\nLIST\r\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
expect(result).toContain(`<${DATAVIEW_BLOCK_TAG}>`);
|
|
expect(result).toContain("<query_type>dataview</query_type>");
|
|
});
|
|
|
|
it("should match dataviewjs blocks", async () => {
|
|
const content = "```dataviewjs\ndv.list()\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
expect(result).toContain(`<${DATAVIEW_BLOCK_TAG}>`);
|
|
expect(result).toContain("<query_type>dataviewjs</query_type>");
|
|
});
|
|
});
|
|
|
|
describe("processDataviewBlocks - Multiple Blocks", () => {
|
|
beforeEach(() => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
successful: true,
|
|
value: {
|
|
type: "list",
|
|
values: [{ path: "note1.md" }],
|
|
},
|
|
})
|
|
.mockResolvedValueOnce({
|
|
successful: true,
|
|
value: {
|
|
type: "list",
|
|
values: [{ path: "note2.md" }],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
});
|
|
|
|
it("should process multiple different dataview blocks", async () => {
|
|
const content = `
|
|
# First Query
|
|
\`\`\`dataview
|
|
LIST WHERE tag = "#project"
|
|
\`\`\`
|
|
|
|
# Second Query
|
|
\`\`\`dataview
|
|
TABLE file.name
|
|
\`\`\`
|
|
`;
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
// Should contain two dataview block tags
|
|
const blockMatches = result.match(new RegExp(`<${DATAVIEW_BLOCK_TAG}>`, "g"));
|
|
expect(blockMatches).toHaveLength(2);
|
|
});
|
|
|
|
it("should process multiple identical dataview blocks correctly", async () => {
|
|
const content = `
|
|
# First Instance
|
|
\`\`\`dataview
|
|
LIST
|
|
\`\`\`
|
|
|
|
# Second Instance
|
|
\`\`\`dataview
|
|
LIST
|
|
\`\`\`
|
|
`;
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
// Should contain two dataview block tags
|
|
const blockMatches = result.match(new RegExp(`<${DATAVIEW_BLOCK_TAG}>`, "g"));
|
|
expect(blockMatches).toHaveLength(2);
|
|
|
|
// Both should have different results (since mock returns different values)
|
|
expect(result).toContain("[[note1.md]]");
|
|
expect(result).toContain("[[note2.md]]");
|
|
});
|
|
});
|
|
|
|
describe("processDataviewBlocks - Query Execution", () => {
|
|
it("should include both original query and executed results", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: true,
|
|
value: {
|
|
type: "list",
|
|
values: [{ path: "note1.md" }],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
|
|
const content = '```dataview\nLIST WHERE contains(tags, "#project")\n```';
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("<original_query>");
|
|
expect(result).toContain('LIST WHERE contains(tags, "#project")');
|
|
expect(result).toContain("<executed_result>");
|
|
expect(result).toContain("[[note1.md]]");
|
|
});
|
|
|
|
it("should handle query timeout gracefully", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockImplementation(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
// Never resolve to simulate timeout
|
|
window.setTimeout(resolve, 10000);
|
|
})
|
|
),
|
|
},
|
|
};
|
|
|
|
const content = "```dataview\nLIST\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("<error>Query timeout</error>");
|
|
}, 10000); // Increase test timeout to 10s
|
|
|
|
it("should handle query execution errors", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: false,
|
|
error: "Invalid syntax",
|
|
}),
|
|
},
|
|
};
|
|
|
|
const content = "```dataview\nINVALID QUERY\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("<error>Invalid syntax</error>");
|
|
});
|
|
|
|
it("should handle dataviewjs with unsupported message", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn(),
|
|
},
|
|
};
|
|
|
|
const content = "```dataviewjs\ndv.pages()\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("DataviewJS execution not yet supported");
|
|
});
|
|
});
|
|
|
|
describe("processDataviewBlocks - Result Formatting", () => {
|
|
it("should format LIST results correctly", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: true,
|
|
value: {
|
|
type: "list",
|
|
values: [{ path: "note1.md" }, { path: "note2.md" }, { path: "note3.md" }],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
|
|
const content = "```dataview\nLIST\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("- [[note1.md]]");
|
|
expect(result).toContain("- [[note2.md]]");
|
|
expect(result).toContain("- [[note3.md]]");
|
|
});
|
|
|
|
it("should format TABLE results correctly", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: true,
|
|
value: {
|
|
type: "table",
|
|
headers: ["File", "Size"],
|
|
values: [
|
|
[{ path: "note1.md" }, 100],
|
|
[{ path: "note2.md" }, 200],
|
|
],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
|
|
const content = "```dataview\nTABLE file.size\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("| File | Size |");
|
|
expect(result).toContain("| --- | --- |");
|
|
expect(result).toContain("| [[note1.md]] | 100 |");
|
|
expect(result).toContain("| [[note2.md]] | 200 |");
|
|
});
|
|
|
|
it("should format TASK results correctly", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: true,
|
|
value: {
|
|
type: "task",
|
|
values: [
|
|
{ text: "Task 1", completed: false },
|
|
{ text: "Task 2", completed: true },
|
|
],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
|
|
const content = "```dataview\nTASK\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("- [ ] Task 1");
|
|
expect(result).toContain("- [x] Task 2");
|
|
});
|
|
|
|
it("should handle empty results", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: true,
|
|
value: {
|
|
type: "list",
|
|
values: [],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
|
|
const content = "```dataview\nLIST WHERE false\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("<executed_result>\nNo results\n</executed_result>");
|
|
});
|
|
|
|
it("should handle null values gracefully", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: true,
|
|
value: {
|
|
type: "list",
|
|
values: [null, { path: "note1.md" }, undefined],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
|
|
const content = "```dataview\nLIST\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
// Should skip null/undefined values
|
|
expect(result).toContain("- ");
|
|
expect(result).toContain("- [[note1.md]]");
|
|
});
|
|
|
|
it("should handle arrays in values", async () => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: true,
|
|
value: {
|
|
type: "list",
|
|
values: [[{ path: "note1.md" }, { path: "note2.md" }]],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
|
|
const content = "```dataview\nLIST\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("[[note1.md]], [[note2.md]]");
|
|
});
|
|
});
|
|
|
|
describe("processDataviewBlocks - Edge Cases", () => {
|
|
beforeEach(() => {
|
|
getPlugins().dataview = {
|
|
api: {
|
|
query: jest.fn().mockResolvedValue({
|
|
successful: true,
|
|
value: {
|
|
type: "list",
|
|
values: [],
|
|
},
|
|
}),
|
|
},
|
|
};
|
|
});
|
|
|
|
it("should not process non-dataview code blocks", async () => {
|
|
const content = "```javascript\nconst x = 1;\n```";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
expect(result).toBe(content);
|
|
expect(result).not.toContain(`<${DATAVIEW_BLOCK_TAG}>`);
|
|
});
|
|
|
|
it("should handle content with no dataview blocks", async () => {
|
|
const content = "Just some regular markdown content.";
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
expect(result).toBe(content);
|
|
});
|
|
|
|
it("should preserve surrounding content", async () => {
|
|
const content = `
|
|
# Before
|
|
|
|
Some text before
|
|
|
|
\`\`\`dataview
|
|
LIST
|
|
\`\`\`
|
|
|
|
Some text after
|
|
|
|
# After
|
|
`;
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("# Before");
|
|
expect(result).toContain("Some text before");
|
|
expect(result).toContain("Some text after");
|
|
expect(result).toContain("# After");
|
|
expect(result).toContain(`<${DATAVIEW_BLOCK_TAG}>`);
|
|
});
|
|
|
|
it("should handle multiline queries", async () => {
|
|
const content = `\`\`\`dataview
|
|
LIST
|
|
WHERE contains(tags, "#project")
|
|
AND file.mtime > date(today) - dur(7 days)
|
|
SORT file.mtime DESC
|
|
\`\`\``;
|
|
const result = await contextProcessor.processDataviewBlocks(content, "test.md");
|
|
|
|
expect(result).toContain("<original_query>");
|
|
expect(result).toContain('WHERE contains(tags, "#project")');
|
|
expect(result).toContain("AND file.mtime > date(today) - dur(7 days)");
|
|
expect(result).toContain("SORT file.mtime DESC");
|
|
});
|
|
});
|
|
});
|