2d78882594
Update the plugin entrypoint and TypeScript output directory to use dist instead of writing generated JavaScript into src. Remove previously checked-in compiled source files and add coverage for the Ollama client and tool executor.
1139 lines
57 KiB
JavaScript
1139 lines
57 KiB
JavaScript
"use strict";
|
|
var __extends = (this && this.__extends) || (function () {
|
|
var extendStatics = function (d, b) {
|
|
extendStatics = Object.setPrototypeOf ||
|
|
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
return extendStatics(d, b);
|
|
};
|
|
return function (d, b) {
|
|
if (typeof b !== "function" && b !== null)
|
|
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
extendStatics(d, b);
|
|
function __() { this.constructor = d; }
|
|
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
};
|
|
})();
|
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
function step(op) {
|
|
if (f) throw new TypeError("Generator is already executing.");
|
|
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
switch (op[0]) {
|
|
case 0: case 1: t = op; break;
|
|
case 4: _.label++; return { value: op[1], done: false };
|
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
default:
|
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
if (t[2]) _.ops.pop();
|
|
_.trys.pop(); continue;
|
|
}
|
|
op = body.call(thisArg, _);
|
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
}
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
var tool_executor_1 = require("../src/tool-executor");
|
|
var obsidian_1 = require("obsidian");
|
|
// Mock Obsidian module
|
|
jest.mock('obsidian', function () {
|
|
var TFile = /** @class */ (function () {
|
|
function TFile() {
|
|
}
|
|
return TFile;
|
|
}());
|
|
return {
|
|
Vault: jest.fn(),
|
|
App: jest.fn(),
|
|
Notice: jest.fn(),
|
|
TFile: TFile,
|
|
};
|
|
});
|
|
// Mock ErrorHandler
|
|
jest.mock('../src/error-handler', function () { return ({
|
|
ErrorHandler: {
|
|
handleError: jest.fn(),
|
|
},
|
|
}); });
|
|
describe('ToolExecutor', function () {
|
|
var executor;
|
|
var mockVault;
|
|
var mockApp;
|
|
beforeEach(function () {
|
|
mockVault = {
|
|
create: jest.fn().mockResolvedValue(null),
|
|
getAbstractFileByPath: jest.fn(),
|
|
cachedRead: jest.fn().mockResolvedValue(''),
|
|
getMarkdownFiles: jest.fn().mockReturnValue([]),
|
|
};
|
|
mockApp = {};
|
|
executor = new tool_executor_1.ToolExecutor(mockVault, mockApp);
|
|
jest.clearAllMocks();
|
|
});
|
|
describe('handleToolCall', function () {
|
|
describe('create_file tool', function () {
|
|
it('should successfully create a file with valid arguments', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_1',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('test-file.md', 'Test content');
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should handle object arguments directly', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_2',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: {
|
|
path: 'obj-args-file.md',
|
|
content: 'Object args content',
|
|
},
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('obj-args-file.md', 'Object args content');
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should successfully create a file in a subdirectory', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_3',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'subdirectory/test-file.md',
|
|
content: 'Subdir content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('subdirectory/test-file.md', 'Subdir content');
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should handle multiple slashes gracefully by normalizing path', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_4',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test//file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('test//file.md', 'Test content');
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should handle empty content gracefully', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_5',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'empty-file.md',
|
|
content: '',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('empty-file.md', '');
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should allow filenames with consecutive dots', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_6',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'project..notes.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('project..notes.md', 'Test content');
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path traversal attempts with ..', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_7',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '../test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path traversal attempts with .\\', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_8',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '.\\test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path traversal attempts with /..', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_9',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '/../test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject absolute paths starting with /', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_10',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '/var/test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject absolute paths starting with \\', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_11',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '\\var\\test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject Windows drive letters', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_12',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'C:\\test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject empty path', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_13',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject undefined path', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_14',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path with invalid characters <', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_15',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test<file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path with invalid characters >', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_16',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test>file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path with invalid characters :', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_17',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test:file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path with invalid characters |', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_18',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test|file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path with invalid characters ?', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_19',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test?file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path with invalid characters *', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_20',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test*file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path longer than 200 characters', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_21',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'a'.repeat(201) + '.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path with ~ character', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_22',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test~file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject non-string content', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_23',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test-file.md',
|
|
content: 123,
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject non-string path', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_24',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 123,
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should handle vault.create rejection gracefully', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
mockVault.create = jest.fn().mockRejectedValue(new Error('Permission denied'));
|
|
call = {
|
|
id: 'call_25',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should handle invalid JSON in arguments', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_26',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: 'invalid json',
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
});
|
|
describe('read_vault_file tool', function () {
|
|
it('should successfully read an existing file', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var mockFile, MockTFile, call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
mockFile = {
|
|
path: 'test-file.md',
|
|
basename: 'test-file.md',
|
|
};
|
|
MockTFile = /** @class */ (function (_super) {
|
|
__extends(MockTFile, _super);
|
|
function MockTFile(path) {
|
|
var _this = _super.call(this) || this;
|
|
_this.path = path;
|
|
_this.basename = path.split('/').pop() || path;
|
|
_this.extension = _this.basename.split('.').pop() || '';
|
|
return _this;
|
|
}
|
|
return MockTFile;
|
|
}(obsidian_1.TFile));
|
|
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(new MockTFile('test-file.md'));
|
|
mockVault.cachedRead = jest.fn().mockResolvedValue('File content');
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([]);
|
|
call = {
|
|
id: 'call_28',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test-file.md',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toBe('File read successfully');
|
|
expect(result.data).toEqual({ path: 'test-file.md', content: 'File content' });
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject path traversal attempts', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_29',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: '../test-file.md',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject invalid characters in path', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_30',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test<file.md',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow()];
|
|
case 1:
|
|
_a.sent();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should throw error when file not found', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
// Return null to simulate file not found
|
|
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
|
|
mockVault.cachedRead = jest.fn().mockResolvedValue('');
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([]);
|
|
call = {
|
|
id: 'call_31',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: 'nonexistent.md',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow('File not found: nonexistent.md')];
|
|
case 1:
|
|
_a.sent();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject non-string path', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_32',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: 123,
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow('Path must be a string')];
|
|
case 1:
|
|
_a.sent();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
});
|
|
describe('search_vault_files tool', function () {
|
|
it('should successfully search vault files', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var mockFiles, call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
mockFiles = [
|
|
{ path: 'file1.md', basename: 'file1.md' },
|
|
{ path: 'file2.md', basename: 'file2.md' },
|
|
];
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([
|
|
{ path: 'query.md', basename: 'query.md' },
|
|
{ path: 'other.md', basename: 'other.md' },
|
|
{ path: 'query2.md', basename: 'query2.md' },
|
|
{ path: 'unrelated.md', basename: 'unrelated.md' },
|
|
]);
|
|
call = {
|
|
id: 'call_33',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'query',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toContain('Found 2 matching files');
|
|
expect(result.data).toHaveLength(2);
|
|
expect(result.data).toContainEqual({ path: 'query.md', basename: 'query.md' });
|
|
expect(result.data).toContainEqual({ path: 'query2.md', basename: 'query2.md' });
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should limit results based on limit parameter', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([
|
|
{ path: 'result1.md', basename: 'result1.md' },
|
|
{ path: 'result2.md', basename: 'result2.md' },
|
|
{ path: 'result3.md', basename: 'result3.md' },
|
|
{ path: 'result4.md', basename: 'result4.md' },
|
|
{ path: 'result5.md', basename: 'result5.md' },
|
|
]);
|
|
call = {
|
|
id: 'call_34',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'result',
|
|
limit: 3,
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.data).toHaveLength(3);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should use default limit of 10 when no limit specified', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var mockFiles, call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
mockFiles = Array.from({ length: 15 }, function (_, i) { return ({
|
|
path: "match".concat(i, ".md"),
|
|
basename: "match".concat(i, ".md"),
|
|
}); });
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue(mockFiles);
|
|
call = {
|
|
id: 'call_35',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'match',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.data).toHaveLength(10);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should handle case-insensitive search', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([
|
|
{ path: 'QUERY.md', basename: 'QUERY.md' },
|
|
{ path: 'Query.md', basename: 'Query.md' },
|
|
{ path: 'query.md', basename: 'query.md' },
|
|
{ path: 'other.md', basename: 'other.md' },
|
|
]);
|
|
call = {
|
|
id: 'call_36',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'query',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.data).toHaveLength(3);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should reject non-string query', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_37',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 123,
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, expect(executor.handleToolCall(call)).rejects.toThrow('Query must be a string')];
|
|
case 1:
|
|
_a.sent();
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should return empty array when no matches found', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([
|
|
{ path: 'unrelated1.md', basename: 'unrelated1.md' },
|
|
{ path: 'unrelated2.md', basename: 'unrelated2.md' },
|
|
]);
|
|
call = {
|
|
id: 'call_38',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'nomatches',
|
|
}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toHaveLength(0);
|
|
expect(result.message).toContain('Found 0 matching files');
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
});
|
|
describe('executeTool method', function () {
|
|
it('should execute create_file tool successfully', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0: return [4 /*yield*/, executor.executeTool('create_file', {
|
|
path: 'test-file.md',
|
|
content: 'Test content',
|
|
})];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.success).toBe(true);
|
|
expect(mockVault.create).toHaveBeenCalledWith('test-file.md', 'Test content');
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should execute read_vault_file tool successfully', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var MockTFile, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
MockTFile = /** @class */ (function (_super) {
|
|
__extends(MockTFile, _super);
|
|
function MockTFile(path) {
|
|
var _this = _super.call(this) || this;
|
|
_this.path = path;
|
|
_this.basename = path.split('/').pop() || path;
|
|
_this.extension = _this.basename.split('.').pop() || '';
|
|
return _this;
|
|
}
|
|
return MockTFile;
|
|
}(obsidian_1.TFile));
|
|
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(new MockTFile('test-file.md'));
|
|
mockVault.cachedRead = jest.fn().mockResolvedValue('File content');
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([]);
|
|
return [4 /*yield*/, executor.executeTool('read_vault_file', {
|
|
path: 'test-file.md',
|
|
})];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.success).toBe(true);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should execute search_vault_files tool successfully', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
mockVault.getMarkdownFiles = jest
|
|
.fn()
|
|
.mockReturnValue([{ path: 'match.md', basename: 'match.md' }]);
|
|
return [4 /*yield*/, executor.executeTool('search_vault_files', {
|
|
query: 'match',
|
|
})];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toHaveLength(1);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
it('should handle unknown tool', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0: return [4 /*yield*/, executor.executeTool('unknown_tool', {})];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain('Unknown tool: unknown_tool');
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
});
|
|
describe('unknown tool', function () {
|
|
it('should return failure for unknown tool', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
var call, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
call = {
|
|
id: 'call_39',
|
|
type: 'function',
|
|
function: {
|
|
name: 'unknown_tool',
|
|
arguments: JSON.stringify({}),
|
|
},
|
|
};
|
|
return [4 /*yield*/, executor.handleToolCall(call)];
|
|
case 1:
|
|
result = _a.sent();
|
|
expect(result).toEqual({ success: false, message: 'Unknown tool: unknown_tool' });
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
}); });
|
|
});
|
|
});
|
|
});
|