logancyang_obsidian-copilot/src/contextProcessor.dataview.test.ts
Zero Liu d10aab9861
Enforce no-global-app via ESLint and fix all violations (#2533)
* 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>
2026-05-30 14:52:46 -07:00

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");
});
});
});