Files
thedotmack--claude-mem/tests/server/server-runtime-smoke.test.ts
wehub-resource-sync f9447f8e5f
CI / typecheck · build · test · bundle-size (push) Failing after 1s
CI / clean-room dependency closure smoke (push) Failing after 1s
CI / server-runtime e2e (docker · pg + valkey) (push) Failing after 2s
Deploy Install Scripts / deploy (push) Failing after 2s
Windows / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:07:03 +08:00

136 lines
5.5 KiB
TypeScript

// SPDX-License-Identifier: Apache-2.0
//
// #2550 — in-process server-runtime smoke test (always runs in CI, no Docker).
//
// This is the non-Docker counterpart to scripts/e2e-server-beta-docker.sh. It
// boots the server runtime's HTTP surface in-process against an in-memory
// SQLite DB and proves the four GA-gating facts from the plan-07 test matrix
// that DON'T require a real pg/redis container:
// 1. a mode is loaded (the #2443 boot guard succeeds)
// 2. an API key can be created (the operator key-gen path works)
// 3. an authed request succeeds with that key (auth contract holds end-to-end)
// 4. the viewer responds (the #2552 static handler is mounted)
//
// The full queue-durability / restart-recovery matrix stays in the Docker e2e.
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import { Database } from 'bun:sqlite';
import { Server, type ServerOptions } from '../../src/services/server/Server.js';
import { ServerV1Routes } from '../../src/server/routes/v1/ServerV1Routes.js';
import { ServerViewerRoutes } from '../../src/server/runtime/ServerViewerRoutes.js';
import { createServerApiKey, DEFAULT_LOCAL_API_KEY_SCOPES } from '../../src/server/auth/sqlite-api-key-service.js';
import { loadServerMode } from '../../src/server/runtime/create-server-service.js';
import { ModeManager } from '../../src/services/domain/ModeManager.js';
import { logger } from '../../src/utils/logger.js';
let loggerSpies: ReturnType<typeof spyOn>[] = [];
describe('server runtime in-process smoke (#2550)', () => {
let db: Database;
let server: Server;
let port: number;
beforeEach(async () => {
loggerSpies = [
spyOn(logger, 'info').mockImplementation(() => {}),
spyOn(logger, 'debug').mockImplementation(() => {}),
spyOn(logger, 'warn').mockImplementation(() => {}),
spyOn(logger, 'error').mockImplementation(() => {}),
];
db = new Database(':memory:');
db.run('PRAGMA foreign_keys = ON');
const options: ServerOptions = {
getInitializationComplete: () => true,
getMcpReady: () => true,
onShutdown: mock(() => Promise.resolve()),
onRestart: mock(() => Promise.resolve()),
workerPath: '/test/worker-service.cjs',
getAiStatus: () => ({ provider: 'claude', authMethod: 'cli', lastInteraction: null }),
};
server = new Server(options);
// Mount the same handlers the server runtime mounts: V1 routes (api-key
// auth) + the viewer static handler (#2552).
server.registerRoutes(new ServerV1Routes({
getDatabase: () => db,
authMode: 'api-key',
runtime: 'server-beta',
}));
server.registerRoutes(new ServerViewerRoutes());
server.finalizeRoutes();
await server.listen(0, '127.0.0.1');
const address = server.getHttpServer()?.address();
if (!address || typeof address === 'string') {
throw new Error('Expected server to bind to an ephemeral TCP port');
}
port = address.port;
});
afterEach(async () => {
try {
await server.close();
} catch (error: any) {
if (error?.code !== 'ERR_SERVER_NOT_RUNNING') throw error;
}
db.close();
loggerSpies.forEach(spy => spy.mockRestore());
mock.restore();
});
it('loads a mode at boot (the #2443 guard succeeds)', () => {
// loadServerMode() is the exact boot guard the real server calls; it
// throws if no mode can be loaded.
expect(() => loadServerMode()).not.toThrow();
expect(ModeManager.getInstance().getActiveMode()).toBeDefined();
});
it('reports the server runtime on /v1/info', async () => {
const res = await fetch(`http://127.0.0.1:${port}/v1/info`);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.runtime).toBe('server-beta');
expect(body.authMode).toBe('api-key');
});
it('rejects an unauthenticated request (auth contract enforced)', async () => {
const res = await fetch(`http://127.0.0.1:${port}/v1/projects`);
expect(res.status).toBe(401);
});
it('creates an API key and an authed request succeeds with it', async () => {
const created = createServerApiKey(db, { name: 'smoke-key' });
// The default key gets the scopes the local routes require.
expect(created.record.scopes).toEqual([...DEFAULT_LOCAL_API_KEY_SCOPES]);
// Write route — must be authorized.
const writeRes = await fetch(`http://127.0.0.1:${port}/v1/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${created.rawKey}` },
body: JSON.stringify({ name: 'Smoke Project' }),
});
expect(writeRes.status).toBe(201);
const { project } = await writeRes.json();
// Read route with the same key — must be authorized and return the project.
const readRes = await fetch(`http://127.0.0.1:${port}/v1/projects`, {
headers: { Authorization: `Bearer ${created.rawKey}` },
});
expect(readRes.status).toBe(200);
const { projects } = await readRes.json();
expect(projects.map((p: any) => p.id)).toContain(project.id);
});
it('serves the viewer at / (the #2552 static handler is mounted)', async () => {
const res = await fetch(`http://127.0.0.1:${port}/`);
// 200 when the build shipped viewer.html; 503 only if no viewer.html exists
// at any expected path. Either way the handler is mounted (not a 404).
if (ServerViewerRoutes.hasViewerHtml()) {
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toContain('text/html');
} else {
expect(res.status).toBe(503);
}
expect(res.status).not.toBe(404);
});
});