Skip to content

Commit bf2594c

Browse files
authored
fix(search): validate Notion search terms before dispatch (#8358)
1 parent 5c446f1 commit bf2594c

9 files changed

Lines changed: 82 additions & 10 deletions

File tree

‎apps/sim/lib/api/contracts/mothership-assistant-tools.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { LIVE_SEARCH_PROVIDER_IDS } from '@/lib/sim-search/live/provider-catalog
44
export const liveSearchProviderSchema = z.enum(LIVE_SEARCH_PROVIDER_IDS)
55
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>
66

7+
export const NOTION_SEARCH_TERMS_REQUIRED =
8+
'Notion requires search terms. Add keywords or a concise question.'
9+
710
/**
811
* Native queries one call may send to the same provider account. Alternatives run as separate
912
* provider searches and fuse into one ranking, so the bound keeps a call within the provider's
@@ -42,6 +45,14 @@ export const nativeSearchQuerySchema = z
4245
keywordOnly: z.boolean().optional(),
4346
})
4447
.strict()
48+
.superRefine((input, context) => {
49+
if (input.provider === 'notion' && !input.query)
50+
context.addIssue({
51+
code: 'custom',
52+
path: ['query'],
53+
message: NOTION_SEARCH_TERMS_REQUIRED,
54+
})
55+
})
4556
export type NativeSearchQuery = z.output<typeof nativeSearchQuerySchema>
4657

4758
export const nativeSearchQueriesSchema = z
@@ -161,15 +172,15 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
161172
nativeQueries: nativeSearchQueriesSchema
162173
.optional()
163174
.describe(
164-
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
175+
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Notion requires nonempty search terms even with dates or sorting. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
165176
),
166177
query: z
167178
.string()
168179
.trim()
169180
.max(2000)
170181
.default('')
171182
.describe(
172-
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.'
183+
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest where supported; Notion requires search terms.'
173184
),
174185
topK: z
175186
.number()

‎apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export const liveSearchProviderSchema = z.enum([
2020
])
2121
export type LiveSearchProvider = z.output<typeof liveSearchProviderSchema>
2222

23+
export const NOTION_SEARCH_TERMS_REQUIRED =
24+
'Notion requires search terms. Add keywords or a concise question.'
25+
2326
/**
2427
* Native queries one call may send to the same provider account. Alternatives run as separate
2528
* provider searches and fuse into one ranking, so the bound keeps a call within the provider's
@@ -58,6 +61,14 @@ export const nativeSearchQuerySchema = z
5861
keywordOnly: z.boolean().optional(),
5962
})
6063
.strict()
64+
.superRefine((input, context) => {
65+
if (input.provider === 'notion' && !input.query)
66+
context.addIssue({
67+
code: 'custom',
68+
path: ['query'],
69+
message: NOTION_SEARCH_TERMS_REQUIRED,
70+
})
71+
})
6172
export type NativeSearchQuery = z.output<typeof nativeSearchQuerySchema>
6273

6374
export const nativeSearchQueriesSchema = z
@@ -177,15 +188,15 @@ export const searchWorkspaceInputSchema = workspaceSearchFiltersSchema
177188
nativeQueries: nativeSearchQueriesSchema
178189
.optional()
179190
.describe(
180-
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
191+
`Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Notion requires nonempty search terms even with dates or sorting. Up to ${MAX_NATIVE_QUERIES_PER_ACCOUNT} per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.`
181192
),
182193
query: z
183194
.string()
184195
.trim()
185196
.max(2000)
186197
.default('')
187198
.describe(
188-
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.'
199+
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest where supported; Notion requires search terms.'
189200
),
190201
topK: z
191202
.number()

‎apps/sim/lib/mothership/generated/tool-catalog-v1.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6096,7 +6096,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
60966096
},
60976097
nativeQueries: {
60986098
description:
6099-
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Up to 4 per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
6099+
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Notion requires nonempty search terms even with dates or sorting. Up to 4 per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
61006100
minItems: 1,
61016101
maxItems: 9,
61026102
type: 'array',
@@ -6140,7 +6140,7 @@ export const SearchWorkspace: ToolCatalogEntry = {
61406140
query: {
61416141
default: '',
61426142
description:
6143-
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.',
6143+
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest where supported; Notion requires search terms.',
61446144
type: 'string',
61456145
maxLength: 2000,
61466146
},

‎apps/sim/lib/mothership/generated/tool-schemas-v1.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6044,7 +6044,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
60446044
},
60456045
nativeQueries: {
60466046
description:
6047-
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Up to 4 per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
6047+
"Live search only: queries in a provider's own language (Drive q, Gmail operators, JQL, CQL, GitHub qualifiers, Slack RTS, plain Linear/Fireflies terms, Granola natural-language questions, Notion keywords or AI questions when available). Notion requires nonempty search terms even with dates or sorting. Up to 4 per account run separately and merge; GitHub and GitLab take one per kind. Write them from the returned live guidance and account IDs; each account status names the queryIndex its cursor belongs to. Omit for simple cross-provider terms.",
60486048
minItems: 1,
60496049
maxItems: 9,
60506050
type: 'array',
@@ -6114,7 +6114,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
61146114
query: {
61156115
default: '',
61166116
description:
6117-
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest.',
6117+
'Search terms, without dates already supplied as filters. May be empty for a live listing with a date bound or sortBy newest or oldest where supported; Notion requires search terms.',
61186118
type: 'string',
61196119
maxLength: 2000,
61206120
},

‎apps/sim/lib/mothership/tools/server/knowledge/workspace-search.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,25 @@ describe('Assistant retrieval tools', () => {
9090
next: null,
9191
})
9292
})
93+
it.each([{ startDate: '2026-09-01T00:00:00Z' }, { sortBy: 'newest' }, { sortBy: 'oldest' }])(
94+
'returns actionable validation for empty Notion native queries with %j',
95+
async (bound) => {
96+
setEnvFlags({ isLiveEnterpriseSearchEnabled: true })
97+
const result = await searchWorkspaceServerTool.execute(
98+
{
99+
...bound,
100+
query: 'fallback terms',
101+
nativeQueries: [{ provider: 'notion', query: ' \t ' }],
102+
},
103+
{ ...context, assistantSearch: undefined }
104+
)
105+
expect(result).toMatchObject({
106+
success: false,
107+
message: 'Notion requires search terms. Add keywords or a concise question.',
108+
})
109+
expect(result).not.toHaveProperty('data')
110+
}
111+
)
93112
it('returns a safe permanent configuration failure instead of empty results or opaque error', async () => {
94113
mocks.search.mockRejectedValue(new EmbeddingConfigurationError())
95114
const result = await searchWorkspaceServerTool.execute({ query: 'policy' }, context)

‎apps/sim/lib/mothership/tools/server/knowledge/workspace-search.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
readDocumentInputSchema,
55
searchWorkspaceInputSchema,
66
} from '@/lib/api/contracts/mothership-assistant-tools'
7+
import { getValidationErrorMessage } from '@/lib/api/server/validation'
78
import { getBaseUrl } from '@/lib/core/utils/urls'
89
import { EmbeddingConfigurationError } from '@/lib/embeddings/configuration-error'
910
import { sourceAuthor } from '@/lib/knowledge/search/author'
@@ -211,7 +212,7 @@ export const searchWorkspaceServerTool: BaseServerTool = {
211212
error instanceof SearchDeadlineError
212213
? error.message
213214
: error instanceof z.ZodError
214-
? 'Invalid search arguments'
215+
? getValidationErrorMessage(error, 'Invalid search arguments')
215216
: messageForCopilotKnowledgeError(error),
216217
}
217218
}

‎apps/sim/lib/sim-search/live/application.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,33 @@ describe('authorized live retrieval', () => {
124124
})
125125
mocks.adminVerify.mockResolvedValue(true)
126126
})
127+
it.each([
128+
{ startDate: '2026-08-01T00:00:00Z' },
129+
{ source: ' notion ', startDate: '2026-08-01T00:00:00Z' },
130+
{ sortBy: 'newest' as const },
131+
{ sortBy: 'oldest' as const },
132+
])('rejects a Notion-only live listing with %j', async (bound) => {
133+
await expect(
134+
searchLiveKnowledge.execute({
135+
principal,
136+
input: { ...input, query: ' \t ', filters: { source: 'notion', ...bound } },
137+
})
138+
).rejects.toMatchObject({
139+
code: 'validation',
140+
message: 'Notion requires search terms. Add keywords or a concise question.',
141+
})
142+
})
143+
it.each([undefined, 'google_drive'])(
144+
'preserves date-only live results with source %s',
145+
async (source) => {
146+
const result = await searchLiveKnowledge.execute({
147+
principal,
148+
input: { ...input, query: '', filters: { source, startDate: '2026-08-01T00:00:00Z' } },
149+
})
150+
expect(result.results.map((row) => decodeLiveReference(row.documentId).id)).toEqual(['doc'])
151+
expect(result.live?.accounts[0]?.status).toBe('ok')
152+
}
153+
)
127154
it('filters admin-token GitLab results through the reader ACL before projection', async () => {
128155
const gitlab = {
129156
...account,

‎apps/sim/lib/sim-search/live/application.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type LiveSearchAccountStatus,
1111
liveSearchProviderSchema,
1212
type NativeSearchQuery,
13+
NOTION_SEARCH_TERMS_REQUIRED,
1314
nativeSearchQueriesSchema,
1415
workspaceSearchFiltersSchema,
1516
} from '@/lib/api/contracts/mothership-assistant-tools'
@@ -312,6 +313,8 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
312313
if (input.filters)
313314
input = { ...input, filters: workspaceSearchFiltersSchema.parse(input.filters) }
314315
const filters = input.filters
316+
if (!queries && filters?.source === 'notion' && !input.query.trim())
317+
throw new OrchestrationError('validation', NOTION_SEARCH_TERMS_REQUIRED)
315318
if (
316319
filters?.startDate &&
317320
filters.endDate &&

‎apps/sim/lib/sim-search/live/providers.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export const LIVE_SEARCH_PROVIDERS = {
216216
transport: 'managed_mcp',
217217
guide: {
218218
syntax:
219-
'Natural-language or plain keyword content search through Notion MCP. Availability depends on the connected account and plan; results are restricted to Notion pages, excluding connected apps.',
219+
'Natural-language or plain keyword content search through Notion MCP. Search terms are required even with dates or sorting. Availability depends on the connected account and plan; results are restricted to Notion pages, excluding connected apps.',
220220
scope:
221221
'project optionally takes a known Notion page URL when the advertised tool supports page scoping. Dates use explicit last-edited timestamps; results without those timestamps cannot satisfy date filters. Read a result for page content.',
222222
example: 'deployment rollback checklist',

0 commit comments

Comments
 (0)