Skip to content

Commit 5688be3

Browse files
fix(tables): compose copilot commands atomically
1 parent 2856cd5 commit 5688be3

21 files changed

Lines changed: 2342 additions & 1713 deletions

apps/sim/lib/copilot/application/execute-table-use-case.test.ts

Lines changed: 0 additions & 80 deletions
This file was deleted.

apps/sim/lib/copilot/application/execute-table-use-case.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

apps/sim/lib/copilot/application/execute-workspace-use-case.test.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -97,32 +97,4 @@ describe('Copilot workspace application delegation', () => {
9797
)
9898
).toThrow('Unregistered Copilot workspace operation')
9999
})
100-
101-
it('merges a domain-defined resource scope into the trusted principal', async () => {
102-
const execute = vi.fn().mockResolvedValue({ ok: true })
103-
const executeCopilotUseCase = createCopilotWorkspaceUseCaseExecutor({
104-
audience: 'sim:skills',
105-
operations: { update: operation },
106-
resourceScope: () => ({ chatId: 'untrusted-chat', tableId: 'table-1' }),
107-
})
108-
109-
await executeCopilotUseCase(
110-
{
111-
userId: 'user-1',
112-
workspaceId: 'workspace-1',
113-
chatId: 'chat-1',
114-
toolCallId: 'call-1',
115-
copilotToolExecution: true,
116-
},
117-
{ operation, execute },
118-
{ workspaceId: 'workspace-1' }
119-
)
120-
121-
expect(execute).toHaveBeenCalledWith({
122-
principal: expect.objectContaining({
123-
resourceScope: { chatId: 'chat-1', tableId: 'table-1' },
124-
}),
125-
input: { workspaceId: 'workspace-1' },
126-
})
127-
})
128100
})

apps/sim/lib/copilot/application/execute-workspace-use-case.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { DelegatedPrincipal } from '@sim/auth/principal'
21
import {
32
type CopilotWorkspaceDelegationContext,
43
createCopilotWorkspacePrincipal,
@@ -8,7 +7,6 @@ import type { OperationUseCase, WorkspaceOperation } from '@/lib/core/applicatio
87
interface CopilotWorkspaceUseCaseExecutorOptions<O extends WorkspaceOperation> {
98
audience: string
109
operations: Readonly<Record<string, O>>
11-
resourceScope?(input: unknown): DelegatedPrincipal['resourceScope']
1210
}
1311

1412
/** Binds a domain registry to the trusted Copilot workspace execution runtime. */
@@ -29,10 +27,7 @@ export function createCopilotWorkspaceUseCaseExecutor<O extends WorkspaceOperati
2927
}
3028

3129
return useCase.execute({
32-
principal: createCopilotWorkspacePrincipal(context, {
33-
audience: options.audience,
34-
resourceScope: options.resourceScope?.(input),
35-
}),
30+
principal: createCopilotWorkspacePrincipal(context, { audience: options.audience }),
3631
input,
3732
})
3833
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const mocks = vi.hoisted(() => ({
8+
addOutput: vi.fn(),
9+
createEnrichment: vi.fn(),
10+
createFromFile: vi.fn(),
11+
createWorkflowGroup: vi.fn(),
12+
importFile: vi.fn(),
13+
replaceProjectedRows: vi.fn(),
14+
resolvePrincipal: vi.fn(),
15+
updateWorkflowGroup: vi.fn(),
16+
}))
17+
18+
vi.mock('@/lib/copilot/auth/table-delegation', () => ({
19+
resolveCopilotTablePrincipal: mocks.resolvePrincipal,
20+
}))
21+
vi.mock('@/lib/table/application/groups', () => ({
22+
addWorkflowTableGroupOutput: { execute: mocks.addOutput },
23+
createTableEnrichmentGroup: { execute: mocks.createEnrichment },
24+
createWorkflowTableGroup: { execute: mocks.createWorkflowGroup },
25+
updateWorkflowTableGroup: { execute: mocks.updateWorkflowGroup },
26+
}))
27+
vi.mock('@/lib/table/application/rows', () => ({
28+
replaceProjectedWireRows: { execute: mocks.replaceProjectedRows },
29+
}))
30+
vi.mock('@/lib/table/application/workspace-file-imports', () => ({
31+
createTableFromWorkspaceFile: { execute: mocks.createFromFile },
32+
importWorkspaceFileIntoTable: { execute: mocks.importFile },
33+
}))
34+
35+
import {
36+
copilotAddWorkflowTableGroupOutputPolicy,
37+
copilotCreateTableEnrichmentGroupPolicy,
38+
copilotCreateTableFromWorkspaceFilePolicy,
39+
copilotCreateWorkflowTableGroupPolicy,
40+
copilotImportWorkspaceFileIntoTablePolicy,
41+
copilotReplaceProjectedWireRowsPolicy,
42+
copilotUpdateWorkflowTableGroupPolicy,
43+
executeCopilotAddWorkflowTableGroupOutput,
44+
executeCopilotCreateTableEnrichmentGroup,
45+
executeCopilotCreateTableFromWorkspaceFile,
46+
executeCopilotCreateWorkflowTableGroup,
47+
executeCopilotImportWorkspaceFileIntoTable,
48+
executeCopilotReplaceProjectedWireRows,
49+
executeCopilotUpdateWorkflowTableGroup,
50+
} from '@/lib/copilot/application/table-commands'
51+
52+
const context = {
53+
userId: 'user-1',
54+
workspaceId: 'workspace-1',
55+
toolCallId: 'tool-call-1',
56+
copilotToolExecution: true,
57+
}
58+
const principal = { kind: 'delegated', audience: 'sim:tables' }
59+
60+
describe('fixed Copilot Table application commands', () => {
61+
beforeEach(() => {
62+
vi.clearAllMocks()
63+
mocks.resolvePrincipal.mockReturnValue(principal)
64+
})
65+
66+
it.each([
67+
['replace projected rows', executeCopilotReplaceProjectedWireRows, mocks.replaceProjectedRows],
68+
['create workflow group', executeCopilotCreateWorkflowTableGroup, mocks.createWorkflowGroup],
69+
['update workflow group', executeCopilotUpdateWorkflowTableGroup, mocks.updateWorkflowGroup],
70+
['add workflow output', executeCopilotAddWorkflowTableGroupOutput, mocks.addOutput],
71+
['create enrichment group', executeCopilotCreateTableEnrichmentGroup, mocks.createEnrichment],
72+
['import a workspace file', executeCopilotImportWorkspaceFileIntoTable, mocks.importFile],
73+
])(
74+
'dispatches %s to exactly one code-defined Table command',
75+
async (_label, execute, command) => {
76+
command.mockResolvedValue({ ok: true })
77+
const input = { tableId: 'table-1', workspaceId: 'workspace-1' }
78+
79+
await expect(execute(context, input as never)).resolves.toEqual({ ok: true })
80+
81+
expect(mocks.resolvePrincipal).toHaveBeenCalledWith(context, 'table-1')
82+
expect(command).toHaveBeenCalledWith({ principal, input })
83+
expect(command).toHaveBeenCalledTimes(1)
84+
}
85+
)
86+
87+
it('uses a workspace-scoped Table principal for create-from-file', async () => {
88+
mocks.createFromFile.mockResolvedValue({ kind: 'empty' })
89+
const input = { workspaceId: 'workspace-1', fileReference: 'files/people.csv' }
90+
91+
await executeCopilotCreateTableFromWorkspaceFile(context, input)
92+
93+
expect(mocks.resolvePrincipal).toHaveBeenCalledWith(context)
94+
expect(mocks.createFromFile).toHaveBeenCalledWith({ principal, input })
95+
})
96+
97+
it('declares inherited request-rate admission and no direct provider cost for every command', () => {
98+
const policies = [
99+
copilotReplaceProjectedWireRowsPolicy,
100+
copilotCreateWorkflowTableGroupPolicy,
101+
copilotUpdateWorkflowTableGroupPolicy,
102+
copilotAddWorkflowTableGroupOutputPolicy,
103+
copilotCreateTableEnrichmentGroupPolicy,
104+
copilotCreateTableFromWorkspaceFilePolicy,
105+
copilotImportWorkspaceFileIntoTablePolicy,
106+
]
107+
108+
for (const policy of policies) {
109+
expect(policy.rate.kind).toBe('inherited_copilot_request')
110+
expect(policy.rate.reason).toBeTruthy()
111+
expect(policy.cost.kind).toBe('none')
112+
expect(policy.cost.reason).toBeTruthy()
113+
}
114+
})
115+
116+
it('rejects an untrusted context before application execution', async () => {
117+
const error = new Error('trusted Copilot execution context required')
118+
mocks.resolvePrincipal.mockImplementationOnce(() => {
119+
throw error
120+
})
121+
122+
expect(() =>
123+
executeCopilotReplaceProjectedWireRows(undefined, {
124+
tableId: 'table-1',
125+
sourceRows: [],
126+
projectedRows: [],
127+
})
128+
).toThrow(error)
129+
expect(mocks.replaceProjectedRows).not.toHaveBeenCalled()
130+
})
131+
})

0 commit comments

Comments
 (0)