diff --git a/test/tools/linear-comment-tool.test.ts b/test/tools/linear-comment-tool.test.ts index 11f3227..b48e90a 100644 --- a/test/tools/linear-comment-tool.test.ts +++ b/test/tools/linear-comment-tool.test.ts @@ -21,11 +21,6 @@ beforeEach(() => { }) describe('linear_comment tool', () => { - it('has correct name', () => { - const tool = createCommentTool() - expect(tool.name).toBe('linear_comment') - }) - describe('list', () => { it('returns comments for an issue', async () => { mockedResolveIssueId.mockResolvedValue('uuid-1') diff --git a/test/tools/linear-issue-tool.test.ts b/test/tools/linear-issue-tool.test.ts index baba4fb..f69a1e6 100644 --- a/test/tools/linear-issue-tool.test.ts +++ b/test/tools/linear-issue-tool.test.ts @@ -27,47 +27,80 @@ function parse(result: { content: { type: string; text?: string }[] }) { return text ? JSON.parse(text) : undefined } +function getMutationInput(callIndex = 0): Record { + return (mockedGraphql.mock.calls[callIndex][1] as { input: Record }).input +} + beforeEach(() => { vi.clearAllMocks() }) describe('linear_issue tool', () => { - it('has correct name', () => { - const tool = createIssueTool() - expect(tool.name).toBe('linear_issue') - }) - describe('view', () => { - it('returns issue details', async () => { - mockedResolveIssueId.mockResolvedValue('uuid-1') - const issue = { - id: 'uuid-1', - identifier: 'ENG-42', - title: 'Fix bug', - state: { name: 'Todo' }, - } - mockedGraphql.mockResolvedValue({ issue }) - - const tool = createIssueTool() - const result = await tool.execute('call-1', { - action: 'view', - issueId: 'ENG-42', + it('resolves identifier and fetches full issue details', async () => { + mockedResolveIssueId.mockResolvedValue('uuid-42') + mockedGraphql.mockResolvedValue({ + issue: { + id: 'uuid-42', + identifier: 'ENG-42', + title: 'Fix bug', + state: { name: 'Todo' }, + }, }) - const data = parse(result) + + const data = parse( + await createIssueTool().execute('call-1', { + action: 'view', + issueId: 'ENG-42', + }), + ) + + expect(mockedResolveIssueId).toHaveBeenCalledWith('ENG-42') expect(data.identifier).toBe('ENG-42') expect(data.title).toBe('Fix bug') }) it('returns error without issueId', async () => { - const tool = createIssueTool() - const result = await tool.execute('call-1', { action: 'view' }) - const data = parse(result) + const data = parse(await createIssueTool().execute('call-1', { action: 'view' })) expect(data.error).toContain('issueId is required') }) }) describe('list', () => { - it('returns filtered issues', async () => { + it('applies no filter and defaults limit to 50 when called with no params', async () => { + mockedGraphql.mockResolvedValue({ issues: { nodes: [] } }) + + await createIssueTool().execute('call-1', { action: 'list' }) + + const vars = mockedGraphql.mock.calls[0][1] as Record + expect(vars.first).toBe(50) + expect(vars.state).toBeUndefined() + expect(vars.assignee).toBeUndefined() + expect(vars.team).toBeUndefined() + expect(vars.project).toBeUndefined() + }) + + it('passes state, team, assignee, and project as query variables', async () => { + mockedGraphql.mockResolvedValue({ issues: { nodes: [] } }) + + await createIssueTool().execute('call-1', { + action: 'list', + state: 'In Progress', + team: 'eng', + assignee: 'alice@example.com', + project: 'Alpha', + limit: 10, + }) + + const vars = mockedGraphql.mock.calls[0][1] as Record + expect(vars.state).toBe('In Progress') + expect(vars.team).toBe('ENG') // uppercased + expect(vars.assignee).toBe('alice@example.com') + expect(vars.project).toBe('Alpha') + expect(vars.first).toBe(10) + }) + + it('returns the issues from the API', async () => { mockedGraphql.mockResolvedValue({ issues: { nodes: [ @@ -77,53 +110,36 @@ describe('linear_issue tool', () => { }, }) - const tool = createIssueTool() - const result = await tool.execute('call-1', { - action: 'list', - state: 'In Progress', - team: 'ENG', - }) - const data = parse(result) + const data = parse(await createIssueTool().execute('call-1', { action: 'list' })) expect(data.issues).toHaveLength(2) - }) - - it('lists without filters', async () => { - mockedGraphql.mockResolvedValue({ - issues: { nodes: [] }, - }) - - const tool = createIssueTool() - const result = await tool.execute('call-1', { action: 'list' }) - const data = parse(result) - expect(data.issues).toEqual([]) + expect(data.issues[0].identifier).toBe('ENG-1') }) }) describe('create', () => { - it('creates an issue with all fields', async () => { - mockedResolveTeamId.mockResolvedValue('team-1') - mockedResolveStateId.mockResolvedValue('state-1') - mockedResolveUserId.mockResolvedValue('user-1') - mockedResolveProjectId.mockResolvedValue('proj-1') + it('composes mutation input correctly from all resolved fields', async () => { + mockedResolveTeamId.mockResolvedValue('team-uuid') + mockedResolveStateId.mockResolvedValue('state-uuid') + mockedResolveUserId.mockResolvedValue('user-uuid') + mockedResolveProjectId.mockResolvedValue('proj-uuid') mockedResolveIssueId.mockResolvedValue('parent-uuid') - mockedResolveLabelIds.mockResolvedValue(['label-1']) + mockedResolveLabelIds.mockResolvedValue(['label-uuid']) mockedGraphql.mockResolvedValue({ issueCreate: { success: true, issue: { - id: 'new-id', + id: 'new', identifier: 'ENG-100', - url: 'https://linear.app/eng/issue/ENG-100', + url: 'u', title: 'New issue', }, }, }) - const tool = createIssueTool() - const result = await tool.execute('call-1', { + await createIssueTool().execute('call-1', { action: 'create', title: 'New issue', - description: 'Details', + description: 'Details here', team: 'ENG', state: 'Todo', assignee: 'Alice', @@ -131,20 +147,32 @@ describe('linear_issue tool', () => { parent: 'ENG-50', labels: ['Bug'], priority: 2, + dueDate: '2026-06-01', }) - const data = parse(result) - expect(data.success).toBe(true) - expect(data.issue.identifier).toBe('ENG-100') - }) - it('returns error without title', async () => { - const tool = createIssueTool() - const result = await tool.execute('call-1', { action: 'create' }) - const data = parse(result) - expect(data.error).toContain('title is required') + // Verify resolvers called with correct args + expect(mockedResolveTeamId).toHaveBeenCalledWith('ENG') + expect(mockedResolveStateId).toHaveBeenCalledWith('team-uuid', 'Todo') + expect(mockedResolveUserId).toHaveBeenCalledWith('Alice') + expect(mockedResolveProjectId).toHaveBeenCalledWith('Alpha') + expect(mockedResolveIssueId).toHaveBeenCalledWith('ENG-50') + expect(mockedResolveLabelIds).toHaveBeenCalledWith('team-uuid', ['Bug']) + + // Verify the mutation input + const input = getMutationInput() + expect(input.title).toBe('New issue') + expect(input.teamId).toBe('team-uuid') + expect(input.description).toBe('Details here') + expect(input.priority).toBe(2) + expect(input.stateId).toBe('state-uuid') + expect(input.assigneeId).toBe('user-uuid') + expect(input.projectId).toBe('proj-uuid') + expect(input.parentId).toBe('parent-uuid') + expect(input.labelIds).toEqual(['label-uuid']) + expect(input.dueDate).toBe('2026-06-01') }) - it('fetches default team when none specified', async () => { + it('fetches first available team when none specified', async () => { mockedGraphql.mockResolvedValueOnce({ teams: { nodes: [{ id: 'default-team' }] } }).mockResolvedValueOnce({ issueCreate: { success: true, @@ -152,215 +180,178 @@ describe('linear_issue tool', () => { }, }) - const tool = createIssueTool() - const result = await tool.execute('call-1', { + await createIssueTool().execute('call-1', { action: 'create', title: 'Minimal', }) - const data = parse(result) - expect(data.success).toBe(true) - }) - }) - describe('update', () => { - it('updates issue fields', async () => { - mockedResolveIssueId.mockResolvedValue('uuid-1') - mockedGraphql.mockResolvedValueOnce({ issue: { team: { id: 'team-1' } } }).mockResolvedValueOnce({ - issueUpdate: { - success: true, - issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'Updated' }, - }, - }) - mockedResolveStateId.mockResolvedValue('state-done') + const input = getMutationInput(1) + expect(input.teamId).toBe('default-team') + }) - const tool = createIssueTool() - const result = await tool.execute('call-1', { - action: 'update', - issueId: 'ENG-42', - state: 'Done', - title: 'Updated', - }) - const data = parse(result) - expect(data.success).toBe(true) + it('returns error without title', async () => { + const data = parse(await createIssueTool().execute('call-1', { action: 'create' })) + expect(data.error).toContain('title is required') }) - it('returns error without issueId', async () => { - const tool = createIssueTool() - const result = await tool.execute('call-1', { - action: 'update', - title: 'No ID', - }) - const data = parse(result) - expect(data.error).toContain('issueId is required') + it('returns error when no teams exist in workspace', async () => { + mockedGraphql.mockResolvedValueOnce({ teams: { nodes: [] } }) + const data = parse( + await createIssueTool().execute('call-1', { + action: 'create', + title: 'Orphaned', + }), + ) + expect(data.error).toContain('No teams found') }) + }) - it('appends to existing description when appendDescription is true', async () => { + describe('update', () => { + it('appends description when appendDescription is true with existing content', async () => { mockedResolveIssueId.mockResolvedValue('uuid-1') mockedGraphql - // First call: fetch team + existing description - .mockResolvedValueOnce({ issue: { team: { id: 'team-1' }, description: 'Original content.' } }) - // Second call: the update mutation + .mockResolvedValueOnce({ + issue: { team: { id: 'team-1' }, description: 'Original' }, + }) .mockResolvedValueOnce({ issueUpdate: { success: true, - issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'Task' }, + issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'T' }, }, }) - const tool = createIssueTool() - const result = await tool.execute('call-1', { + await createIssueTool().execute('call-1', { action: 'update', issueId: 'ENG-42', - description: 'Appended note.', + description: 'Appended', appendDescription: true, }) - const data = parse(result) - expect(data.success).toBe(true) - - // Verify the mutation was called with the concatenated description - const mutationCall = mockedGraphql.mock.calls[1] as [string, Record] - const input = mutationCall[1].input as Record - expect(input.description).toBe('Original content.\n\nAppended note.') - }) - - it('sets description directly (no append) when appendDescription is false', async () => { - mockedResolveIssueId.mockResolvedValue('uuid-1') - mockedGraphql.mockResolvedValueOnce({ - issueUpdate: { - success: true, - issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'Task' }, - }, - }) - - const tool = createIssueTool() - const result = await tool.execute('call-1', { - action: 'update', - issueId: 'ENG-42', - description: 'Replacement description.', - }) - const data = parse(result) - expect(data.success).toBe(true) - // Only one graphql call (no fetch for existing description) - expect(mockedGraphql).toHaveBeenCalledOnce() - const call = mockedGraphql.mock.calls[0] as [string, Record] - const input = call[1].input as Record - expect(input.description).toBe('Replacement description.') + const vars = mockedGraphql.mock.calls[1][1] as { + input: Record + } + expect(vars.input.description).toBe('Original\n\nAppended') }) - it('appends to empty description when existing description is absent', async () => { + it('sets description directly when appendDescription is true but existing is empty', async () => { mockedResolveIssueId.mockResolvedValue('uuid-1') mockedGraphql - .mockResolvedValueOnce({ issue: { team: { id: 'team-1' }, description: undefined } }) .mockResolvedValueOnce({ - issueUpdate: { success: true, issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'Task' } }, + issue: { team: { id: 'team-1' }, description: null }, + }) + .mockResolvedValueOnce({ + issueUpdate: { + success: true, + issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'T' }, + }, }) - const tool = createIssueTool() - await tool.execute('call-1', { + await createIssueTool().execute('call-1', { action: 'update', issueId: 'ENG-42', - description: 'First note.', + description: 'Fresh', appendDescription: true, }) - const mutationCall = mockedGraphql.mock.calls[1] as [string, Record] - const input = mutationCall[1].input as Record - // No prefix when existing description is empty - expect(input.description).toBe('First note.') - }) - - it('updates labels via resolveLabelIds', async () => { - mockedResolveIssueId.mockResolvedValue('uuid-1') - mockedResolveLabelIds.mockResolvedValue(['label-bug', 'label-feat']) - mockedGraphql.mockResolvedValueOnce({ issue: { team: { id: 'team-1' } } }).mockResolvedValueOnce({ - issueUpdate: { success: true, issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'Task' } }, - }) - - const tool = createIssueTool() - await tool.execute('call-1', { - action: 'update', - issueId: 'ENG-42', - labels: ['Bug', 'Feature'], - }) - - expect(mockedResolveLabelIds).toHaveBeenCalledWith('team-1', ['Bug', 'Feature']) - - const mutationCall = mockedGraphql.mock.calls[1] as [string, Record] - const input = mutationCall[1].input as Record - expect(input.labelIds).toEqual(['label-bug', 'label-feat']) + const vars = mockedGraphql.mock.calls[1][1] as { + input: Record + } + expect(vars.input.description).toBe('Fresh') }) it('clears dueDate when empty string is passed', async () => { mockedResolveIssueId.mockResolvedValue('uuid-1') - mockedGraphql.mockResolvedValueOnce({ - issueUpdate: { success: true, issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'Task' } }, + mockedGraphql.mockResolvedValue({ + issueUpdate: { + success: true, + issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'T' }, + }, }) - const tool = createIssueTool() - await tool.execute('call-1', { + await createIssueTool().execute('call-1', { action: 'update', issueId: 'ENG-42', dueDate: '', }) - const call = mockedGraphql.mock.calls[0] as [string, Record] - const input = call[1].input as Record - expect(input.dueDate).toBeNull() + const vars = mockedGraphql.mock.calls[0][1] as { + input: Record + } + expect(vars.input.dueDate).toBeNull() }) - it('sets dueDate when a date string is passed', async () => { + it('resolves labels against the issue team', async () => { mockedResolveIssueId.mockResolvedValue('uuid-1') - mockedGraphql.mockResolvedValueOnce({ - issueUpdate: { success: true, issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'Task' } }, - }) + mockedResolveLabelIds.mockResolvedValue(['label-bug']) + mockedGraphql + .mockResolvedValueOnce({ + issue: { team: { id: 'team-eng' }, description: null }, + }) + .mockResolvedValueOnce({ + issueUpdate: { + success: true, + issue: { id: 'uuid-1', identifier: 'ENG-42', title: 'T' }, + }, + }) - const tool = createIssueTool() - await tool.execute('call-1', { + await createIssueTool().execute('call-1', { action: 'update', issueId: 'ENG-42', - dueDate: '2026-03-18', + labels: ['Bug'], }) - const call = mockedGraphql.mock.calls[0] as [string, Record] - const input = call[1].input as Record - expect(input.dueDate).toBe('2026-03-18') + expect(mockedResolveLabelIds).toHaveBeenCalledWith('team-eng', ['Bug']) + const vars = mockedGraphql.mock.calls[1][1] as { + input: Record + } + expect(vars.input.labelIds).toEqual(['label-bug']) + }) + + it('returns error without issueId', async () => { + const data = parse( + await createIssueTool().execute('call-1', { + action: 'update', + title: 'No ID', + }), + ) + expect(data.error).toContain('issueId is required') }) }) describe('delete', () => { - it('deletes an issue', async () => { - mockedResolveIssueId.mockResolvedValue('uuid-1') - mockedGraphql.mockResolvedValue({ - issueDelete: { success: true }, - }) - - const tool = createIssueTool() - const result = await tool.execute('call-1', { - action: 'delete', - issueId: 'ENG-42', - }) - const data = parse(result) + it('resolves identifier and sends delete mutation', async () => { + mockedResolveIssueId.mockResolvedValue('uuid-42') + mockedGraphql.mockResolvedValue({ issueDelete: { success: true } }) + + const data = parse( + await createIssueTool().execute('call-1', { + action: 'delete', + issueId: 'ENG-42', + }), + ) + + expect(mockedResolveIssueId).toHaveBeenCalledWith('ENG-42') + const vars = mockedGraphql.mock.calls[0][1] as { id: string } + expect(vars.id).toBe('uuid-42') expect(data.success).toBe(true) + expect(data.issueId).toBe('ENG-42') }) it('returns error without issueId', async () => { - const tool = createIssueTool() - const result = await tool.execute('call-1', { action: 'delete' }) - const data = parse(result) + const data = parse(await createIssueTool().execute('call-1', { action: 'delete' })) expect(data.error).toContain('issueId is required') }) }) - it('catches and returns errors from the API', async () => { + it('surfaces API errors as structured error response rather than throwing', async () => { mockedResolveIssueId.mockRejectedValue(new Error('Network failure')) - const tool = createIssueTool() - const result = await tool.execute('call-1', { - action: 'view', - issueId: 'ENG-1', - }) - const data = parse(result) + const data = parse( + await createIssueTool().execute('call-1', { + action: 'view', + issueId: 'ENG-1', + }), + ) expect(data.error).toContain('Network failure') }) }) diff --git a/test/tools/linear-project-tool.test.ts b/test/tools/linear-project-tool.test.ts index c6ccc86..7677977 100644 --- a/test/tools/linear-project-tool.test.ts +++ b/test/tools/linear-project-tool.test.ts @@ -21,11 +21,6 @@ beforeEach(() => { }) describe('linear_project tool', () => { - it('has correct name', () => { - const tool = createProjectTool() - expect(tool.name).toBe('linear_project') - }) - describe('list', () => { it('returns projects', async () => { mockedGraphql.mockResolvedValue({ diff --git a/test/tools/linear-relation-tool.test.ts b/test/tools/linear-relation-tool.test.ts index e72cae9..0d9519b 100644 --- a/test/tools/linear-relation-tool.test.ts +++ b/test/tools/linear-relation-tool.test.ts @@ -21,11 +21,6 @@ beforeEach(() => { }) describe('linear_relation tool', () => { - it('has correct name', () => { - const tool = createRelationTool() - expect(tool.name).toBe('linear_relation') - }) - describe('list', () => { it('returns relations and inverse relations', async () => { mockedResolveIssueId.mockResolvedValue('uuid-1') diff --git a/test/tools/linear-team-tool.test.ts b/test/tools/linear-team-tool.test.ts index 492e386..3113e50 100644 --- a/test/tools/linear-team-tool.test.ts +++ b/test/tools/linear-team-tool.test.ts @@ -19,11 +19,6 @@ beforeEach(() => { }) describe('linear_team tool', () => { - it('has correct name', () => { - const tool = createTeamTool() - expect(tool.name).toBe('linear_team') - }) - describe('list', () => { it('returns all teams', async () => { mockedGraphql.mockResolvedValue({ diff --git a/test/tools/linear-view-tool.test.ts b/test/tools/linear-view-tool.test.ts new file mode 100644 index 0000000..cb39c78 --- /dev/null +++ b/test/tools/linear-view-tool.test.ts @@ -0,0 +1,221 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +vi.mock('../../src/linear-api.js', () => ({ + graphql: vi.fn(), +})) + +const { graphql } = await import('../../src/linear-api.js') +const { createViewTool } = await import('../../src/tools/linear-view-tool.js') + +const mockedGraphql = vi.mocked(graphql) + +function parse(result: { content: { type: string; text?: string }[] }) { + const text = result.content.find((c) => c.type === 'text')?.text + return text ? JSON.parse(text) : undefined +} + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('linear_view tool', () => { + describe('list', () => { + it('returns all custom views', async () => { + mockedGraphql.mockResolvedValue({ + customViews: { + nodes: [ + { + id: 'view-1', + name: 'My Issues', + description: 'Issues assigned to me', + shared: false, + creator: { name: 'Alice', email: 'alice@example.com' }, + updatedAt: '2025-01-01T00:00:00Z', + }, + { + id: 'view-2', + name: 'Team Backlog', + description: null, + shared: true, + creator: { name: 'Bob', email: 'bob@example.com' }, + updatedAt: '2025-01-02T00:00:00Z', + }, + ], + }, + }) + + const data = parse(await createViewTool().execute('call-1', { action: 'list' })) + expect(data.count).toBe(2) + expect(data.views[0].name).toBe('My Issues') + expect(data.views[1].name).toBe('Team Backlog') + }) + + it('returns empty list when no views exist', async () => { + mockedGraphql.mockResolvedValue({ customViews: { nodes: [] } }) + + const data = parse(await createViewTool().execute('call-1', { action: 'list' })) + expect(data.count).toBe(0) + expect(data.views).toEqual([]) + }) + }) + + describe('get', () => { + it('fetches view by id and passes viewId as variable', async () => { + mockedGraphql.mockResolvedValue({ + customView: { id: 'view-1', name: 'My Issues' }, + }) + + const data = parse( + await createViewTool().execute('call-1', { + action: 'get', + viewId: 'view-1', + }), + ) + + const vars = mockedGraphql.mock.calls[0][1] + expect(vars).toEqual({ id: 'view-1' }) + expect(data.name).toBe('My Issues') + }) + + it('returns null when view is not found', async () => { + mockedGraphql.mockResolvedValue({ customView: null }) + + const data = parse( + await createViewTool().execute('call-1', { + action: 'get', + viewId: 'nonexistent', + }), + ) + expect(data).toBeNull() + }) + + it('returns error when viewId is missing', async () => { + const data = parse(await createViewTool().execute('call-1', { action: 'get' })) + expect(data.error).toContain('viewId is required') + }) + }) + + describe('create', () => { + it('sends all optional fields in the mutation input', async () => { + mockedGraphql.mockResolvedValue({ + customViewCreate: { + success: true, + customView: { id: 'view-new', name: 'Full View' }, + }, + }) + + await createViewTool().execute('call-1', { + action: 'create', + name: 'Full View', + description: 'A complete view', + icon: 'Eye', + color: '#00FF00', + filterData: '{"assignee":{"id":{"eq":"user-1"}}}', + shared: true, + }) + + const vars = mockedGraphql.mock.calls[0][1] as { + input: Record + } + expect(vars.input.name).toBe('Full View') + expect(vars.input.description).toBe('A complete view') + expect(vars.input.icon).toBe('Eye') + expect(vars.input.color).toBe('#00FF00') + expect(vars.input.shared).toBe(true) + expect(vars.input.filterData).toEqual({ + assignee: { id: { eq: 'user-1' } }, + }) + }) + + it('returns error when name is missing', async () => { + const data = parse(await createViewTool().execute('call-1', { action: 'create' })) + expect(data.error).toContain('name is required') + }) + + it('returns error when filterData is invalid JSON', async () => { + const data = parse( + await createViewTool().execute('call-1', { + action: 'create', + name: 'Bad Filter', + filterData: '{ not valid json', + }), + ) + expect(data.error).toBeDefined() + }) + }) + + describe('update', () => { + it('sends viewId and all provided fields as mutation variables', async () => { + mockedGraphql.mockResolvedValue({ + customViewUpdate: { + success: true, + customView: { id: 'view-1', name: 'Updated' }, + }, + }) + + await createViewTool().execute('call-1', { + action: 'update', + viewId: 'view-1', + name: 'Updated', + description: 'New desc', + icon: 'Check', + color: '#0000FF', + shared: false, + filterData: '{"state":{"type":{"eq":"started"}}}', + }) + + const vars = mockedGraphql.mock.calls[0][1] as { + id: string + input: Record + } + expect(vars.id).toBe('view-1') + expect(vars.input.name).toBe('Updated') + expect(vars.input.description).toBe('New desc') + expect(vars.input.icon).toBe('Check') + expect(vars.input.color).toBe('#0000FF') + expect(vars.input.shared).toBe(false) + expect(vars.input.filterData).toEqual({ + state: { type: { eq: 'started' } }, + }) + }) + + it('returns error when viewId is missing', async () => { + const data = parse( + await createViewTool().execute('call-1', { + action: 'update', + name: 'No ID', + }), + ) + expect(data.error).toContain('viewId is required') + }) + }) + + describe('delete', () => { + it('sends viewId as mutation variable', async () => { + mockedGraphql.mockResolvedValue({ customViewDelete: { success: true } }) + + const data = parse( + await createViewTool().execute('call-1', { + action: 'delete', + viewId: 'view-99', + }), + ) + + const vars = mockedGraphql.mock.calls[0][1] + expect(vars).toEqual({ id: 'view-99' }) + expect(data.success).toBe(true) + }) + + it('returns error when viewId is missing', async () => { + const data = parse(await createViewTool().execute('call-1', { action: 'delete' })) + expect(data.error).toContain('viewId is required') + }) + }) + + it('surfaces API errors as structured error response rather than throwing', async () => { + mockedGraphql.mockRejectedValue(new Error('Rate limit exceeded')) + + const data = parse(await createViewTool().execute('call-1', { action: 'list' })) + expect(data.error).toContain('Rate limit exceeded') + }) +}) diff --git a/test/tools/queue-tool.test.ts b/test/tools/queue-tool.test.ts index 4d12368..792cf98 100644 --- a/test/tools/queue-tool.test.ts +++ b/test/tools/queue-tool.test.ts @@ -25,16 +25,6 @@ afterEach(() => { }) describe('linear_queue tool', () => { - it('has correct name and description', () => { - const queue = new InboxQueue(QUEUE_PATH) - const tool = createQueueTool(queue) - expect(tool.name).toBe('linear_queue') - expect(tool.description).toContain('peek') - expect(tool.description).toContain('pop') - expect(tool.description).toContain('drain') - expect(tool.description).toContain('complete') - }) - it('peek returns empty items on empty queue', async () => { const queue = new InboxQueue(QUEUE_PATH) const tool = createQueueTool(queue) @@ -111,7 +101,10 @@ describe('linear_queue tool', () => { await queue.pop() // claim it const tool = createQueueTool(queue) - const result = await tool.execute('call-1', { action: 'complete', issueId: 'ENG-42' }) + const result = await tool.execute('call-1', { + action: 'complete', + issueId: 'ENG-42', + }) const data = parse(result) expect(data.completed).toBe(true) expect(data.issueId).toBe('ENG-42')