moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
/**
* AI-OS #4 — integration tests for GET /api/tools/stream + observe launch.
*
* Mocks the launcher (like tools-routes-launch.test.ts) so no real process
* spawns; injects an observable handle so the output buffer is populated and
* the SSE stream can replay it. The stream route ends synchronously for an
* already-exited process, so fastify.inject resolves with the captured body.
*/
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import type { FastifyInstance } from 'fastify';
import { MindDB, FrameStore, SessionStore } from '@waggle/core';
vi.mock('@waggle/agent', async (importOriginal) => {
const actual = await importOriginal<typeof import('@waggle/agent')>();
return {
...actual,
detectInstalledTools: vi.fn(async () => ({
platform: process.platform,
detectedAt: new Date().toISOString(),
tools: [{
id: 'claude-code',
displayName: 'Claude Code',
installed: true,
installedPath: '/server-detected/claude-code',
version: 'test',
hooksInstalled: false,
hookPointerPath: null,
}],
})),
launchTool: vi.fn((opts: { id: string; installedPath: string }) => ({
ok: true,
pid: 99999,
executed: { binary: opts.installedPath, args: [] },
})),
};
});
import { buildLocalServer } from '../src/local/index.js';
import { injectWithAuth } from './test-utils.js';
import { launchTool } from '@waggle/agent';
/** Controllable observable handle: capture the buffer's callbacks, drive them. */
function makeHandle() {
let onData: ((c: string) => void) | undefined;
let onExit: ((c: number | null) => void) | undefined;
return {
handle: {
onData: (cb: (c: string) => void) => { onData = cb; },
onExit: (cb: (c: number | null) => void) => { onExit = cb; },
},
emit: (c: string) => onData?.(c),
end: (code: number | null) => onExit?.(code),
};
}
function tmp(prefix: string): string {
return fs.mkdtempSync(path.join(os.tmpdir(), `waggle-stream-${prefix}-`));
}
function cleanup(dir: string): void {
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
}
describe('GET /api/tools/stream', () => {
let server: FastifyInstance;
let dir: string;
beforeAll(async () => {
dir = tmp('s');
const mind = new MindDB(path.join(dir, 'personal.mind'));
const sessions = new SessionStore(mind);
const frames = new FrameStore(mind);
const s = sessions.create('stream-test');
frames.createIFrame(s.gop_id, 'stream seed', 'normal');
mind.close();
server = await buildLocalServer({ dataDir: dir });
await server.ready();
});
afterAll(async () => {
if (server) await server.close();
if (dir) cleanup(dir);
});
it('400s on a missing/invalid pid', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/tools/stream?pid=abc' });
expect(res.statusCode).toBe(400);
});
it('404s when the pid has no observed output buffer', async () => {
const res = await injectWithAuth(server, { method: 'GET', url: '/api/tools/stream?pid=987654' });
expect(res.statusCode).toBe(404);
});
it('observe launch attaches a buffer the stream replays as SSE', async () => {
const f = makeHandle();
const PID = 4242;
vi.mocked(launchTool).mockReturnValueOnce({
ok: true,
pid: PID,
executed: { binary: '/x', args: [] },
output: f.handle,
});
const launch = await injectWithAuth(server, {
method: 'POST',
url: '/api/tools/launch',
headers: { 'content-type': 'application/json' },
payload: { id: 'claude-code', installedPath: '/x', observe: true },
});
expect(launch.statusCode).toBe(202);
// Buffer is now attached; emit + exit so the stream takes the exited
// path (replay + exit + end) and inject resolves.
f.emit('building...\n');
f.end(0);
expect(server.toolOutputBuffer?.has(PID)).toBe(true);
const stream = await injectWithAuth(server, {
method: 'GET',
url: `/api/tools/stream?pid=${PID}`,
});
expect(stream.statusCode).toBe(200);
expect(stream.headers['content-type']).toContain('text/event-stream');
expect(stream.body).toContain('event: line');
expect(stream.body).toContain('building...');
expect(stream.body).toContain('event: exit');
});
});