-
Notifications
You must be signed in to change notification settings - Fork 3.7k
fix(mothership): tenant-check outputTable writes and route them through replaceTableRows #5011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
TheodoreSpeaks
merged 94 commits into
staging
from
fix/mothership-output-table-tenant-check
Jun 13, 2026
+268
−63
Merged
Changes from all commits
Commits
Show all changes
94 commits
Select commit
Hold shift + click to select a range
0b9019d
v0.6.23: MCP fixes, remove local state in favor of server state, moth…
waleedlatif1 a54dcbe
v0.6.24: copilot feedback wiring, captcha fixes
waleedlatif1 28af223
v0.6.25: cloudwatch, cloudformation, live kb sync, linear fixes, post…
waleedlatif1 d889f32
v0.6.26: ui improvements, multiple response blocks, docx previews, ol…
waleedlatif1 316bc8c
v0.6.27: new triggers, mothership improvements, files archive, queuei…
waleedlatif1 3f508e4
v0.6.28: new docs, delete confirmation standardization, dagster integ…
waleedlatif1 d6ec115
v0.6.29: login improvements, posthog telemetry (#4026)
TheodoreSpeaks d7da35b
v0.6.30: slack trigger enhancements, connectors performance improveme…
waleedlatif1 cf233bb
v0.6.31: elevenlabs voice, trigger.dev fixes, cloud whitelabeling for…
waleedlatif1 f8f3758
v0.6.32: BYOK fixes, ui improvements, cloudwatch tools, jsm tools ext…
waleedlatif1 3c8bb40
v0.6.33: polling improvements, jsm forms tools, credentials reactquer…
waleedlatif1 d33acf4
v0.6.34: trigger.dev fixes, CI speedup, atlassian error extractor
waleedlatif1 4f40c4c
v0.6.35: additional jira fields, HITL docs, logs cleanup efficiency
waleedlatif1 cbfab1c
v0.6.36: new chunkers, sockets state machine, google sheets/drive/cal…
waleedlatif1 4309d06
v0.6.37: audit logs page, isolated-vm worker rotation, permission gro…
waleedlatif1 8b57476
v0.6.38: models page
waleedlatif1 e3d0e74
v0.6.39: billing fixes, tools audit, landing fix
waleedlatif1 0ac0539
v0.6.40: mothership tool loop, new skills, agiloft, STS, IAM integrat…
waleedlatif1 3838b6e
v0.6.41: webhooks fix, workers removal
waleedlatif1 fc07922
v0.6.42: mothership nested file reads, search modal improvements
waleedlatif1 3a1b1a8
v0.6.43: mothership billing idempotency, env var resolution fixes
waleedlatif1 46ffc49
v0.6.44: streamdown, mothership intelligence, excel extension
waleedlatif1 010435c
v0.6.45: superagent, csp, brightdata integration, gemini response for…
Sg312 c0bc62c
Merge pull request #4190 from simstudioai/staging
icecrasher321 387cc97
v0.6.46: mothership queueing, web vitals
waleedlatif1 2dbc7fd
v0.6.47: files focusing, documentation, opus 4.7
waleedlatif1 8a50f18
v0.6.48: import csv into tables, subflow fixes, CSP updates
waleedlatif1 dcf3302
v0.6.49: deploy sockets event, resolver, logs improvements, monday.co…
waleedlatif1 bc09865
v0.6.50: ppt/doc/pdf worker isolation, docs, chat, sidebar improvements
icecrasher321 5f56e46
v0.6.51: tables improvements, billing fixes, 404 pages, code hygiene
waleedlatif1 ca3bbf1
v0.6.52: data retention, docs updates, slack manifest generator, secu…
waleedlatif1 bbf400f
v0.6.53: permissions groups migration, docs updates
waleedlatif1 7c619e7
Merge pull request #4261 from simstudioai/staging
icecrasher321 64cfda5
v0.6.54: mothership tracing, db pool size increase
icecrasher321 7ca736a
v0.6.55: standardize monorepo conventions, api key hash, thinking tex…
waleedlatif1 6066fc1
v0.6.56: data retention improvements, tables column double click resi…
waleedlatif1 3422f64
Merge pull request #4285 from simstudioai/staging
waleedlatif1 595c4c3
Merge pull request #4293 from simstudioai/staging
TheodoreSpeaks d6c1bc2
v0.6.58: queue abort state machine improvement, contributing guide
icecrasher321 58a3ae2
v0.6.59: gpt 5.5, security hardening, parallel subagents rendering
icecrasher321 489f2d3
v0.6.60: copilot security improvements, slack canvas ops, retention j…
icecrasher321 6aa3fe3
v0.6.61: SAP integration, live URLs for browser use, 5xx error catego…
icecrasher321 ecbf5e5
Merge pull request #4342 from simstudioai/staging
TheodoreSpeaks 2aaf2b7
v0.6.62: firecrawl parse, new gmail tools, trace improvements, tool f…
waleedlatif1 d445b9c
v0.6.63: knowledgebase UI, folder search in mothership
waleedlatif1 4bc6a17
v0.6.64: table limits env vars, workspace files improvements, integra…
waleedlatif1 5be12f8
v0.6.65: memory fix, image uploads in files
waleedlatif1 4253e57
v0.6.66: child trace spans, reranker controls, attachment previews, l…
waleedlatif1 8d6b615
v0.6.67: VFS upload fix, posthog/copilot correlation, exa date filter…
TheodoreSpeaks efcd51a
v0.6.68: atlassian service accounts, 30 day wait block, markdown rend…
waleedlatif1 8d934f3
v0.6.69: security hardening, nextjs upgrade, SAP Concur, Emailbison i…
waleedlatif1 5ea80a8
v0.6.70: legacy workflow sanitization
icecrasher321 3cc581e
v0.6.71: build error fix
icecrasher321 273e608
Merge pull request #4496 from simstudioai/staging
TheodoreSpeaks 07b8f1b
v0.6.72: tables improvements, search and replace, logs with files, im…
waleedlatif1 dcaf3e9
v0.6.73: zustand v5 migration fix
icecrasher321 6aeb981
v0.6.74: security hardening, workers recycling, next-mdx-remote and o…
waleedlatif1 3e9849b
v0.6.75: scheduler claim-budget drain, helm chart hardening, mothersh…
TheodoreSpeaks 64d855a
v0.6.76: helm updates, media centering, lazy loading, security hardening
waleedlatif1 ab156b5
v0.6.77: mothership improvements, trigger.dev telemetry
icecrasher321 c09a2c9
v0.6.78: file block get
Sg312 6a5eebc
v0.6.79: rate limits, tables checkboxes, drizzle config changes, bill…
waleedlatif1 4efe999
v0.6.80: security hardening, nextjs minor version bump, cloudwatch to…
waleedlatif1 f69a9a0
v0.6.81: files in agent block, file block update, mermaid version upd…
waleedlatif1 db7f1c1
v0.6.82: fix duplicate migration
Sg312 dbe8e51
v0.6.83: redis TLS SNI override for IP-based REDIS_URL, zod schema fixes
TheodoreSpeaks 11bcb8f
v0.6.84: redis pub/sub SNI override, security hardening, copilot read…
TheodoreSpeaks d14af04
v0.6.85: mothership stream, resource column spacing, prospeo, findyma…
waleedlatif1 e6b3cce
v0.6.86: gemini 3.5 flash, wiza integration, CORS cleanup, railway an…
waleedlatif1 97a609a
v0.6.86: CORS updates, OAuth MCP, navigation pinning dynamic pages, g…
waleedlatif1 fde70e2
v0.6.87: performance improvements
icecrasher321 e9ee351
v0.6.88: mutex lock on oauth refresh, files export fix, hubspot trigg…
waleedlatif1 b5b2d83
v0.6.89: connectors ui, perf improvements, mcp hardening, og image
waleedlatif1 f6c9998
v0.6.90: resource breadcrumb flash fix, dedupe external URL fetches, …
icecrasher321 e532e0a
v0.6.91: file zoom, Zoom KB connector, error classifications, LiteLLM…
waleedlatif1 fd19470
v0.6.92: enrichment table column type, table run fixes, scheduled jit…
TheodoreSpeaks 856182b
v0.6.93: schedules/mcp performance improvements, integration bugfixes
icecrasher321 6bf9e96
v0.6.94: 4.8 opus, better auth upgrade, zoominfo integration, copilot…
waleedlatif1 503432c
v0.6.95: data enrichment block, nullable workflow description fix
TheodoreSpeaks a8dcdd5
v0.6.96: pinned table columns, sequence number in copilot messages, t…
waleedlatif1 2f1f633
v0.6.97: migration fix for copilot_messages
icecrasher321 e32699d
v0.6.98: redundant index, security hardening, new copilot messages ta…
waleedlatif1 12ada0c
v0.6.99: tables filter operators, copilot chat persistence consolidat…
waleedlatif1 e8f09ae
v0.6.100: auth, mothership, scopes improvements, new apify tools
icecrasher321 3ba8668
v0.6.101: 11 new knowledgebase connectors, slack scopes update, login…
waleedlatif1 1192e20
v0.6.102: support S3-compatible in object storage, GitLab code knowle…
waleedlatif1 1ce8e92
v0.6.103: readme updates, tables lifecycle improvements, new connecto…
waleedlatif1 0c2df1e
v0.7.0: vibes improvement, new UI, new tools, chat-first, mothership …
waleedlatif1 7ffc495
v0.7.1: chat voice mode model update, sim trigger, codepipeline integ…
waleedlatif1 d4722f9
v0.7.2: logs export security, code hygiene, mship cost attribution
icecrasher321 f4d22ff
v0.7.3: jira oauth scope fix, read-replica client, table wire data fi…
TheodoreSpeaks a48b4a1
v0.7.4: round-robin byok support, table block fix, db read replica ro…
waleedlatif1 49afd5f
fix(mothership): tenant-check outputTable writes and route them throu…
TheodoreSpeaks 68c3ebb
fix(mothership): fail outputTable writes per-row when any row matches…
TheodoreSpeaks File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| /** | ||
| * @vitest-environment node | ||
| */ | ||
|
|
||
| import { beforeEach, describe, expect, it, vi } from 'vitest' | ||
| import type { TableDefinition } from '@/lib/table' | ||
|
|
||
| const { mockGetTableById, mockReplaceTableRows } = vi.hoisted(() => ({ | ||
| mockGetTableById: vi.fn(), | ||
| mockReplaceTableRows: vi.fn(), | ||
| })) | ||
|
|
||
| vi.mock('@/lib/table/service', () => ({ | ||
| getTableById: mockGetTableById, | ||
| replaceTableRows: mockReplaceTableRows, | ||
| })) | ||
|
|
||
| vi.mock('@/lib/copilot/request/otel', () => ({ | ||
| withCopilotSpan: ( | ||
| _name: string, | ||
| _attrs: Record<string, unknown> | undefined, | ||
| fn: (span: unknown) => Promise<unknown> | ||
| ) => fn({ setAttribute: vi.fn(), setAttributes: vi.fn(), addEvent: vi.fn() }), | ||
| })) | ||
|
|
||
| import { FunctionExecute, Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1' | ||
| import { | ||
| maybeWriteOutputToTable, | ||
| maybeWriteReadCsvToTable, | ||
| } from '@/lib/copilot/request/tools/tables' | ||
| import type { ExecutionContext } from '@/lib/copilot/request/types' | ||
|
|
||
| function buildTable(overrides: Partial<TableDefinition> = {}): TableDefinition { | ||
| return { | ||
| id: 'tbl_1', | ||
| name: 'People', | ||
| description: null, | ||
| schema: { | ||
| columns: [ | ||
| { id: 'col_name', name: 'name', type: 'string' }, | ||
| { id: 'col_age', name: 'age', type: 'number' }, | ||
| ], | ||
| }, | ||
| metadata: null, | ||
| rowCount: 0, | ||
| maxRows: 100, | ||
| workspaceId: 'workspace-1', | ||
| createdBy: 'user-1', | ||
| archivedAt: null, | ||
| createdAt: new Date('2024-01-01'), | ||
| updatedAt: new Date('2024-01-01'), | ||
| ...overrides, | ||
| } as TableDefinition | ||
| } | ||
|
|
||
| function buildContext(overrides: Partial<ExecutionContext> = {}): ExecutionContext { | ||
| return { | ||
| userId: 'user-1', | ||
| workflowId: 'wf-1', | ||
| workspaceId: 'workspace-1', | ||
| ...overrides, | ||
| } | ||
| } | ||
|
|
||
| describe('maybeWriteOutputToTable', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks() | ||
| mockGetTableById.mockResolvedValue(buildTable()) | ||
| mockReplaceTableRows.mockResolvedValue({ deletedCount: 0, insertedCount: 2 }) | ||
| }) | ||
|
|
||
| it('rejects a table from another workspace without touching it', async () => { | ||
| mockGetTableById.mockResolvedValue(buildTable({ workspaceId: 'other-workspace' })) | ||
|
|
||
| const result = await maybeWriteOutputToTable( | ||
| FunctionExecute.id, | ||
| { outputTable: 'tbl_1' }, | ||
| { success: true, output: { result: [{ name: 'Alice' }] } }, | ||
| buildContext() | ||
| ) | ||
|
|
||
| expect(result).toEqual({ success: false, error: 'Table "tbl_1" not found' }) | ||
| expect(mockReplaceTableRows).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('replaces rows through the service with name keys remapped to column ids', async () => { | ||
| const result = await maybeWriteOutputToTable( | ||
| FunctionExecute.id, | ||
| { outputTable: 'tbl_1' }, | ||
| { | ||
| success: true, | ||
| output: { | ||
| result: [ | ||
| { name: 'Alice', age: 30 }, | ||
| { name: 'Bob', age: 40 }, | ||
| ], | ||
| }, | ||
| }, | ||
| buildContext() | ||
| ) | ||
|
|
||
| expect(result.success).toBe(true) | ||
| expect(mockReplaceTableRows).toHaveBeenCalledTimes(1) | ||
| const [data, table] = mockReplaceTableRows.mock.calls[0] | ||
| expect(data).toMatchObject({ | ||
| tableId: 'tbl_1', | ||
| workspaceId: 'workspace-1', | ||
| userId: 'user-1', | ||
| rows: [ | ||
| { col_name: 'Alice', col_age: 30 }, | ||
| { col_name: 'Bob', col_age: 40 }, | ||
| ], | ||
| }) | ||
| expect(table.id).toBe('tbl_1') | ||
| }) | ||
|
|
||
| it('fails fast when no row keys match the table columns', async () => { | ||
| const result = await maybeWriteOutputToTable( | ||
| FunctionExecute.id, | ||
| { outputTable: 'tbl_1' }, | ||
| { success: true, output: { result: [{ wrong: 1 }, { keys: 2 }] } }, | ||
| buildContext() | ||
| ) | ||
|
|
||
| expect(result.success).toBe(false) | ||
| expect(result.error).toContain('Row 1 has no keys matching columns') | ||
| expect(mockReplaceTableRows).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('fails fast when only some rows match instead of writing empty rows', async () => { | ||
| const result = await maybeWriteOutputToTable( | ||
| FunctionExecute.id, | ||
| { outputTable: 'tbl_1' }, | ||
| { success: true, output: { result: [{ name: 'Alice' }, { wrong: 'x' }] } }, | ||
| buildContext() | ||
| ) | ||
|
|
||
| expect(result.success).toBe(false) | ||
| expect(result.error).toContain('Row 2 has no keys matching columns') | ||
| expect(mockReplaceTableRows).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('surfaces service validation failures as tool errors', async () => { | ||
| mockReplaceTableRows.mockRejectedValue(new Error('Row 1: name is required')) | ||
|
|
||
| const result = await maybeWriteOutputToTable( | ||
| FunctionExecute.id, | ||
| { outputTable: 'tbl_1' }, | ||
| { success: true, output: { result: [{ age: 30 }] } }, | ||
| buildContext() | ||
| ) | ||
|
|
||
| expect(result.success).toBe(false) | ||
| expect(result.error).toContain('Row 1: name is required') | ||
| }) | ||
| }) | ||
|
|
||
| describe('maybeWriteReadCsvToTable', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks() | ||
| mockGetTableById.mockResolvedValue(buildTable()) | ||
| mockReplaceTableRows.mockResolvedValue({ deletedCount: 0, insertedCount: 2 }) | ||
| }) | ||
|
|
||
| it('rejects a table from another workspace without touching it', async () => { | ||
| mockGetTableById.mockResolvedValue(buildTable({ workspaceId: 'other-workspace' })) | ||
|
|
||
| const result = await maybeWriteReadCsvToTable( | ||
| ReadTool.id, | ||
| { outputTable: 'tbl_1', path: 'files/people.csv' }, | ||
| { success: true, output: { content: 'name,age\nAlice,30' } }, | ||
| buildContext() | ||
| ) | ||
|
|
||
| expect(result).toEqual({ success: false, error: 'Table "tbl_1" not found' }) | ||
| expect(mockReplaceTableRows).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('imports CSV content through the service with id-keyed rows', async () => { | ||
| const result = await maybeWriteReadCsvToTable( | ||
| ReadTool.id, | ||
| { outputTable: 'tbl_1', path: 'files/people.csv' }, | ||
| { success: true, output: { content: 'name,age\nAlice,30\nBob,40' } }, | ||
| buildContext() | ||
| ) | ||
|
|
||
| expect(result.success).toBe(true) | ||
| const [data] = mockReplaceTableRows.mock.calls[0] | ||
| expect(data.rows).toEqual([ | ||
| { col_name: 'Alice', col_age: '30' }, | ||
| { col_name: 'Bob', col_age: '40' }, | ||
| ]) | ||
| }) | ||
|
|
||
| it('fails fast when the file headers match no table columns', async () => { | ||
| const result = await maybeWriteReadCsvToTable( | ||
| ReadTool.id, | ||
| { outputTable: 'tbl_1', path: 'files/people.csv' }, | ||
| { success: true, output: { content: 'wrong,headers\n1,2' } }, | ||
| buildContext() | ||
| ) | ||
|
|
||
| expect(result.success).toBe(false) | ||
| expect(result.error).toContain('Row 1 has no keys matching columns') | ||
| expect(mockReplaceTableRows).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('surfaces service validation failures as tool errors', async () => { | ||
| mockReplaceTableRows.mockRejectedValue(new Error('Row 1: name is required')) | ||
|
|
||
| const result = await maybeWriteReadCsvToTable( | ||
| ReadTool.id, | ||
| { outputTable: 'tbl_1', path: 'files/people.csv' }, | ||
| { success: true, output: { content: 'age\n30' } }, | ||
| buildContext() | ||
| ) | ||
|
|
||
| expect(result.success).toBe(false) | ||
| expect(result.error).toContain('Row 1: name is required') | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.