openpencil/apps/desktop/git/__tests__/repo-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

123 lines
3.6 KiB
TypeScript

// apps/desktop/git/__tests__/repo-session.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import {
registerSession,
getSession,
updateTrackedFile,
updateCandidates,
unregisterSession,
clearAllSessions,
sessionCount,
setInflightMerge,
clearInflightMerge,
} from '../repo-session';
import type { IsoRepoHandle } from '../git-iso';
const stubHandle: IsoRepoHandle = {
dir: '/tmp/stub',
gitdir: '/tmp/stub/.git',
mode: 'folder',
};
describe('repo-session', () => {
beforeEach(() => {
clearAllSessions();
});
it('registerSession allocates a unique repoId and getSession round-trips', () => {
const a = registerSession({
handle: stubHandle,
trackedFilePath: '/tmp/stub/a.op',
candidateFiles: [],
engineKind: 'iso',
});
const b = registerSession({
handle: stubHandle,
trackedFilePath: '/tmp/stub/b.op',
candidateFiles: [],
engineKind: 'iso',
});
expect(a.repoId).not.toBe(b.repoId);
expect(getSession(a.repoId)?.trackedFilePath).toBe('/tmp/stub/a.op');
expect(getSession(b.repoId)?.trackedFilePath).toBe('/tmp/stub/b.op');
expect(sessionCount()).toBe(2);
});
it('updateTrackedFile mutates the session and returns true; unknown id returns false', () => {
const s = registerSession({
handle: stubHandle,
trackedFilePath: null,
candidateFiles: [],
engineKind: 'iso',
});
expect(updateTrackedFile(s.repoId, '/tmp/stub/picked.op')).toBe(true);
expect(getSession(s.repoId)?.trackedFilePath).toBe('/tmp/stub/picked.op');
expect(updateTrackedFile('not-a-real-id', '/tmp/x.op')).toBe(false);
});
it('updateCandidates replaces the cached candidate list', () => {
const s = registerSession({
handle: stubHandle,
trackedFilePath: null,
candidateFiles: [],
engineKind: 'iso',
});
expect(
updateCandidates(s.repoId, [
{
path: '/tmp/stub/a.op',
relativePath: 'a.op',
milestoneCount: 0,
autosaveCount: 0,
lastCommitAt: null,
lastCommitMessage: null,
},
]),
).toBe(true);
expect(getSession(s.repoId)?.candidateFiles).toHaveLength(1);
});
it('unregisterSession removes the entry and getSession returns undefined', () => {
const s = registerSession({
handle: stubHandle,
trackedFilePath: null,
candidateFiles: [],
engineKind: 'iso',
});
expect(unregisterSession(s.repoId)).toBe(true);
expect(getSession(s.repoId)).toBeUndefined();
expect(unregisterSession(s.repoId)).toBe(false); // already gone
});
it('setInflightMerge and clearInflightMerge mutate the session', () => {
const s = registerSession({
handle: stubHandle,
trackedFilePath: '/tmp/stub/a.op',
candidateFiles: [],
engineKind: 'iso',
});
expect(getSession(s.repoId)?.inflightMerge).toBeNull();
// Minimal InflightMerge stub (types cast bypasses full shape — the test
// just exercises the registry mutators, not the merge logic).
const merge = {
oursCommit: 'a'.repeat(40),
theirsCommit: 'b'.repeat(40),
baseCommit: 'c'.repeat(40),
mergeResult: {
merged: { version: '1.0.0', name: 'd', children: [] },
nodeConflicts: [],
docFieldConflicts: [],
},
conflictMap: new Map(),
resolutions: new Map(),
defaultMessage: 'Merge',
};
expect(setInflightMerge(s.repoId, merge as never)).toBe(true);
expect(getSession(s.repoId)?.inflightMerge).toBe(merge);
expect(clearInflightMerge(s.repoId)).toBe(true);
expect(getSession(s.repoId)?.inflightMerge).toBeNull();
});
});