Skip to content

Commit 3b0f9b0

Browse files
fix(tables): compose copilot commands atomically
1 parent 32dc7a2 commit 3b0f9b0

19 files changed

Lines changed: 2322 additions & 1704 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 & 44 deletions
This file was deleted.
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+
})
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import type { CopilotTableDelegationContext } from '@/lib/copilot/auth/table-delegation'
2+
import { resolveCopilotTablePrincipal } from '@/lib/copilot/auth/table-delegation'
3+
import {
4+
type AddTableGroupOutputInput,
5+
addWorkflowTableGroupOutput,
6+
type CreateTableEnrichmentGroupInput,
7+
type CreateWorkflowTableGroupInput,
8+
createTableEnrichmentGroup,
9+
createWorkflowTableGroup,
10+
type UpdateWorkflowTableGroupInput,
11+
updateWorkflowTableGroup,
12+
} from '@/lib/table/application/groups'
13+
import {
14+
type ReplaceProjectedWireRowsInput,
15+
replaceProjectedWireRows,
16+
} from '@/lib/table/application/rows'
17+
import {
18+
type CreateTableFromWorkspaceFileInput,
19+
createTableFromWorkspaceFile,
20+
type ImportWorkspaceFileInput,
21+
importWorkspaceFileIntoTable,
22+
} from '@/lib/table/application/workspace-file-imports'
23+
24+
const INHERITED_COPILOT_RATE_POLICY = {
25+
kind: 'inherited_copilot_request',
26+
reason: 'The authenticated Copilot request owns request-rate admission.',
27+
} as const
28+
29+
const NO_DIRECT_PROVIDER_COST_POLICY = {
30+
kind: 'none',
31+
reason: 'This command does not invoke a paid provider; table quota and storage limits apply.',
32+
} as const
33+
34+
export const copilotReplaceProjectedWireRowsPolicy = {
35+
rate: INHERITED_COPILOT_RATE_POLICY,
36+
cost: NO_DIRECT_PROVIDER_COST_POLICY,
37+
} as const
38+
39+
export function executeCopilotReplaceProjectedWireRows(
40+
context: CopilotTableDelegationContext | undefined,
41+
input: ReplaceProjectedWireRowsInput
42+
) {
43+
return replaceProjectedWireRows.execute({
44+
principal: resolveCopilotTablePrincipal(context, input.tableId),
45+
input,
46+
})
47+
}
48+
49+
export const copilotCreateWorkflowTableGroupPolicy = {
50+
rate: INHERITED_COPILOT_RATE_POLICY,
51+
cost: NO_DIRECT_PROVIDER_COST_POLICY,
52+
} as const
53+
54+
export function executeCopilotCreateWorkflowTableGroup(
55+
context: CopilotTableDelegationContext | undefined,
56+
input: CreateWorkflowTableGroupInput
57+
) {
58+
return createWorkflowTableGroup.execute({
59+
principal: resolveCopilotTablePrincipal(context, input.tableId),
60+
input,
61+
})
62+
}
63+
64+
export const copilotUpdateWorkflowTableGroupPolicy = {
65+
rate: INHERITED_COPILOT_RATE_POLICY,
66+
cost: NO_DIRECT_PROVIDER_COST_POLICY,
67+
} as const
68+
69+
export function executeCopilotUpdateWorkflowTableGroup(
70+
context: CopilotTableDelegationContext | undefined,
71+
input: UpdateWorkflowTableGroupInput
72+
) {
73+
return updateWorkflowTableGroup.execute({
74+
principal: resolveCopilotTablePrincipal(context, input.tableId),
75+
input,
76+
})
77+
}
78+
79+
export const copilotAddWorkflowTableGroupOutputPolicy = {
80+
rate: INHERITED_COPILOT_RATE_POLICY,
81+
cost: NO_DIRECT_PROVIDER_COST_POLICY,
82+
} as const
83+
84+
export function executeCopilotAddWorkflowTableGroupOutput(
85+
context: CopilotTableDelegationContext | undefined,
86+
input: AddTableGroupOutputInput
87+
) {
88+
return addWorkflowTableGroupOutput.execute({
89+
principal: resolveCopilotTablePrincipal(context, input.tableId),
90+
input,
91+
})
92+
}
93+
94+
export const copilotCreateTableEnrichmentGroupPolicy = {
95+
rate: INHERITED_COPILOT_RATE_POLICY,
96+
cost: NO_DIRECT_PROVIDER_COST_POLICY,
97+
} as const
98+
99+
export function executeCopilotCreateTableEnrichmentGroup(
100+
context: CopilotTableDelegationContext | undefined,
101+
input: CreateTableEnrichmentGroupInput
102+
) {
103+
return createTableEnrichmentGroup.execute({
104+
principal: resolveCopilotTablePrincipal(context, input.tableId),
105+
input,
106+
})
107+
}
108+
109+
export const copilotCreateTableFromWorkspaceFilePolicy = {
110+
rate: INHERITED_COPILOT_RATE_POLICY,
111+
cost: NO_DIRECT_PROVIDER_COST_POLICY,
112+
} as const
113+
114+
export function executeCopilotCreateTableFromWorkspaceFile(
115+
context: CopilotTableDelegationContext | undefined,
116+
input: CreateTableFromWorkspaceFileInput
117+
) {
118+
return createTableFromWorkspaceFile.execute({
119+
principal: resolveCopilotTablePrincipal(context),
120+
input,
121+
})
122+
}
123+
124+
export const copilotImportWorkspaceFileIntoTablePolicy = {
125+
rate: INHERITED_COPILOT_RATE_POLICY,
126+
cost: NO_DIRECT_PROVIDER_COST_POLICY,
127+
} as const
128+
129+
export function executeCopilotImportWorkspaceFileIntoTable(
130+
context: CopilotTableDelegationContext | undefined,
131+
input: ImportWorkspaceFileInput
132+
) {
133+
return importWorkspaceFileIntoTable.execute({
134+
principal: resolveCopilotTablePrincipal(context, input.tableId),
135+
input,
136+
})
137+
}

0 commit comments

Comments
 (0)