mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
148 lines
5.9 KiB
TypeScript
148 lines
5.9 KiB
TypeScript
/**
|
|||
|
|
* @fileoverview End to end through the real WebServer: a session created with a
|
||
|
|
* `tabGroupId` lands in that group in ONE layout version, and a session spawned
|
||
|
|
* by another one (the X-Codeman-Parent-Session header) follows its parent into
|
||
|
|
* the parent's group until it is moved by hand.
|
||
|
|
*
|
||
|
|
* The service and route halves have their own unit tests; this pins the wiring
|
||
|
|
* in server.ts (the session joins the live map inside the layout owner lock and
|
||
|
|
* the placement commits with it) against the real routes and StateStore.
|
||
|
|
*/
|
||
|
|
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||
|
|
import { mkdtempSync } from 'node:fs';
|
||
|
|
import { tmpdir } from 'node:os';
|
||
|
|
import { join } from 'node:path';
|
||
|
|
import { WebServer } from '../src/web/server.js';
|
||
|
|
|
||
|
|
const TEST_PORT = 3301;
|
||
|
|
|
||
|
|
type Ref = { kind: string; id: string; placement?: string };
|
||
|
|
type Layout = {
|
||
|
|
version: number;
|
||
|
|
updatedAt: string;
|
||
|
|
groups: Array<{ id: string; name: string; refs: Ref[] }>;
|
||
|
|
ungrouped: Ref[];
|
||
|
|
};
|
||
|
|
|
||
|
|
describe('grouped session creation through the server', () => {
|
||
|
|
let server: WebServer;
|
||
|
|
const base = `http://localhost:${TEST_PORT}`;
|
||
|
|
const workingDir = mkdtempSync(join(tmpdir(), 'codeman-grouped-create-'));
|
||
|
|
|
||
|
|
const api = async (
|
||
|
|
path: string,
|
||
|
|
init: { method?: string; body?: unknown; headers?: Record<string, string> } = {}
|
||
|
|
) => {
|
||
|
|
const res = await fetch(`${base}${path}`, {
|
||
|
|
method: init.method ?? 'GET',
|
||
|
|
headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}) },
|
||
|
|
...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
|
||
|
|
});
|
||
|
|
return { status: res.status, json: (await res.json()) as any };
|
||
|
|
};
|
||
|
|
const layout = async (): Promise<Layout> => (await api('/api/tab-layout')).json.data.layout;
|
||
|
|
const create = async (body: Record<string, unknown>, headers?: Record<string, string>) => {
|
||
|
|
const res = await api('/api/sessions', { method: 'POST', body: { workingDir, mode: 'shell', ...body }, headers });
|
||
|
|
expect(res.status).toBe(200);
|
||
|
|
return res.json.data as { session: { id: string }; tabLayout?: Layout };
|
||
|
|
};
|
||
|
|
const ids = (refs: Ref[]) => refs.map((ref) => ref.id);
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
server = new WebServer(TEST_PORT, false, true);
|
||
|
|
await server.start();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await server.stop();
|
||
|
|
}, 60000);
|
||
|
|
|
||
|
|
it('places the new session at the end of the group in the same write that records it', async () => {
|
||
|
|
const first = await create({ name: 'first' });
|
||
|
|
const current = await layout();
|
||
|
|
const put = await api('/api/tab-layout', {
|
||
|
|
method: 'PUT',
|
||
|
|
body: {
|
||
|
|
baseVersion: current.version,
|
||
|
|
layout: {
|
||
|
|
...current,
|
||
|
|
groups: [{ id: 'g1', name: 'Core', refs: [{ kind: 'session', id: first.session.id }] }],
|
||
|
|
ungrouped: [],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
expect(put.status).toBe(200);
|
||
|
|
const before = put.json.data.layout as Layout;
|
||
|
|
|
||
|
|
const made = await create({ name: 'grouped', tabGroupId: 'g1' });
|
||
|
|
const after = await layout();
|
||
|
|
expect(after.version).toBe(before.version + 1);
|
||
|
|
expect(ids(after.groups[0].refs)).toEqual([first.session.id, made.session.id]);
|
||
|
|
expect(made.tabLayout).toEqual(after);
|
||
|
|
|
||
|
|
// An unknown group is a hint that does not apply: the session is still created.
|
||
|
|
const loose = await create({ name: 'loose', tabGroupId: 'no-such-group' });
|
||
|
|
expect(ids((await layout()).ungrouped)).toContain(loose.session.id);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('a spawned child follows its parent into the group, and stays put once moved by hand', async () => {
|
||
|
|
const start = await layout();
|
||
|
|
const parentId = start.groups[0].refs[0].id;
|
||
|
|
const child = await create({ name: 'child' }, { 'X-Codeman-Parent-Session': parentId });
|
||
|
|
const followed = await layout();
|
||
|
|
expect(ids(followed.groups[0].refs).slice(0, 2)).toEqual([parentId, child.session.id]);
|
||
|
|
|
||
|
|
// Hand-move the child to Ungrouped (what the browser sends for a moveRef).
|
||
|
|
const moved = await api('/api/tab-layout', {
|
||
|
|
method: 'PUT',
|
||
|
|
body: {
|
||
|
|
baseVersion: followed.version,
|
||
|
|
layout: {
|
||
|
|
...followed,
|
||
|
|
groups: [
|
||
|
|
{ ...followed.groups[0], refs: followed.groups[0].refs.filter((ref) => ref.id !== child.session.id) },
|
||
|
|
],
|
||
|
|
ungrouped: [...followed.ungrouped, { kind: 'session', id: child.session.id, placement: 'manual' }],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
expect(moved.status).toBe(200);
|
||
|
|
const after = await layout();
|
||
|
|
expect(after.ungrouped.find((ref) => ref.id === child.session.id)).toEqual({
|
||
|
|
kind: 'session',
|
||
|
|
id: child.session.id,
|
||
|
|
placement: 'manual',
|
||
|
|
});
|
||
|
|
expect(ids(after.groups[0].refs)).not.toContain(child.session.id);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('server.ts addSession wiring', () => {
|
||
|
|
it("keys the placement on the SESSION's owner and registers it only inside the layout call", async () => {
|
||
|
|
const sessions = new Map<string, unknown>();
|
||
|
|
let registered: (() => void | (() => void)) | undefined;
|
||
|
|
const sessionCreated = vi.fn(async (_owner: string, _id: string, _placement: unknown, register: () => unknown) => {
|
||
|
|
expect(sessions.has('bob-new')).toBe(false);
|
||
|
|
registered = register as never;
|
||
|
|
const rollback = register() as () => void;
|
||
|
|
expect(sessions.get('bob-new')).toBe(session);
|
||
|
|
rollback();
|
||
|
|
return { version: 1 };
|
||
|
|
});
|
||
|
|
const server = Object.create(WebServer.prototype) as any;
|
||
|
|
server.sessions = sessions;
|
||
|
|
server.tabLayouts = { sessionCreated };
|
||
|
|
const session = { id: 'bob-new', owner: 'bob' };
|
||
|
|
|
||
|
|
await server.registerSessionWithLayout(session, { tabGroupId: 'g1' });
|
||
|
|
|
||
|
|
expect(sessionCreated).toHaveBeenCalledWith('bob', 'bob-new', { tabGroupId: 'g1' }, expect.any(Function));
|
||
|
|
// The rollback removed it; a rollback never removes a DIFFERENT session under that id.
|
||
|
|
expect(sessions.has('bob-new')).toBe(false);
|
||
|
|
const rollback = registered!() as () => void;
|
||
|
|
const replacement = { id: 'bob-new', owner: 'bob' };
|
||
|
|
sessions.set('bob-new', replacement);
|
||
|
|
rollback();
|
||
|
|
expect(sessions.get('bob-new')).toBe(replacement);
|
||
|
|
});
|
||
|
|
});
|