96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import { InstanceService } from '../instances/instance-service.js';
|
|
import { migrateDatabase } from '../../infrastructure/database/migrations.js';
|
|
import type { SecretStore } from '../../infrastructure/secrets/secret-store.js';
|
|
import type { TransportResponse } from '../../infrastructure/transport/safe-instance-transport.js';
|
|
import { ConnectionProbe } from './connection-probe.js';
|
|
|
|
class Store implements SecretStore {
|
|
async set(key: { instanceId: string; purpose: string; slot?: string }) {
|
|
return `keychain://multi-simadmin/${Buffer.from(JSON.stringify([key.instanceId, key.purpose, key.slot])).toString('base64url')}`;
|
|
}
|
|
async get() {
|
|
return undefined;
|
|
}
|
|
async delete() {
|
|
return false;
|
|
}
|
|
}
|
|
const dbs: Database.Database[] = [];
|
|
afterEach(() => {
|
|
for (const db of dbs.splice(0)) db.close();
|
|
});
|
|
const fixture = (response: TransportResponse) => {
|
|
const db = new Database(':memory:');
|
|
db.pragma('foreign_keys=ON');
|
|
migrateDatabase(db);
|
|
dbs.push(db);
|
|
const instances = new InstanceService({
|
|
db,
|
|
store: new Store(),
|
|
idFactory: () => 'instance-1',
|
|
now: () => new Date('2026-07-16T12:00:00.000Z'),
|
|
});
|
|
const transport = {
|
|
get: async (url: string) => {
|
|
expect(url).toBe('http://192.168.1.20:3000/api/health');
|
|
return response;
|
|
},
|
|
};
|
|
return {
|
|
db,
|
|
instances,
|
|
probe: new ConnectionProbe({
|
|
db,
|
|
instances,
|
|
transport,
|
|
now: () => new Date('2026-07-16T12:00:00.000Z'),
|
|
}),
|
|
};
|
|
};
|
|
|
|
describe('ConnectionProbe', () => {
|
|
it('uses the unauthenticated health endpoint and records a reachable unauthenticated instance', async () => {
|
|
const { instances, probe } = fixture({ status: 200, headers: {}, body: '{}' });
|
|
await instances.create({ name: 'LAN', origin: 'http://192.168.1.20:3000' });
|
|
await expect(probe.test('instance-1')).resolves.toEqual({
|
|
instanceId: 'instance-1',
|
|
authenticated: true,
|
|
checkedAt: '2026-07-16T12:00:00.000Z',
|
|
});
|
|
});
|
|
it('treats 401 as reachable but not authenticated rather than offline', async () => {
|
|
const { instances, probe } = fixture({ status: 401, headers: {}, body: '{}' });
|
|
await instances.create({ name: 'LAN', origin: 'http://192.168.1.20:3000' });
|
|
await expect(probe.test('instance-1')).resolves.toMatchObject({ authenticated: false });
|
|
});
|
|
it('persists only a redacted connection freshness snapshot, not a blanket capability claim', async () => {
|
|
const { db, instances, probe } = fixture({ status: 401, headers: {}, body: '{}' });
|
|
await instances.create({ name: 'LAN', origin: 'http://192.168.1.20:3000' });
|
|
await probe.test('instance-1');
|
|
await probe.test('instance-1');
|
|
const snapshots = db
|
|
.prepare('SELECT category, state, payload_json, expires_at FROM status_snapshots')
|
|
.all() as {
|
|
category: string;
|
|
state: string;
|
|
payload_json: string;
|
|
expires_at: string;
|
|
}[];
|
|
expect(snapshots).toHaveLength(1);
|
|
expect(snapshots[0]).toMatchObject({ category: 'connection', state: 'fresh' });
|
|
expect(JSON.parse(snapshots[0]!.payload_json)).toEqual({
|
|
reachable: true,
|
|
authenticated: false,
|
|
});
|
|
expect(snapshots[0]!.expires_at).toBe('2026-07-16T12:00:30.000Z');
|
|
expect(db.prepare('SELECT * FROM capabilities').all()).toEqual([]);
|
|
});
|
|
|
|
it('does not invoke transport for an unknown instance', async () => {
|
|
const { probe } = fixture({ status: 200, headers: {}, body: '{}' });
|
|
await expect(probe.test('missing')).rejects.toMatchObject({ code: 'NOT_FOUND' });
|
|
});
|
|
});
|