feat: 2.0.0-alpha.11 — DB-backed service registry + LAN auto-discovery
- monitored_services table (mig 015) replaces config/services.json (now a boot seed) - owner CRUD over /api/health/services; GET is DB-backed; cron+worker read the DB - discover.lan worker: pure-Node TCP sweep + HTTP-title probe -> disabled 'discovered' candidates (never clobbers curated entries); POST /api/health/discover + GET .../discovered - dashboard: Scan button + Discovered(N) section with one-click promote Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,55 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import { setup } from './helpers.js';
|
||||
import { resetDb } from '../helpers/db.js';
|
||||
import { migrateUp } from '../../lib/db/migrate.js';
|
||||
import * as statusRepo from '../../lib/db/repos/service_status.js';
|
||||
import * as services from '../../lib/db/repos/monitored_services.js';
|
||||
|
||||
let app, ownerHeaders;
|
||||
beforeAll(async () => {
|
||||
({ app, ownerHeaders } = await setup());
|
||||
beforeAll(async () => { ({ app, ownerHeaders } = await setup()); });
|
||||
beforeEach(async () => {
|
||||
await resetDb(); await migrateUp();
|
||||
await services.create({ id: 'gitea', name: 'Gitea', category: 'infrastructure', host: 'ct105', url: 'http://192.168.1.223:3000', icon: 'gitea', check: { type: 'http' } });
|
||||
await statusRepo.upsert({ service_id: 'gitea', status: 'ok', latency_ms: 10, detail: '200' });
|
||||
});
|
||||
describe('health api', () => {
|
||||
|
||||
describe('health api (DB-backed registry)', () => {
|
||||
it('401 without auth', async () => expect((await request(app).get('/api/health/services')).status).toBe(401));
|
||||
it('POST /check rejects anonymous (owner-only mutation)', async () =>
|
||||
expect((await request(app).post('/api/health/check')).status).toBe(401));
|
||||
it('returns groups with counts + merged cached status', async () => {
|
||||
it('POST /check rejects anonymous', async () => expect((await request(app).post('/api/health/check')).status).toBe(401));
|
||||
it('POST /discover rejects anonymous', async () => expect((await request(app).post('/api/health/discover')).status).toBe(401));
|
||||
it('POST /services rejects anonymous', async () => expect((await request(app).post('/api/health/services')).status).toBe(401));
|
||||
|
||||
it('GET /services returns grouped counts + merged status', async () => {
|
||||
const res = await request(app).get('/api/health/services').set(ownerHeaders);
|
||||
expect(res.status).toBe(200);
|
||||
const infra = res.body.find(g => g.category === 'infrastructure');
|
||||
expect(infra).toBeTruthy();
|
||||
expect(infra.healthy).toBeGreaterThanOrEqual(1); // gitea ok
|
||||
const gitea = infra.services.find(s => s.id === 'gitea');
|
||||
expect(gitea.status).toBe('ok');
|
||||
expect(infra.healthy).toBe(1);
|
||||
expect(infra.services.find(s => s.id === 'gitea').status).toBe('ok');
|
||||
});
|
||||
|
||||
it('POST /services adds a service that shows up in the band', async () => {
|
||||
const create = await request(app).post('/api/health/services').set(ownerHeaders)
|
||||
.send({ id: 'ollama', name: 'Ollama', category: 'agents', host: 'ct102', url: 'http://192.168.1.185:11434' });
|
||||
expect(create.status).toBe(201);
|
||||
const res = await request(app).get('/api/health/services').set(ownerHeaders);
|
||||
expect(res.body.find(g => g.category === 'agents').services.some(s => s.id === 'ollama')).toBe(true);
|
||||
});
|
||||
|
||||
it('PATCH disables a service (drops out of the band); DELETE removes it', async () => {
|
||||
await request(app).patch('/api/health/services/gitea').set(ownerHeaders).send({ enabled: false });
|
||||
let res = await request(app).get('/api/health/services').set(ownerHeaders);
|
||||
expect(res.body.find(g => g.category === 'infrastructure')).toBeUndefined(); // no enabled infra now
|
||||
const del = await request(app).delete('/api/health/services/gitea').set(ownerHeaders);
|
||||
expect(del.status).toBe(204);
|
||||
expect((await request(app).delete('/api/health/services/gitea').set(ownerHeaders)).status).toBe(404);
|
||||
});
|
||||
|
||||
it('GET /services/discovered lists owner-only candidates', async () => {
|
||||
await services.upsertDiscovered({ id: 'disc-x', name: 'Mystery', url: 'http://192.168.1.99:8000' });
|
||||
expect((await request(app).get('/api/health/services/discovered')).status).toBe(401); // anon
|
||||
const res = await request(app).get('/api/health/services/discovered').set(ownerHeaders);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.map(s => s.id)).toContain('disc-x');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user