v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes
Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
33
apps/server/src/services/__tests__/agents.test.ts
Normal file
33
apps/server/src/services/__tests__/agents.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isAgentRegistryMarkdown, parseAgentsMd } from '../agents.js';
|
||||
|
||||
describe('isAgentRegistryMarkdown', () => {
|
||||
it('rejects Cursor navigation AGENTS.md at repo root', () => {
|
||||
expect(
|
||||
isAgentRegistryMarkdown('# Agent navigation\n\n## Doc map\n'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts the global data/AGENTS.md registry shape', () => {
|
||||
expect(isAgentRegistryMarkdown('# Agents\n\n## Code Reviewer\n---\n')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseAgentsMd', () => {
|
||||
it('does not emit errors for navigation sections when file is skipped upstream', () => {
|
||||
// When isAgentRegistryMarkdown returns false, getAgentsForProject never calls this.
|
||||
// Sanity: a nav-shaped file would produce six "missing fence" errors if parsed.
|
||||
const nav = `# Agent navigation
|
||||
|
||||
## Doc map
|
||||
| Need | Read |
|
||||
|------|------|
|
||||
|
||||
## Task routing
|
||||
Start here
|
||||
`;
|
||||
const r = parseAgentsMd(nav);
|
||||
expect(r.agents).toHaveLength(0);
|
||||
expect(r.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -226,6 +226,76 @@ describe('buildMessagesPayload', async () => {
|
||||
expect(result[4]).toMatchObject({ role: 'assistant', content: 'here it is' });
|
||||
});
|
||||
|
||||
it('strips assistant tool_calls when matching tool results are missing', async () => {
|
||||
const session = makeSession();
|
||||
const project = makeProject();
|
||||
const toolCall: ToolCall = {
|
||||
id: 'call_orphan',
|
||||
name: 'grep',
|
||||
args: { pattern: 'foo' },
|
||||
};
|
||||
const history: Message[] = [
|
||||
makeMessage('user', 'search'),
|
||||
makeMessage('assistant', 'partial answer', { tool_calls: [toolCall] }),
|
||||
makeMessage('assistant', 'final answer'),
|
||||
];
|
||||
const result = await buildMessagesPayload(session, project, history);
|
||||
// tool_calls stripped from the orphan turn; text content kept.
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result[1]).toMatchObject({ role: 'user', content: 'search' });
|
||||
expect(result[2]).toMatchObject({ role: 'assistant', content: 'partial answer' });
|
||||
expect(result[2]!.tool_calls).toBeUndefined();
|
||||
expect(result[3]).toMatchObject({ role: 'assistant', content: 'final answer' });
|
||||
});
|
||||
|
||||
it('drops tool-call-only assistant rows when tool results never arrived', async () => {
|
||||
const session = makeSession();
|
||||
const project = makeProject();
|
||||
const toolCall: ToolCall = {
|
||||
id: 'call_orphan_only',
|
||||
name: 'grep',
|
||||
args: { pattern: 'foo' },
|
||||
};
|
||||
const history: Message[] = [
|
||||
makeMessage('user', 'search'),
|
||||
makeMessage('assistant', '', { tool_calls: [toolCall] }),
|
||||
makeMessage('assistant', 'final answer'),
|
||||
];
|
||||
const result = await buildMessagesPayload(session, project, history);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[2]).toMatchObject({ role: 'assistant', content: 'final answer' });
|
||||
});
|
||||
|
||||
it('skips stray tool rows when the owning assistant tool_calls were stripped', async () => {
|
||||
const session = makeSession();
|
||||
const project = makeProject();
|
||||
const toolCallA: ToolCall = {
|
||||
id: 'call_a',
|
||||
name: 'grep',
|
||||
args: { pattern: 'foo' },
|
||||
};
|
||||
const toolCallB: ToolCall = {
|
||||
id: 'call_b',
|
||||
name: 'read',
|
||||
args: { path: 'x' },
|
||||
};
|
||||
const toolResult: ToolResult = {
|
||||
tool_call_id: 'call_a',
|
||||
output: 'match',
|
||||
truncated: false,
|
||||
};
|
||||
const history: Message[] = [
|
||||
makeMessage('user', 'search'),
|
||||
makeMessage('assistant', '', { tool_calls: [toolCallA, toolCallB] }),
|
||||
makeMessage('tool', '', { tool_results: toolResult }),
|
||||
makeMessage('assistant', 'final answer'),
|
||||
];
|
||||
const result = await buildMessagesPayload(session, project, history);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.find((m) => m.role === 'tool')).toBeUndefined();
|
||||
expect(result[2]).toMatchObject({ role: 'assistant', content: 'final answer' });
|
||||
});
|
||||
|
||||
it('skips tool rows with no tool_results', async () => {
|
||||
const session = makeSession();
|
||||
const project = makeProject();
|
||||
|
||||
Reference in New Issue
Block a user