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).
123 lines
3.6 KiB
TypeScript
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();
|
|
});
|
|
});
|