clean-tool.test.tsβ’8.39 kB
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { spawn, ChildProcess } from 'child_process';
import * as path from 'path';
const SERVER_PATH = path.resolve(process.cwd(), 'dist/index.js');
// Simple JSON-RPC client for testing
class JSONRPCClient {
private proc: ChildProcess;
private messageId = 1;
private responseHandlers = new Map<number, (response: any) => void>();
private buffer = "";
constructor() {
this.proc = spawn("node", [SERVER_PATH], {
stdio: ["pipe", "pipe", "inherit"],
});
this.proc.stdout?.on("data", (data) => {
this.buffer += data.toString();
this.processBuffer();
});
}
private processBuffer() {
const lines = this.buffer.split("\n");
this.buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
try {
const message = JSON.parse(line);
if (message.id && this.responseHandlers.has(message.id)) {
const handler = this.responseHandlers.get(message.id)!;
this.responseHandlers.delete(message.id);
handler(message);
}
} catch (e) {
// Ignore non-JSON lines
}
}
}
}
async request(method: string, params: any = {}): Promise<any> {
const id = this.messageId++;
const request = {
jsonrpc: "2.0",
id,
method,
params,
};
return new Promise((resolve, reject) => {
this.responseHandlers.set(id, (response) => {
if (response.error) {
reject(new Error(response.error.message));
} else {
resolve(response.result);
}
});
this.proc.stdin?.write(JSON.stringify(request) + "\n");
});
}
async close() {
this.proc.kill();
await new Promise((resolve) => this.proc.on("exit", resolve));
}
}
// Clean tool integration tests disabled by default to prevent unintended filesystem modifications
// These tests can delete session files and temporary data when run in full mode
describe.skipIf(globalThis.shouldSkipFullTests)('[full] clean tool integration', () => {
let client: JSONRPCClient;
beforeAll(async () => {
// Initialize the client
client = new JSONRPCClient();
// Wait for server to be ready
await new Promise(resolve => setTimeout(resolve, 1000));
});
afterAll(async () => {
await client?.close();
});
describe('tool registration', () => {
it('should list clean tool', async () => {
const response = await client.request('tools/list', {});
const cleanTool = response.tools.find((t: any) => t.name === 'clean');
expect(cleanTool).toBeDefined();
expect(cleanTool.description).toContain('Cleans up session cache');
expect(cleanTool.inputSchema.type).toBe('object');
expect(cleanTool.inputSchema.properties).toHaveProperty('all_sessions');
expect(cleanTool.inputSchema.properties).toHaveProperty('older_than');
expect(cleanTool.inputSchema.properties).toHaveProperty('session');
expect(cleanTool.inputSchema.properties).toHaveProperty('dry_run');
});
});
describe('tool execution', () => {
it('should handle dry run for all sessions', async () => {
const response = await client.request('tools/call', {
name: 'clean',
arguments: {
all_sessions: true,
dry_run: true
}
});
expect(response.content).toBeDefined();
expect(response.content).toHaveLength(1);
expect(response.content[0].type).toBe('text');
const text = response.content[0].text;
expect(text).toContain('Dry run mode');
expect(text).toMatch(/(Would remove \d+ session|No sessions to clean)/);
});
it('should clean sessions older than specified hours', async () => {
const response = await client.request('tools/call', {
name: 'clean',
arguments: {
older_than: 24,
dry_run: true
}
});
expect(response.content).toBeDefined();
expect(response.content).toHaveLength(1);
expect(response.content[0].type).toBe('text');
const text = response.content[0].text;
expect(text).toContain('Dry run mode');
expect(text).toMatch(/(Would remove \d+ session|No sessions to clean)/);
});
it('should clean specific session', async () => {
const response = await client.request('tools/call', {
name: 'clean',
arguments: {
session: '1751889198010-5978', // Use timestamp-based format
dry_run: true
}
});
expect(response.content).toBeDefined();
expect(response.content).toHaveLength(1);
expect(response.content[0].type).toBe('text');
const text = response.content[0].text;
expect(text).toContain('Dry run mode');
});
it('should reject multiple cleanup options', async () => {
const response = await client.request('tools/call', {
name: 'clean',
arguments: {
all_sessions: true,
older_than: 24
}
});
expect(response.isError).toBe(true);
expect(response.content[0].text).toContain('Invalid arguments');
});
it('should reject no cleanup options', async () => {
const response = await client.request('tools/call', {
name: 'clean',
arguments: {}
});
expect(response.isError).toBe(true);
expect(response.content[0].text).toContain('Invalid arguments');
});
it('should handle invalid session ID', async () => {
const response = await client.request('tools/call', {
name: 'clean',
arguments: {
session: '9999999999999-9999', // Use invalid timestamp-based format
dry_run: true
}
});
expect(response.content).toBeDefined();
const text = response.content[0].text;
// Should indicate no sessions found or removed
expect(text).toMatch(/(No sessions to clean|Would remove 0 session)/);
});
it('should show execution time when available', async () => {
const response = await client.request('tools/call', {
name: 'clean',
arguments: {
all_sessions: true,
dry_run: true
}
});
expect(response.content).toBeDefined();
const text = response.content[0].text;
// Execution time may not always be returned by the Swift CLI
// The test should pass if the command completes successfully
expect(text).toContain('Dry run mode');
expect(response.isError).not.toBe(true);
});
it('should format bytes correctly', async () => {
const response = await client.request('tools/call', {
name: 'clean',
arguments: {
all_sessions: true,
dry_run: true
}
});
expect(response.content).toBeDefined();
const text = response.content[0].text;
// Should show formatted bytes (B, KB, MB, or GB)
expect(text).toMatch(/(Space to be freed: \d+\.\d+ (B|KB|MB|GB)|No sessions to clean)/);
});
});
describe('edge cases', () => {
it('should handle negative older_than value gracefully', async () => {
try {
await client.request('tools/call', {
name: 'clean',
arguments: {
older_than: -1
}
});
// If it doesn't throw, check the response
} catch (error: any) {
// Negative values might be rejected by validation
expect(error.message).toBeDefined();
}
});
it('should handle very large older_than value', async () => {
const response = await client.request('tools/call', {
name: 'clean',
arguments: {
older_than: 999999,
dry_run: true
}
});
expect(response.content).toBeDefined();
const text = response.content[0].text;
// With such a large value, no sessions should be old enough
expect(text).toMatch(/(No sessions to clean|Would remove 0 session)/);
});
it('should handle empty session ID', async () => {
try {
await client.request('tools/call', {
name: 'clean',
arguments: {
session: ''
}
});
} catch (error: any) {
// Empty string might be rejected
expect(error.message).toBeDefined();
}
});
});
});