Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"Bash(pnpm type-check:*)",
"Bash(pnpm lint:*)",
"Bash(pnpm --filter ./api lint)",
"Bash(mv:*)"
"Bash(mv:*)",
"Bash(ls:*)",
"mcp__ide__getDiagnostics",
"Bash(pnpm --filter \"*connect*\" test connect-status-writer.service.spec)"
]
},
"enableAllProjectMcpServers": false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { ConfigService } from '@nestjs/config';
import { access, constants, mkdir, readFile, rm } from 'fs/promises';
import { join } from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { ConfigType } from '../config/connect.config.js';
import { ConnectStatusWriterService } from './connect-status-writer.service.js';

describe('ConnectStatusWriterService Config Behavior', () => {
let service: ConnectStatusWriterService;
let configService: ConfigService<ConfigType, true>;
const testDir = '/tmp/connect-status-config-test';
const testFilePath = join(testDir, 'connectStatus.json');

// Simulate config changes
let configStore: any = {};

beforeEach(async () => {
vi.clearAllMocks();

// Reset config store
configStore = {};

// Create test directory
await mkdir(testDir, { recursive: true });

// Create a ConfigService mock that behaves like the real one
configService = {
get: vi.fn().mockImplementation((key: string) => {
console.log(`ConfigService.get('${key}') called, returning:`, configStore[key]);
return configStore[key];
}),
set: vi.fn().mockImplementation((key: string, value: any) => {
console.log(`ConfigService.set('${key}', ${JSON.stringify(value)}) called`);
configStore[key] = value;
}),
} as unknown as ConfigService<ConfigType, true>;

service = new ConnectStatusWriterService(configService);

// Override the status file path to use our test location
Object.defineProperty(service, 'statusFilePath', {
get: () => testFilePath,
});
});

afterEach(async () => {
await service.onModuleDestroy();
await rm(testDir, { recursive: true, force: true });
});

it('should write status when config is updated directly', async () => {
// Initialize service - should write PRE_INIT
await service.onApplicationBootstrap();
await new Promise(resolve => setTimeout(resolve, 50));

let content = await readFile(testFilePath, 'utf-8');
let data = JSON.parse(content);
console.log('Initial status:', data);
expect(data.connectionStatus).toBe('PRE_INIT');

// Update config directly (simulating what ConnectionService does)
console.log('\n=== Updating config to CONNECTED ===');
configService.set('connect.mothership', {
status: 'CONNECTED',
error: null,
lastPing: Date.now(),
});

// Call the writeStatus method directly (since @OnEvent handles the event)
await service['writeStatus']();

content = await readFile(testFilePath, 'utf-8');
data = JSON.parse(content);
console.log('Status after config update:', data);
expect(data.connectionStatus).toBe('CONNECTED');
});

it('should test the actual flow with multiple status updates', async () => {
await service.onApplicationBootstrap();
await new Promise(resolve => setTimeout(resolve, 50));

const statusUpdates = [
{ status: 'CONNECTING', error: null, lastPing: null },
{ status: 'CONNECTED', error: null, lastPing: Date.now() },
{ status: 'DISCONNECTED', error: 'Lost connection', lastPing: Date.now() - 10000 },
{ status: 'RECONNECTING', error: null, lastPing: Date.now() - 10000 },
{ status: 'CONNECTED', error: null, lastPing: Date.now() },
];

for (const update of statusUpdates) {
console.log(`\n=== Updating to ${update.status} ===`);

// Update config
configService.set('connect.mothership', update);

// Call writeStatus directly
await service['writeStatus']();

const content = await readFile(testFilePath, 'utf-8');
const data = JSON.parse(content);
console.log(`Status file shows: ${data.connectionStatus}`);
expect(data.connectionStatus).toBe(update.status);
}
});

it('should handle case where config is not set before event', async () => {
await service.onApplicationBootstrap();
await new Promise(resolve => setTimeout(resolve, 50));

// Delete the config
delete configStore['connect.mothership'];

// Call writeStatus without config
console.log('\n=== Calling writeStatus with no config ===');
await service['writeStatus']();

const content = await readFile(testFilePath, 'utf-8');
const data = JSON.parse(content);
console.log('Status with no config:', data);
expect(data.connectionStatus).toBe('PRE_INIT');

// Now set config and call writeStatus again
console.log('\n=== Setting config and calling writeStatus ===');
configService.set('connect.mothership', {
status: 'CONNECTED',
error: null,
lastPing: Date.now(),
});
await service['writeStatus']();

const content2 = await readFile(testFilePath, 'utf-8');
const data2 = JSON.parse(content2);
console.log('Status after setting config:', data2);
expect(data2.connectionStatus).toBe('CONNECTED');
});

describe('cleanup on shutdown', () => {
it('should delete status file on module destroy', async () => {
await service.onApplicationBootstrap();
await new Promise(resolve => setTimeout(resolve, 50));

// Verify file exists
await expect(access(testFilePath, constants.F_OK)).resolves.not.toThrow();

// Cleanup
await service.onModuleDestroy();

// Verify file is deleted
await expect(access(testFilePath, constants.F_OK)).rejects.toThrow();
});

it('should handle cleanup when file does not exist', async () => {
// Don't bootstrap (so no file is written)
await expect(service.onModuleDestroy()).resolves.not.toThrow();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { ConfigService } from '@nestjs/config';
import { access, constants, mkdir, readFile, rm } from 'fs/promises';
import { join } from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { ConfigType } from '../config/connect.config.js';
import { ConnectStatusWriterService } from './connect-status-writer.service.js';

describe('ConnectStatusWriterService Integration', () => {
let service: ConnectStatusWriterService;
let configService: ConfigService<ConfigType, true>;
const testDir = '/tmp/connect-status-test';
const testFilePath = join(testDir, 'connectStatus.json');

beforeEach(async () => {
vi.clearAllMocks();

// Create test directory
await mkdir(testDir, { recursive: true });

configService = {
get: vi.fn().mockImplementation((key: string) => {
console.log(`ConfigService.get called with key: ${key}`);
return {
status: 'CONNECTED',
error: null,
lastPing: Date.now(),
};
}),
} as unknown as ConfigService<ConfigType, true>;

service = new ConnectStatusWriterService(configService);

// Override the status file path to use our test location
Object.defineProperty(service, 'statusFilePath', {
get: () => testFilePath,
});
});

afterEach(async () => {
await service.onModuleDestroy();
await rm(testDir, { recursive: true, force: true });
});

it('should write initial PRE_INIT status, then update on event', async () => {
// First, mock the config to return undefined (no connection metadata)
vi.mocked(configService.get).mockReturnValue(undefined);

console.log('=== Starting onApplicationBootstrap ===');
await service.onApplicationBootstrap();

// Wait a bit for the initial write to complete
await new Promise(resolve => setTimeout(resolve, 50));

// Read initial status
const initialContent = await readFile(testFilePath, 'utf-8');
const initialData = JSON.parse(initialContent);
console.log('Initial status written:', initialData);

expect(initialData.connectionStatus).toBe('PRE_INIT');
expect(initialData.error).toBeNull();
expect(initialData.lastPing).toBeNull();

// Now update the mock to return CONNECTED status
vi.mocked(configService.get).mockReturnValue({
status: 'CONNECTED',
error: null,
lastPing: 1234567890,
});

console.log('=== Calling writeStatus directly ===');
await service['writeStatus']();

// Read updated status
const updatedContent = await readFile(testFilePath, 'utf-8');
const updatedData = JSON.parse(updatedContent);
console.log('Updated status after writeStatus:', updatedData);

expect(updatedData.connectionStatus).toBe('CONNECTED');
expect(updatedData.lastPing).toBe(1234567890);
});

it('should handle rapid status changes correctly', async () => {
const statusChanges = [
{ status: 'PRE_INIT', error: null, lastPing: null },
{ status: 'CONNECTING', error: null, lastPing: null },
{ status: 'CONNECTED', error: null, lastPing: Date.now() },
{ status: 'DISCONNECTED', error: 'Connection lost', lastPing: Date.now() - 5000 },
{ status: 'CONNECTED', error: null, lastPing: Date.now() },
];

let changeIndex = 0;
vi.mocked(configService.get).mockImplementation(() => {
const change = statusChanges[changeIndex];
console.log(`Returning status ${changeIndex}: ${change.status}`);
return change;
});

await service.onApplicationBootstrap();
await new Promise(resolve => setTimeout(resolve, 50));

// Simulate the final status change
changeIndex = statusChanges.length - 1;
console.log(`=== Calling writeStatus for final status: ${statusChanges[changeIndex].status} ===`);
await service['writeStatus']();

// Read final status
const finalContent = await readFile(testFilePath, 'utf-8');
const finalData = JSON.parse(finalContent);
console.log('Final status after status change:', finalData);

// Should have the last status
expect(finalData.connectionStatus).toBe('CONNECTED');
expect(finalData.error).toBeNull();
});

it('should handle multiple write calls correctly', async () => {
const writes: number[] = [];
const originalWriteStatus = service['writeStatus'].bind(service);

service['writeStatus'] = async function() {
const timestamp = Date.now();
writes.push(timestamp);
console.log(`writeStatus called at ${timestamp}`);
return originalWriteStatus();
};

await service.onApplicationBootstrap();
await new Promise(resolve => setTimeout(resolve, 50));

const initialWrites = writes.length;
console.log(`Initial writes: ${initialWrites}`);

// Make multiple write calls
for (let i = 0; i < 3; i++) {
console.log(`Calling writeStatus ${i}`);
await service['writeStatus']();
}

console.log(`Total writes: ${writes.length}`);
console.log('Write timestamps:', writes);

// Should have initial write + 3 additional writes
expect(writes.length).toBe(initialWrites + 3);
});

describe('cleanup on shutdown', () => {
it('should delete status file on module destroy', async () => {
await service.onApplicationBootstrap();
await new Promise(resolve => setTimeout(resolve, 50));

// Verify file exists
await expect(access(testFilePath, constants.F_OK)).resolves.not.toThrow();

// Cleanup
await service.onModuleDestroy();

// Verify file is deleted
await expect(access(testFilePath, constants.F_OK)).rejects.toThrow();
});

it('should handle cleanup gracefully when file does not exist', async () => {
// Don't bootstrap (so no file is created)
await expect(service.onModuleDestroy()).resolves.not.toThrow();
});
});
});
Loading