import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import 'dotenv/config';
import { spawn, ChildProcess } from 'child_process';
import { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from '../../src/types/jsonrpc.js';
import { terminateServer } from './test-utils.js';
/**
* Integration test for server initialization with subscription configuration
*
* Tests that the server properly initializes with subscription capabilities
* and handles the complete lifecycle including shutdown.
*/
describe('Server Subscription Integration', () => {
let server: ChildProcess | null = null;
let requestId = 1;
const responses = new Map<number, JsonRpcResponse>();
const notifications: JsonRpcNotification[] = [];
const sendRequest = (request: JsonRpcRequest): void => {
server?.stdin?.write(JSON.stringify(request) + '\n');
};
const waitForResponse = (id: number, timeout = 5000): Promise<JsonRpcResponse> => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkResponse = () => {
if (responses.has(id)) {
const response = responses.get(id)!;
responses.delete(id);
resolve(response);
} else if (Date.now() - startTime > timeout) {
reject(new Error(`Timeout waiting for response to request ${id}`));
} else {
setTimeout(checkResponse, 10);
}
};
checkResponse();
});
};
beforeAll(async () => {
// Skip if no API key (integration tests are optional)
if (!process.env.OBSIDIAN_API_KEY) {
console.warn('Skipping integration tests - OBSIDIAN_API_KEY not set');
return;
}
// Start the MCP server with subscription configuration
server = spawn('tsx', ['src/index.ts'], {
env: {
...process.env,
OBSIDIAN_API_KEY: process.env.OBSIDIAN_API_KEY,
OBSIDIAN_ENABLE_SUBSCRIPTIONS: 'true',
OBSIDIAN_MAX_SUBSCRIPTIONS: '10',
OBSIDIAN_DEFAULT_SUBSCRIPTIONS: 'vault://recent,vault://tags'
}
});
// Collect responses and notifications
server.stdout?.on('data', (data) => {
const lines = data.toString().split('\n').filter(Boolean);
for (const line of lines) {
try {
const message = JSON.parse(line);
if (message.jsonrpc === '2.0') {
if ('id' in message && message.id !== null) {
responses.set(message.id as number, message as JsonRpcResponse);
} else if ('method' in message && !('id' in message)) {
notifications.push(message as JsonRpcNotification);
}
}
} catch (e) {
// Ignore non-JSON output (like stderr messages)
}
}
});
// Initialize the server
const initRequest: JsonRpcRequest = {
jsonrpc: '2.0',
id: requestId++,
method: 'initialize',
params: {
protocolVersion: '0.1.0',
capabilities: {},
clientInfo: {
name: 'subscription-test-client',
version: '1.0.0'
}
}
};
sendRequest(initRequest);
const initResponse = await waitForResponse(initRequest.id as number);
expect(initResponse.result).toBeDefined();
});
afterAll(async () => {
await terminateServer(server);
server = null;
});
it('should initialize server with subscription capabilities', async () => {
if (!process.env.OBSIDIAN_API_KEY) {
console.warn('Skipping test - OBSIDIAN_API_KEY not set');
return;
}
// The server should have started successfully with subscription capabilities
// This is verified by the successful initialization in beforeAll
expect(server).toBeDefined();
});
it('should support subscription requests', async () => {
if (!process.env.OBSIDIAN_API_KEY) {
console.warn('Skipping test - OBSIDIAN_API_KEY not set');
return;
}
// Test subscription to a subscribable resource
const subscribeRequest: JsonRpcRequest = {
jsonrpc: '2.0',
id: requestId++,
method: 'resources/subscribe',
params: {
uri: 'vault://recent'
}
};
sendRequest(subscribeRequest);
const subscribeResponse = await waitForResponse(subscribeRequest.id as number);
// Should succeed without error
expect(subscribeResponse.error).toBeUndefined();
expect(subscribeResponse.result).toBeDefined();
});
it('should support unsubscription requests', async () => {
if (!process.env.OBSIDIAN_API_KEY) {
console.warn('Skipping test - OBSIDIAN_API_KEY not set');
return;
}
// First subscribe
const subscribeRequest: JsonRpcRequest = {
jsonrpc: '2.0',
id: requestId++,
method: 'resources/subscribe',
params: {
uri: 'vault://tags'
}
};
sendRequest(subscribeRequest);
await waitForResponse(subscribeRequest.id as number);
// Then unsubscribe
const unsubscribeRequest: JsonRpcRequest = {
jsonrpc: '2.0',
id: requestId++,
method: 'resources/unsubscribe',
params: {
uri: 'vault://tags'
}
};
sendRequest(unsubscribeRequest);
const unsubscribeResponse = await waitForResponse(unsubscribeRequest.id as number);
// Should succeed without error
expect(unsubscribeResponse.error).toBeUndefined();
expect(unsubscribeResponse.result).toBeDefined();
});
it('should reject subscription to non-subscribable resources', async () => {
if (!process.env.OBSIDIAN_API_KEY) {
console.warn('Skipping test - OBSIDIAN_API_KEY not set');
return;
}
// Try to subscribe to a non-subscribable resource
const subscribeRequest: JsonRpcRequest = {
jsonrpc: '2.0',
id: requestId++,
method: 'resources/subscribe',
params: {
uri: 'vault://files/some-file.md'
}
};
sendRequest(subscribeRequest);
const subscribeResponse = await waitForResponse(subscribeRequest.id as number);
// Should return an error for non-subscribable resource
expect(subscribeResponse.error).toBeDefined();
expect(subscribeResponse.error?.message).toContain('not subscribable');
});
it('should handle graceful shutdown with active subscriptions', async () => {
if (!process.env.OBSIDIAN_API_KEY) {
console.warn('Skipping test - OBSIDIAN_API_KEY not set');
return;
}
// Subscribe to a resource
const subscribeRequest: JsonRpcRequest = {
jsonrpc: '2.0',
id: requestId++,
method: 'resources/subscribe',
params: {
uri: 'vault://stats'
}
};
sendRequest(subscribeRequest);
await waitForResponse(subscribeRequest.id as number);
// The server should handle shutdown gracefully in afterAll
// We don't test the actual shutdown here as it would terminate the server
// for other tests, but the afterAll hook verifies graceful shutdown works
expect(true).toBe(true); // Placeholder assertion
});
});