Edit File Lines MCP Server
by oakenai
- src
- __tests__
import { EditOperation } from "../types/editTypes.js";
import { StateManager } from "../utils/stateManager.js";
describe("StateManager", () => {
let stateManager: StateManager;
beforeEach(() => {
// Reset environment variable before each test
delete process.env.MCP_EDIT_STATE_TTL;
stateManager = new StateManager();
});
describe("Constructor", () => {
it("should initialize with default TTL", () => {
expect(stateManager.getTTL()).toBe(60 * 1000); // 1 minute
});
it("should use custom TTL from environment variable", () => {
process.env.MCP_EDIT_STATE_TTL = "30000"; // 30 seconds
const customStateManager = new StateManager();
expect(customStateManager.getTTL()).toBe(30000);
});
it("should throw error for invalid TTL", () => {
process.env.MCP_EDIT_STATE_TTL = "-1000";
expect(() => new StateManager()).toThrow(
"MCP_EDIT_STATE_TTL must be a positive number when set"
);
});
it("should throw error for non-numeric TTL", () => {
process.env.MCP_EDIT_STATE_TTL = "invalid";
expect(() => new StateManager()).toThrow(
"MCP_EDIT_STATE_TTL must be a positive number when set"
);
});
});
describe("saveState", () => {
const samplePath = "/path/to/file.txt";
const sampleEdit: EditOperation = {
startLine: 1,
endLine: 2,
content: "new content",
strMatch: "old content"
};
it("should save state and return consistent ID for same input", () => {
const id1 = stateManager.saveState(samplePath, [sampleEdit]);
const id2 = stateManager.saveState(samplePath, [sampleEdit]);
expect(id1).toBe(id2);
expect(id1).toMatch(/^[a-f0-9]{8}$/); // 8 character hex string
});
it("should handle array-style edits", () => {
const arrayStyleEdit: [number, number, string, string] = [
1,
2,
"new content",
"old content"
];
const id = stateManager.saveState(samplePath, [arrayStyleEdit]);
expect(id).toMatch(/^[a-f0-9]{8}$/);
});
it("should generate different IDs for different inputs", () => {
const id1 = stateManager.saveState(samplePath, [sampleEdit]);
const id2 = stateManager.saveState(samplePath, [
{
...sampleEdit,
content: "different content"
}
]);
expect(id1).not.toBe(id2);
});
it("should handle regex matches", () => {
const regexEdit: EditOperation = {
...sampleEdit,
regexMatch: "\\s*old\\s+content\\s*"
};
const id = stateManager.saveState(samplePath, [regexEdit]);
expect(id).toMatch(/^[a-f0-9]{8}$/);
});
});
describe("getState", () => {
const samplePath = "/path/to/file.txt";
const sampleEdit: EditOperation = {
startLine: 1,
endLine: 2,
content: "new content"
};
it("should return undefined for non-existent state", () => {
expect(stateManager.getState("nonexistent")).toBeUndefined();
});
it("should return state for valid ID", () => {
const id = stateManager.saveState(samplePath, [sampleEdit]);
const state = stateManager.getState(id);
expect(state).toBeDefined();
expect(state?.path).toBe(samplePath);
expect(state?.edits).toHaveLength(1);
expect(state?.edits[0]).toEqual(sampleEdit);
});
it("should handle expired states", async () => {
// Create state manager with 100ms TTL
process.env.MCP_EDIT_STATE_TTL = "100";
const shortTTLManager = new StateManager();
const id = shortTTLManager.saveState(samplePath, [sampleEdit]);
// Wait for state to expire
await new Promise((resolve) => setTimeout(resolve, 150));
expect(shortTTLManager.getState(id)).toBeUndefined();
});
});
describe("deleteState", () => {
const samplePath = "/path/to/file.txt";
const sampleEdit: EditOperation = {
startLine: 1,
endLine: 2,
content: "new content"
};
it("should delete existing state", () => {
const id = stateManager.saveState(samplePath, [sampleEdit]);
expect(stateManager.getState(id)).toBeDefined();
stateManager.deleteState(id);
expect(stateManager.getState(id)).toBeUndefined();
});
it("should handle deleting non-existent state", () => {
expect(() => stateManager.deleteState("nonexistent")).not.toThrow();
});
});
describe("cleanup", () => {
it("should automatically clean up expired states", async () => {
process.env.MCP_EDIT_STATE_TTL = "100";
const shortTTLManager = new StateManager();
// Add multiple states
shortTTLManager.saveState("/path1.txt", [
{ startLine: 1, endLine: 1, content: "content1" }
]);
shortTTLManager.saveState("/path2.txt", [
{ startLine: 1, endLine: 1, content: "content2" }
]);
expect(shortTTLManager.getActiveStateCount()).toBe(2);
// Wait for states to expire
await new Promise((resolve) => setTimeout(resolve, 150));
// Trigger cleanup by trying to get a state
shortTTLManager.getState("any-id");
expect(shortTTLManager.getActiveStateCount()).toBe(0);
});
});
});