mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(copilot): mount input tables with display-name CSV headers, not column IDs (#5121)
This commit is contained in:
@@ -89,7 +89,8 @@ describe('executeFunctionExecute table mounts', () => {
|
||||
mockExecuteTool.mockResolvedValue({ success: true })
|
||||
mockGetTableById.mockResolvedValue(table)
|
||||
mockIsFeatureEnabled.mockResolvedValue(false)
|
||||
mockQueryRows.mockResolvedValue({ rows: [{ data: { name: 'Ada' } }] })
|
||||
// Row data is keyed by stable column id at rest, not display name.
|
||||
mockQueryRows.mockResolvedValue({ rows: [{ data: { col_name: 'Ada' } }] })
|
||||
mockHasCloudStorage.mockReturnValue(true)
|
||||
mockGeneratePresignedDownloadUrl.mockResolvedValue('https://s3.example/presigned?sig=abc')
|
||||
})
|
||||
@@ -104,6 +105,53 @@ describe('executeFunctionExecute table mounts', () => {
|
||||
expect(files[0].content).toBe('name\nAda')
|
||||
})
|
||||
|
||||
it('mounts CSV with display-name headers and id-keyed values, never column ids', async () => {
|
||||
mockGetTableById.mockResolvedValue({
|
||||
id: 'tbl_2',
|
||||
workspaceId: 'ws_1',
|
||||
rowCount: 2,
|
||||
schema: {
|
||||
columns: [
|
||||
{ id: 'col_name', name: 'name', type: 'string' },
|
||||
{ id: 'col_company', name: 'company', type: 'string' },
|
||||
],
|
||||
},
|
||||
})
|
||||
mockQueryRows.mockResolvedValue({
|
||||
rows: [
|
||||
{ data: { col_name: 'Ada', col_company: 'Analytical Engine' } },
|
||||
{ data: { col_name: 'Grace', col_company: 'Navy, Inc' } },
|
||||
],
|
||||
})
|
||||
|
||||
await executeFunctionExecute({ inputTables: ['tbl_2'] }, context as never)
|
||||
|
||||
const csv = mountedFiles()[0].content as string
|
||||
const lines = csv.split('\n')
|
||||
expect(lines[0]).toBe('name,company')
|
||||
expect(lines[1]).toBe('Ada,Analytical Engine')
|
||||
// Value containing a comma is quoted.
|
||||
expect(lines[2]).toBe('Grace,"Navy, Inc"')
|
||||
// No stable column id leaks into the mounted file.
|
||||
expect(csv).not.toContain('col_name')
|
||||
expect(csv).not.toContain('col_company')
|
||||
})
|
||||
|
||||
it('reads values by column id for legacy name-keyed rows too', async () => {
|
||||
// Legacy column with no id: getColumnId falls back to name, so name-keyed data is correct.
|
||||
mockGetTableById.mockResolvedValue({
|
||||
id: 'tbl_legacy',
|
||||
workspaceId: 'ws_1',
|
||||
rowCount: 1,
|
||||
schema: { columns: [{ name: 'email', type: 'string' }] },
|
||||
})
|
||||
mockQueryRows.mockResolvedValue({ rows: [{ data: { email: 'a@b.com' } }] })
|
||||
|
||||
await executeFunctionExecute({ inputTables: ['tbl_legacy'] }, context as never)
|
||||
|
||||
expect(mountedFiles()[0].content).toBe('email\na@b.com')
|
||||
})
|
||||
|
||||
it('flag ON + cloud storage: mounts by presigned URL, no bytes through web', async () => {
|
||||
mockIsFeatureEnabled.mockImplementation(snapshotCacheOn)
|
||||
mockGetOrCreateTableSnapshot.mockResolvedValue({
|
||||
|
||||
@@ -3,6 +3,8 @@ import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/copilot/vfs/
|
||||
import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver'
|
||||
import { isPlanAliasPath, workflowAliasSandboxPath } from '@/lib/copilot/vfs/workflow-aliases'
|
||||
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
|
||||
import { getColumnId } from '@/lib/table/column-keys'
|
||||
import { formatCsvValue, neutralizeCsvFormula, toCsvRow } from '@/lib/table/export-format'
|
||||
import { queryRows } from '@/lib/table/rows/service'
|
||||
import { getTableById, listTables } from '@/lib/table/service'
|
||||
import { getOrCreateTableSnapshot, SNAPSHOT_MAX_BYTES } from '@/lib/table/snapshot-cache'
|
||||
@@ -80,7 +82,7 @@ async function resolveTableRef(
|
||||
return tablePathLookup?.get(tableName) ?? null
|
||||
}
|
||||
|
||||
async function resolveInputFiles(
|
||||
export async function resolveInputFiles(
|
||||
workspaceId: string,
|
||||
inputFiles?: unknown[],
|
||||
inputTables?: unknown[],
|
||||
@@ -333,28 +335,11 @@ async function resolveInputFiles(
|
||||
|
||||
const rows = await queryRows(table, {}, 'copilot-fn-exec')
|
||||
|
||||
const allKeys = new Set(table.schema.columns.map((column) => column.name))
|
||||
for (const row of rows.rows ?? []) {
|
||||
if (row.data && typeof row.data === 'object') {
|
||||
for (const key of Object.keys(row.data as Record<string, unknown>)) {
|
||||
allKeys.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
const headers = Array.from(allKeys)
|
||||
const csvLines = [headers.join(',')]
|
||||
for (const row of rows.rows ?? []) {
|
||||
const data = (row.data || {}) as Record<string, unknown>
|
||||
const columns = table.schema.columns
|
||||
const csvLines = [toCsvRow(columns.map((column) => neutralizeCsvFormula(column.name)))]
|
||||
for (const row of rows.rows) {
|
||||
csvLines.push(
|
||||
headers
|
||||
.map((h) => {
|
||||
const val = data[h]
|
||||
const str = val === null || val === undefined ? '' : String(val)
|
||||
return str.includes(',') || str.includes('"') || str.includes('\n')
|
||||
? `"${str.replace(/"/g, '""')}"`
|
||||
: str
|
||||
})
|
||||
.join(',')
|
||||
toCsvRow(columns.map((column) => formatCsvValue(row.data[getColumnId(column)])))
|
||||
)
|
||||
}
|
||||
const csvContent = csvLines.join('\n')
|
||||
|
||||
Reference in New Issue
Block a user