@andend-collective/locker
Version:
Advanced file locking and context management for AI coding assistants using Model Context Protocol
380 lines • 17.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');
});
});
describe('relative path support', () => {
it('should work with relative paths', () => {
// Create a file with a simple name in the project root
const relativeFileName = 'relative-test.ts';
const fullPath = path.join(tempDir, relativeFileName);
fs.writeFileSync(fullPath, 'console.log("relative test");');
const context = 'relative file test';
// Lock using relative path
const result = locker.lockFile({ filePath: relativeFileName, context });
expect(result.state).toBe('locked-context');
expect(result.context).toBe(context);
// Verify the actual file was modified
const content = fs.readFileSync(fullPath, 'utf-8');
expect(content).toMatch(/^\/\/ LOCKED: .+ STATE=locked-context/);
});
it('should handle nested relative paths', () => {
// Create nested directories
const subDir = path.join(tempDir, 'src', 'components');
fs.mkdirSync(subDir, { recursive: true });
const nestedFileName = 'src/components/Button.tsx';
const fullPath = path.join(tempDir, nestedFileName);
fs.writeFileSync(fullPath, 'export const Button = () => <button />;');
const context = 'Button component';
// Lock using nested relative path
const result = locker.lockFile({ filePath: nestedFileName, context });
expect(result.state).toBe('locked-context');
expect(result.context).toBe(context);
// Verify file state can be retrieved with same relative path
const state = locker.getFileState({ filePath: nestedFileName });
expect(state.state).toBe('locked-context');
expect(state.context).toBe(context);
});
it('should work with all operations using relative paths', () => {
const relativeFileName = 'workflow-test.ts';
const fullPath = path.join(tempDir, relativeFileName);
fs.writeFileSync(fullPath, 'console.log("workflow test");');
// Lock with relative path
let result = locker.lockFile({ filePath: relativeFileName, context: 'initial' });
expect(result.state).toBe('locked-context');
// Update with relative path
result = locker.updateFile({
filePath: relativeFileName,
context: 'updated context',
state: 'locked-context'
});
expect(result.context).toBe('updated context');
// Finalize with relative path
result = locker.finalizeFile(relativeFileName);
expect(result.state).toBe('locked');
// Get state with relative path
result = locker.getFileState({ filePath: relativeFileName });
expect(result.state).toBe('locked');
expect(result.context).toBe('updated context');
// Unlock with relative path
result = locker.unlockFile({ filePath: relativeFileName });
expect(result.state).toBe('unlocked');
});
it('should fail for non-existent relative paths', () => {
expect(() => {
locker.lockFile({ filePath: 'non-existent.ts', context: 'test' });
}).toThrow('File does not exist:');
expect(() => {
locker.lockFile({ filePath: 'deep/nested/non-existent.ts', context: 'test' });
}).toThrow('File does not exist:');
});
it('should work with different project roots', () => {
// Create a different temp directory
const otherTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'locker-other-'));
const otherLocker = new locker_1.Locker(otherTempDir);
try {
const fileName = 'other-test.ts';
const fullPath = path.join(otherTempDir, fileName);
fs.writeFileSync(fullPath, 'console.log("other test");');
const context = 'other project file';
// Lock using the other locker instance
const result = otherLocker.lockFile({ filePath: fileName, context });
expect(result.state).toBe('locked-context');
expect(result.context).toBe(context);
// Verify the file was modified in the correct location
const content = fs.readFileSync(fullPath, 'utf-8');
expect(content).toMatch(/^\/\/ LOCKED: .+ STATE=locked-context/);
// Verify the original locker doesn't see this file
const originalState = locker.getFileState({ filePath: fileName });
expect(originalState.state).toBe('unlocked');
}
finally {
// Cleanup
if (fs.existsSync(otherTempDir)) {
fs.rmSync(otherTempDir, { recursive: true, force: true });
}
}
});
});
});
//# sourceMappingURL=locker.test.js.map