|
1 | 1 | /** @vitest-environment node */ |
2 | | -import { queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' |
| 2 | +import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing' |
3 | 3 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
4 | 4 |
|
5 | | -const mocks = vi.hoisted(() => ({ latest: vi.fn(), stop: vi.fn(), signal: vi.fn() })) |
| 5 | +const mocks = vi.hoisted(() => ({ |
| 6 | + latest: vi.fn(), |
| 7 | + stop: vi.fn(), |
| 8 | + signal: vi.fn(), |
| 9 | + permissions: vi.fn(), |
| 10 | +})) |
6 | 11 | vi.mock('@/lib/mothership/async-runs/repository', () => ({ |
7 | 12 | getLatestRunForStream: mocks.latest, |
8 | 13 | requestRunStop: mocks.stop, |
9 | | - areStreamToolExecutionsSettled: vi.fn(), |
10 | | - getUnsettledClientWorkflowExecutions: vi.fn(), |
11 | | - getUnsettledStreamSandboxProcesses: vi.fn(), |
| 14 | + areStreamToolExecutionsSettled: vi.fn(async () => true), |
| 15 | + getUnsettledClientWorkflowExecutions: vi.fn(async () => []), |
| 16 | + getUnsettledStreamSandboxProcesses: vi.fn(async () => []), |
12 | 17 | })) |
13 | 18 | vi.mock('@/lib/mothership/request/session/explicit-abort', () => ({ |
14 | 19 | requestExplicitStreamAbort: mocks.signal, |
15 | 20 | })) |
| 21 | +vi.mock('@/lib/mothership/request/session', () => ({ |
| 22 | + abortActiveStream: vi.fn(), |
| 23 | + waitForPendingChatStream: vi.fn(async () => true), |
| 24 | + releasePendingChatStream: vi.fn(), |
| 25 | +})) |
| 26 | +vi.mock('@/lib/permission-groups/resolve.server', async (importOriginal) => ({ |
| 27 | + ...(await importOriginal<typeof import('@/lib/permission-groups/resolve.server')>()), |
| 28 | + getUserPermissionConfigForOrganization: mocks.permissions, |
| 29 | +})) |
16 | 30 | vi.mock('@/lib/auth/ban', () => ({ getActivelyBannedUserIds: async () => [] })) |
17 | 31 |
|
18 | 32 | import { abortRun } from '@/lib/mothership/request/application/controls' |
19 | 33 |
|
20 | 34 | beforeEach(() => { |
21 | 35 | vi.clearAllMocks() |
22 | 36 | resetDbChainMock() |
| 37 | + mocks.permissions |
| 38 | + .mockReset() |
| 39 | + .mockResolvedValue({ hideCopilot: true, disableWorkspaceCreation: true }) |
| 40 | + mocks.stop.mockReset().mockResolvedValue(null) |
| 41 | + mocks.signal.mockReset().mockResolvedValue({ settled: true }) |
23 | 42 | mocks.latest.mockResolvedValue({ |
24 | 43 | chatId: 'chat', |
25 | 44 | workspaceId: null, |
26 | 45 | organizationId: 'organization', |
27 | 46 | }) |
28 | 47 | }) |
29 | 48 | describe('abort authorization before service signaling', () => { |
| 49 | + const principal = { kind: 'session', userId: 'actor', sessionId: 'session' } as const |
| 50 | + const input = { streamId: 'stream', chatId: 'chat' } |
| 51 | + const ownedChat = { |
| 52 | + userId: 'actor', |
| 53 | + workspaceId: null, |
| 54 | + organizationId: 'organization', |
| 55 | + type: 'mothership', |
| 56 | + } |
| 57 | + |
| 58 | + it('persists and forwards Stop for a current member after chat and Build capabilities are revoked', async () => { |
| 59 | + queueTableRows(schemaMock.copilotChats, [ownedChat]) |
| 60 | + queueTableRows(schemaMock.copilotChats, [ownedChat]) |
| 61 | + queueTableRows(schemaMock.member, [{ role: 'member' }]) |
| 62 | + mocks.stop.mockResolvedValue({ |
| 63 | + chatId: 'chat', |
| 64 | + workspaceId: null, |
| 65 | + organizationId: 'organization', |
| 66 | + }) |
| 67 | + |
| 68 | + await expect(abortRun.execute({ principal, input })).resolves.toEqual({ |
| 69 | + aborted: true, |
| 70 | + settled: true, |
| 71 | + }) |
| 72 | + |
| 73 | + expect(mocks.stop).toHaveBeenCalledWith({ |
| 74 | + streamId: 'stream', |
| 75 | + chatId: 'chat', |
| 76 | + userId: 'actor', |
| 77 | + organizationId: 'organization', |
| 78 | + workspaceId: undefined, |
| 79 | + }) |
| 80 | + expect(mocks.signal).toHaveBeenCalledWith({ |
| 81 | + streamId: 'stream', |
| 82 | + chatId: 'chat', |
| 83 | + userId: 'actor', |
| 84 | + timeoutMs: 3000, |
| 85 | + }) |
| 86 | + expect(mocks.permissions).not.toHaveBeenCalled() |
| 87 | + }) |
| 88 | + |
| 89 | + it('rejects a removed member before persisting or forwarding Stop', async () => { |
| 90 | + queueTableRows(schemaMock.copilotChats, [ownedChat]) |
| 91 | + queueTableRows(schemaMock.member, []) |
| 92 | + await expect(abortRun.execute({ principal, input })).rejects.toMatchObject({ |
| 93 | + code: 'not_found', |
| 94 | + }) |
| 95 | + expect(mocks.stop).not.toHaveBeenCalled() |
| 96 | + expect(mocks.signal).not.toHaveBeenCalled() |
| 97 | + }) |
| 98 | + |
| 99 | + it('propagates a live membership lookup failure before persisting or forwarding Stop', async () => { |
| 100 | + const error = new Error('membership database unavailable') |
| 101 | + dbChainMockFns.limit.mockResolvedValueOnce([ownedChat]).mockRejectedValueOnce(error) |
| 102 | + await expect(abortRun.execute({ principal, input })).rejects.toBe(error) |
| 103 | + expect(mocks.stop).not.toHaveBeenCalled() |
| 104 | + expect(mocks.signal).not.toHaveBeenCalled() |
| 105 | + }) |
| 106 | + |
30 | 107 | it('rejects another user’s canonical chat before persisting or forwarding Stop', async () => { |
31 | 108 | queueTableRows(schemaMock.copilotChats, [ |
32 | 109 | { userId: 'other', workspaceId: null, organizationId: 'organization', type: 'mothership' }, |
|
0 commit comments