openpencil/apps/desktop/git/__tests__/merge-session.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

95 lines
2.8 KiB
TypeScript

// apps/desktop/git/__tests__/merge-session.test.ts
import { describe, it, expect } from 'vitest';
import {
encodeNodeConflictId,
encodeDocFieldConflictId,
parseConflictId,
buildConflictBag,
} from '../merge-session';
import type { NodeConflict, DocFieldConflict, MergeResult } from '@zseven-w/pen-core';
describe('conflict id codec', () => {
it('encodes node conflicts with the page-id placeholder for legacy single-page docs', () => {
const a: NodeConflict = {
pageId: 'page-1',
nodeId: 'rect-7',
reason: 'both-modified-same-field',
base: null,
ours: null,
theirs: null,
};
expect(encodeNodeConflictId(a)).toBe('node:page-1:rect-7');
const b: NodeConflict = {
pageId: null,
nodeId: 'frame-root',
reason: 'both-modified-same-field',
base: null,
ours: null,
theirs: null,
};
expect(encodeNodeConflictId(b)).toBe('node:_:frame-root');
});
it('encodes doc-field conflicts with field:path format', () => {
const a: DocFieldConflict = {
field: 'variables',
path: 'variables.color-1.value',
base: null,
ours: null,
theirs: null,
};
expect(encodeDocFieldConflictId(a)).toBe('field:variables:variables.color-1.value');
});
it('parses node and field ids back into structured form', () => {
expect(parseConflictId('node:page-1:rect-7')).toEqual({
kind: 'node',
pageId: 'page-1',
nodeId: 'rect-7',
});
expect(parseConflictId('node:_:frame-root')).toEqual({
kind: 'node',
pageId: null,
nodeId: 'frame-root',
});
expect(parseConflictId('field:variables:variables.color-1.value')).toEqual({
kind: 'field',
field: 'variables',
path: 'variables.color-1.value',
});
});
it('buildConflictBag attaches ids and builds the lookup map in one pass', () => {
const merged: MergeResult = {
merged: { version: '1.0.0', name: 'doc', children: [] },
nodeConflicts: [
{
pageId: null,
nodeId: 'frame-root',
reason: 'both-modified-same-field',
base: null,
ours: null,
theirs: null,
},
],
docFieldConflicts: [
{
field: 'variables',
path: 'variables.color-1.value',
base: 'red',
ours: 'blue',
theirs: 'green',
},
],
};
const { bag, conflictMap } = buildConflictBag(merged);
expect(bag.nodeConflicts).toHaveLength(1);
expect(bag.nodeConflicts[0].id).toBe('node:_:frame-root');
expect(bag.docFieldConflicts[0].id).toBe('field:variables:variables.color-1.value');
expect(conflictMap.size).toBe(2);
expect(conflictMap.get('node:_:frame-root')).toBeDefined();
expect(conflictMap.get('field:variables:variables.color-1.value')).toBeDefined();
});
});