locker-mcp
Version:
MCP server for file locking and access control for AI code tools
287 lines • 12.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const locker_1 = require("../locker");
describe('Locker', () => {
let tempDir;
let locker;
let testFile;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locker-test-'));
locker = new locker_1.Locker(tempDir);
testFile = path.join(tempDir, 'test.ts');
fs.writeFileSync(testFile, 'console.log("hello world");');
});
afterEach(() => {
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('getFileState', () => {
it('should return unlocked state for untracked file', () => {
const result = locker.getFileState({ filePath: testFile });
expect(result.state).toBe('unlocked');
expect(result.context).toBe('');
expect(result.id).toBe('');
expect(result.history).toEqual([]);
});
it('should return tracked file state', () => {
const context = 'test context';
locker.lockFile({ filePath: testFile, context });
const result = locker.getFileState({ filePath: testFile });
expect(result.state).toBe('locked-context');
expect(result.context).toBe(context);
expect(result.id).toBeDefined();
expect(result.history).toHaveLength(1);
});
});
describe('lockFile', () => {
it('should lock file successfully', () => {
const context = 'test context';
const result = locker.lockFile({ filePath: testFile, context });
expect(result.state).toBe('locked-context');
expect(result.context).toBe(context);
expect(result.id).toBeDefined();
expect(result.history).toHaveLength(1);
// Verify file comment was added
const content = fs.readFileSync(testFile, 'utf-8');
expect(content).toMatch(/^\/\/ LOCKED: .+ STATE=locked-context/);
});
it('should throw error for empty context', () => {
expect(() => {
locker.lockFile({ filePath: testFile, context: '' });
}).toThrow('Context cannot be empty');
expect(() => {
locker.lockFile({ filePath: testFile, context: ' ' });
}).toThrow('Context cannot be empty');
});
it('should throw error for already tracked file', () => {
const context = 'test context';
locker.lockFile({ filePath: testFile, context });
expect(() => {
locker.lockFile({ filePath: testFile, context: 'another context' });
}).toThrow('File is already tracked:');
});
it('should cleanup on failure', () => {
const nonExistentFile = path.join(tempDir, 'non-existent.ts');
expect(() => {
locker.lockFile({ filePath: nonExistentFile, context: 'test' });
}).toThrow();
// Verify no metadata entry was created
const state = locker.getFileState({ filePath: nonExistentFile });
expect(state.state).toBe('unlocked');
});
});
describe('unlockFile', () => {
it('should unlock tracked file', () => {
const context = 'test context';
locker.lockFile({ filePath: testFile, context });
const result = locker.unlockFile({ filePath: testFile });
expect(result.state).toBe('unlocked');
expect(result.context).toBe(context); // Context preserved
expect(result.history).toHaveLength(2); // Original lock + unlock
// Verify lock comment was removed
const content = fs.readFileSync(testFile, 'utf-8');
expect(content).not.toMatch(/^\/\/ LOCKED:/);
});
it('should throw error for untracked file', () => {
expect(() => {
locker.unlockFile({ filePath: testFile });
}).toThrow('File is not tracked:');
});
it('should handle file removal errors gracefully', () => {
const context = 'test context';
locker.lockFile({ filePath: testFile, context });
// Delete the file to simulate removal error
fs.unlinkSync(testFile);
expect(() => {
locker.unlockFile({ filePath: testFile });
}).toThrow('Failed to unlock file:');
});
});
describe('updateFile', () => {
it('should update file context and state', () => {
const initialContext = 'initial context';
locker.lockFile({ filePath: testFile, context: initialContext });
const newContext = 'updated context';
const newState = 'locked';
const result = locker.updateFile({
filePath: testFile,
context: newContext,
state: newState
});
expect(result.state).toBe(newState);
expect(result.context).toBe(newContext);
expect(result.history).toHaveLength(2);
// Verify file comment was updated
const content = fs.readFileSync(testFile, 'utf-8');
expect(content).toMatch(/^\/\/ LOCKED: .+ STATE=locked/);
});
it('should update context without changing state', () => {
const initialContext = 'initial context';
locker.lockFile({ filePath: testFile, context: initialContext });
const newContext = 'updated context';
const result = locker.updateFile({
filePath: testFile,
context: newContext,
state: 'locked-context' // Same state
});
expect(result.context).toBe(newContext);
expect(result.history).toHaveLength(1); // No state change
});
it('should throw error for empty context', () => {
locker.lockFile({ filePath: testFile, context: 'initial' });
expect(() => {
locker.updateFile({
filePath: testFile,
context: '',
state: 'locked'
});
}).toThrow('Context cannot be empty');
});
it('should throw error for untracked file', () => {
expect(() => {
locker.updateFile({
filePath: testFile,
context: 'test',
state: 'locked'
});
}).toThrow('File is not tracked:');
});
it('should handle file update errors gracefully', () => {
const context = 'test context';
locker.lockFile({ filePath: testFile, context });
// Delete the file to simulate update error
fs.unlinkSync(testFile);
expect(() => {
locker.updateFile({
filePath: testFile,
context: 'updated context',
state: 'locked'
});
}).toThrow('Failed to update file:');
});
});
describe('finalizeFile', () => {
it('should finalize locked-context file', () => {
const context = 'test context';
locker.lockFile({ filePath: testFile, context });
const result = locker.finalizeFile(testFile);
expect(result.state).toBe('locked');
expect(result.history).toHaveLength(2);
// Verify file comment was updated
const content = fs.readFileSync(testFile, 'utf-8');
expect(content).toMatch(/^\/\/ LOCKED: .+ STATE=locked/);
});
it('should return same result for already locked file', () => {
const context = 'test context';
locker.lockFile({ filePath: testFile, context });
locker.finalizeFile(testFile);
const result = locker.finalizeFile(testFile);
expect(result.state).toBe('locked');
expect(result.history).toHaveLength(2); // No additional history entry
});
it('should throw error for untracked file', () => {
expect(() => {
locker.finalizeFile(testFile);
}).toThrow('File is not tracked:');
});
it('should handle file finalization errors gracefully', () => {
const context = 'test context';
locker.lockFile({ filePath: testFile, context });
// Delete the file to simulate finalization error
fs.unlinkSync(testFile);
expect(() => {
locker.finalizeFile(testFile);
}).toThrow('Failed to finalize file:');
});
});
describe('getAllTrackedFiles', () => {
it('should return empty array when no files tracked', () => {
const result = locker.getAllTrackedFiles();
expect(result).toEqual([]);
});
it('should return all tracked files', () => {
const files = ['test1.ts', 'test2.ts', 'test3.ts'];
files.forEach((fileName) => {
const filePath = path.join(tempDir, fileName);
fs.writeFileSync(filePath, `console.log("${fileName}");`);
locker.lockFile({ filePath, context: `context for ${fileName}` });
});
const result = locker.getAllTrackedFiles();
expect(result).toHaveLength(3);
expect(result.map(r => path.basename(r.filePath))).toEqual(expect.arrayContaining(files));
});
});
describe('integration scenarios', () => {
it('should handle complete workflow', () => {
const context = 'authentication module';
// Lock file
let result = locker.lockFile({ filePath: testFile, context });
expect(result.state).toBe('locked-context');
// Update context
result = locker.updateFile({
filePath: testFile,
context: 'improved authentication module',
state: 'locked-context'
});
expect(result.context).toBe('improved authentication module');
// Finalize
result = locker.finalizeFile(testFile);
expect(result.state).toBe('locked');
// Check final state
result = locker.getFileState({ filePath: testFile });
expect(result.state).toBe('locked');
expect(result.context).toBe('improved authentication module');
expect(result.history).toHaveLength(2);
});
it('should handle unlock and re-lock workflow', () => {
const context = 'test module';
// Lock file
locker.lockFile({ filePath: testFile, context });
// Unlock file
let result = locker.unlockFile({ filePath: testFile });
expect(result.state).toBe('unlocked');
// Lock again with new context
result = locker.lockFile({ filePath: testFile, context: 'updated module' });
expect(result.state).toBe('locked-context');
expect(result.context).toBe('updated module');
});
});
});
//# sourceMappingURL=locker.test.js.map