openpencil/apps/web/server/__tests__/mcp-sync-state-active.test.ts
Kayshen-X a7d73ebb62 feat(ai): pencil-style agentic design tool-loop, multi-chat tabs, #27 panel restyle
Built-in design generation now runs as an agentic MCP tool-loop (reusing the
agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the
Settings experimental toggle; the orchestrator stays the default.

- design-agent system prompt + in-process design toolset (parity-locked with
  the MCP surface) + flag-gated Intent::Design routing
- spawn_agents execution as sequential sub-loops + live creation-mode badges
  (per-agent glow + 'N/M designing...' header)
- new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state,
  export_nodes, spawn_agents; style-guide local audit
- #27 AI panel restyle: rounded tool cards + green check-rings, gray user
  bubbles, model-pill bottom toolbar, header, empty-state pills, the
  PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown
- multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI
  (switch / close / + / Cmd+T) with each run bound to its tab

Large checkpoint commit spanning the working tree (Rust shell crates).
2026-07-02 21:21:06 +08:00

83 lines
2.9 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import {
registerSSEClient,
unregisterSSEClient,
setSyncDocument,
setSyncSelection,
markClientActive,
getLastActiveClientId,
isClientConnected,
sendToClient,
clearSyncState,
} from '../utils/mcp-sync-state';
import type { PenDocument } from '../../src/types/pen';
describe('mcp-sync-state: active client tracking', () => {
beforeEach(() => {
clearSyncState();
});
it('getLastActiveClientId returns null initially', () => {
expect(getLastActiveClientId()).toBeNull();
});
it('setSyncDocument updates lastActiveClientId when sourceClientId provided', () => {
registerSSEClient('client-a', { push: () => {} });
setSyncDocument({ version: '1.0.0', children: [] } as PenDocument, 'client-a');
expect(getLastActiveClientId()).toBe('client-a');
});
it('setSyncSelection updates lastActiveClientId', () => {
registerSSEClient('client-b', { push: () => {} });
setSyncSelection(['node-1'], 'page-1', 'client-b');
expect(getLastActiveClientId()).toBe('client-b');
});
it('markClientActive updates lastActiveClientId only for connected clients', () => {
registerSSEClient('client-c', { push: () => {} });
markClientActive('client-c');
expect(getLastActiveClientId()).toBe('client-c');
markClientActive('client-nonexistent');
expect(getLastActiveClientId()).toBe('client-c');
});
it('isClientConnected reflects registration state', () => {
registerSSEClient('client-d', { push: () => {} });
expect(isClientConnected('client-d')).toBe(true);
unregisterSSEClient('client-d');
expect(isClientConnected('client-d')).toBe(false);
});
it('sendToClient dispatches payload to a specific client', () => {
const received: string[] = [];
registerSSEClient('client-e', { push: (data: string) => received.push(data) });
const ok = sendToClient('client-e', { type: 'screenshot:request' });
expect(ok).toBe(true);
expect(received).toHaveLength(1);
expect(JSON.parse(received[0]).type).toBe('screenshot:request');
});
it('sendToClient returns false for unknown client', () => {
expect(sendToClient('nope', { type: 'x' })).toBe(false);
});
it('broadcasts document updates to other clients and prunes broken writers', () => {
const received: string[] = [];
registerSSEClient('client-source', { push: () => {} });
registerSSEClient('client-target', { push: (data: string) => received.push(data) });
registerSSEClient('client-broken', {
push: () => {
throw new Error('writer closed');
},
});
setSyncDocument({ version: '1.0.0', children: [] } as PenDocument, 'client-source');
expect(received).toHaveLength(1);
expect(JSON.parse(received[0]).type).toBe('document:update');
expect(isClientConnected('client-source')).toBe(true);
expect(isClientConnected('client-target')).toBe(true);
expect(isClientConnected('client-broken')).toBe(false);
});
});