Skip to content

Commit 1fac7df

Browse files
committed
Merge remote-tracking branch 'origin/fix/w6-tidy' into integrate/v2-w5
2 parents 4fb855d + 9f9aca8 commit 1fac7df

17 files changed

Lines changed: 168 additions & 80 deletions

File tree

apps/sim/app/api/v2/workflows/[id]/deployment/route.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,9 @@ export const revalidate = 0
2121
* fallback: it retains the timestamp of a deployment that has since been
2222
* undeployed, so reading it would report a deploy time alongside
2323
* `isDeployed: false`.
24-
*/
25-
/**
26-
* Deliberately head-safe despite issuing a write.
2724
*
28-
* Reading a workflow can trigger a migrate-on-read `workflow_blocks` update when
25+
* Deliberately head-safe despite issuing a write. Reading a workflow can trigger
26+
* a migrate-on-read `workflow_blocks` update when
2927
* `applyBlockMigrations` upgrades a stored block. That write is convergent: it is
3028
* conditional on a migration actually applying, idempotent, and would be issued by
3129
* the next ordinary read regardless, so a `HEAD` only brings it forward.

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/utils.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5+
import { columnTypeOf } from '@/lib/table/column-types'
56
import {
67
cleanCellValue,
78
dateValueToLocalParts,
@@ -160,6 +161,32 @@ describe('cleanCellValue', () => {
160161
expect(cleanCellValue('Nope', column)).toBeNull()
161162
expect(cleanCellValue('Bug, Nope', column)).toBeNull()
162163
})
164+
165+
/**
166+
* The refusal above is `coerce`'s, not the last word the registry has on the
167+
* value: `salvage` reads the same paste as the one option that resolved. That
168+
* reading is reserved for writes with no caller to answer — a CSV row, a block
169+
* output — and a typed cell has one, so this helper must not reach for it. The
170+
* pairing is asserted rather than described so a future helper that "improves"
171+
* the paste by salvaging it fails here.
172+
*/
173+
it('refuses a partial multiselect paste the registry could still salvage', () => {
174+
const column = {
175+
name: 'tags',
176+
type: 'select',
177+
multiple: true,
178+
options: [
179+
{ id: 'opt_a', name: 'Bug' },
180+
{ id: 'opt_b', name: 'Docs' },
181+
],
182+
} as const
183+
184+
expect(columnTypeOf(column).salvage?.('Bug, Nope', column)).toEqual({
185+
ok: true,
186+
value: ['opt_a'],
187+
})
188+
expect(cleanCellValue('Bug, Nope', column)).toBeNull()
189+
})
163190
})
164191

165192
describe('formatValueForInput', () => {

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/utils.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,18 @@ export function generateColumnName(columns: ReadonlyArray<{ name: string }>): st
1919
}
2020

2121
/**
22-
* Coerce a raw input value to the appropriate type for a column.
23-
* Throws on invalid JSON.
22+
* Coerce a value a person typed or pasted into a cell to that column's type.
23+
* Throws on invalid JSON, and answers `null` for everything else the column
24+
* type refuses.
25+
*
26+
* `null` is what the server would store for the same value, which is the point:
27+
* the optimistic cache and the row that comes back agree. It deliberately does
28+
* not consult `ColumnTypeDefinition.salvage`, which reads a refused value
29+
* lossily — a multiselect paste naming one option that no longer exists blanks
30+
* the cell here rather than storing the members that did resolve. Salvage is
31+
* reserved for writes with no caller to answer, and this one has one: a person
32+
* watching the cell, who is better served seeing the paste refused than seeing
33+
* part of it silently kept.
2434
*/
2535
export function cleanCellValue(
2636
value: unknown,

apps/sim/hooks/queries/mcp.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,14 @@ export function useCreateMcpServer() {
317317
authType,
318318
}
319319
},
320+
/**
321+
* Both caches are dropped, so neither waits out its stale time — but the
322+
* refetched row still reads `disconnected`, because the discovery that
323+
* moves it runs on the tools query this same invalidation kicks off, after
324+
* the list has already come back. The status catches up on the next list
325+
* refetch; the tools do not wait for it, since
326+
* {@link isServerEligibleForDiscovery} gates only OAuth rows on `connected`.
327+
*/
320328
onSettled: (_data, _error, variables) => {
321329
queryClient.invalidateQueries({ queryKey: mcpKeys.serversList(variables.workspaceId) })
322330
queryClient.invalidateQueries({

apps/sim/lib/api/contracts/v2/openapi/shared.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,14 @@ export const WORKSPACE_API_KEY_DENIED =
257257
* {@link WORKSPACE_API_KEY_DENIED} for an operation behind the resource-concealment
258258
* error policy, which rewrites the authorization failure to a not-found response so
259259
* the caller learns nothing about the resource.
260+
*
261+
* Published on no operation today: every one audited so far refuses a workspace
262+
* key through its principal-kind list, which raises an error the concealment
263+
* policy does not rewrite, so all of them say 403. Kept because a concealed
264+
* operation that denies the key through the policy itself would need this exact
265+
* wording, and because `scripts/openapi/documents.test.ts` asserts the file-share
266+
* description does not carry it — inlining the string there would let the guard
267+
* and the wording it guards drift apart.
260268
*/
261269
export const WORKSPACE_API_KEY_DENIED_AS_NOT_FOUND =
262270
'A workspace API key is rejected as `404` rather than `403`, because unauthorized resources are concealed; use a personal API key.'

apps/sim/lib/api/contracts/v2/workflows.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -816,18 +816,15 @@ export const v2ExecutionErrorSchema = z
816816
export type V2ExecutionError = z.output<typeof v2ExecutionErrorSchema>
817817

818818
/**
819-
* The mutually-exclusive execute option matrix, mirrored from the route's
820-
* post-parse checks in `app/api/v2/workflows/[id]/execute/route.ts`. Kept as one
819+
* That the execute options constrain each other, said once. Kept as one
821820
* exported string so the request-body description and the OpenAPI operation
822821
* description cannot drift from each other.
823-
*/
824-
/**
825-
* The six rejected option combinations used to be enumerated here and pasted
826-
* onto both the operation and the request-body description, restating what each
827-
* field already says. A caller reads the constraint where it applies — on the
828-
* field it constrains — so the enumeration lives on `async`, `stream`,
829-
* `executionTimeoutSeconds`, `includeThinking`, and `includeToolCalls`, and the
830-
* operation says only that the options are mutually constrained.
822+
*
823+
* It deliberately does not enumerate the combinations the route rejects. That
824+
* list used to be pasted onto both the operation and the request-body
825+
* description, restating what each field already says; a caller reads a
826+
* constraint where it applies, so it lives on `async`, `stream`,
827+
* `executionTimeoutSeconds`, `includeThinking`, and `includeToolCalls`.
831828
*/
832829
export const EXECUTE_OPTION_CONSTRAINTS =
833830
'Each option carries the modes it requires and the modes that reject it; a violated combination is a 400.'
@@ -838,8 +835,9 @@ export const EXECUTE_OPTION_CONSTRAINTS =
838835
* (triggerType, draft state, deployment pinning) are NEVER wire fields; they
839836
* are typed options on the execution service.
840837
*
841-
* The six rejected option combinations are enumerated in
842-
* {@link EXECUTE_OPTION_CONSTRAINTS} and enforced by the route after parsing.
838+
* The rejected option combinations are enforced by the route after parsing and
839+
* described on the fields they constrain; {@link EXECUTE_OPTION_CONSTRAINTS}
840+
* only tells a caller that the options constrain each other.
843841
*/
844842
export const v2ExecuteWorkflowBodySchema = z
845843
.object({

apps/sim/lib/api/list-query.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,6 @@ export type CursorKey = string | number
5757
export const INVALID_CURSOR_MESSAGE =
5858
'cursor does not match the requested sortBy/sortOrder. Restart pagination without a cursor after changing the sort.'
5959

60-
/**
61-
* Caller-facing message for a cursor that cannot be read back at all.
62-
*
63-
* Separate from {@link INVALID_CURSOR_MESSAGE} because that one names
64-
* `sortBy`/`sortOrder`, and the lists that mint a wrapped domain token accept
65-
* neither param — `GET /logs` carries its direction in `order`, and
66-
* `GET /billing/logs` takes no sort param whatsoever. Sending those callers to
67-
* inspect a knob their operation does not have is the same wrong-signpost
68-
* problem `UNKNOWN_CURSOR_MESSAGE` was written to avoid on the ledger. The
69-
* actionable half — restart without a cursor — is identical.
70-
*/
7160
/**
7261
* One column of a keyset ordering, with the codec that moves its value through
7362
* the opaque cursor.

apps/sim/lib/credentials/application/list-workspace-credentials.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ export interface ListWorkspaceCredentialsInput {
2525
export interface ListWorkspaceCredentialsResult {
2626
credentials: VisibleWorkspaceCredential[]
2727
nextCursorKeys: CursorKey[] | null
28-
/**
29-
* Echoed back because the route's presenter receives only this result, and the
30-
* cursor it hands out has to be stamped with the sort that produced it.
31-
*/
32-
sortBy: ListWorkspaceCredentialsInput['sortBy']
33-
sortOrder: ListSortOrder
3428
}
3529

3630
export const listWorkspaceCredentials = defineAuthorizedWorkspaceUseCase({
@@ -57,7 +51,7 @@ export const listWorkspaceCredentials = defineAuthorizedWorkspaceUseCase({
5751
limit: input.limit,
5852
cursorKeys: input.cursorKeys,
5953
})
60-
return { credentials: page.data, nextCursorKeys: page.nextCursorKeys, ...sort }
54+
return { credentials: page.data, nextCursorKeys: page.nextCursorKeys }
6155
}
6256

6357
const workspaceAccess = await checkWorkspaceAccess(context.workspaceId, principal.userId)
@@ -82,6 +76,6 @@ export const listWorkspaceCredentials = defineAuthorizedWorkspaceUseCase({
8276
limit: input.limit,
8377
cursorKeys: input.cursorKeys,
8478
})
85-
return { credentials: page.data, nextCursorKeys: page.nextCursorKeys, ...sort }
79+
return { credentials: page.data, nextCursorKeys: page.nextCursorKeys }
8680
},
8781
})

apps/sim/lib/knowledge/application/search.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,22 @@ describe('knowledge search application use case', () => {
352352
expect(result.results[0]).not.toHaveProperty('rerankerScore')
353353
})
354354

355+
/**
356+
* A resolved call with an empty ordering leaves the caller in the same place a
357+
* thrown one does — vector order, no `rerankerScore` — so it reports the same
358+
* status. It is not "the reranker matched nothing": `rerank` sends a non-empty
359+
* document list and asks for `top_n` of it, so an empty array means the
360+
* response carried nothing usable rather than a legitimate empty ranking.
361+
*/
362+
it('reports unavailable when the call resolves without a usable ordering', async () => {
363+
mocks.rerank.mockResolvedValueOnce({ results: [], isBYOK: false })
364+
365+
const result = await rerankedSearch(true)
366+
367+
expect(result.rerankerStatus).toBe('unavailable')
368+
expect(result.results[0]).not.toHaveProperty('rerankerScore')
369+
})
370+
355371
it('reports skipped for a tag-only search, which has no query to rank against', async () => {
356372
mocks.getTagDefinitions.mockResolvedValue([
357373
{ tagSlot: 'tag1', displayName: 'team', fieldType: 'text' },

apps/sim/lib/knowledge/application/search.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,18 @@ export const searchKnowledge = defineAuthorizedKnowledgeUseCase({
328328
* never completes, so only the success path has to move it. A request with
329329
* nothing to rank — no query text, or no candidate rows — is `skipped` rather
330330
* than `unavailable`: the reranker was never the obstacle. Anything else that
331-
* was asked for and did not run is `unavailable`, including a request that
332-
* reaches here with no model, which no HTTP contract can now produce.
331+
* was asked for and did not produce a usable ordering is `unavailable`,
332+
* including a request that reaches here with no model, which no HTTP contract
333+
* can now produce.
334+
*
335+
* A call that returns without raising but hands back an empty ordering counts
336+
* as `unavailable` too, and it is not the reranker "matching nothing":
337+
* `rerank` asks for `top_n` over a non-empty document list, so a provider that
338+
* ranked them returns one entry per document. Empty means the response carried
339+
* nothing usable — no results, or only indices outside the batch, which
340+
* `rerank` drops. The caller is left in vector order with no `rerankerScore`,
341+
* which is exactly what `unavailable` promises, and retrying is exactly the
342+
* right advice.
333343
*/
334344
let rerankerStatus: RerankerStatus = !input.rerankerEnabled
335345
? 'not_requested'

0 commit comments

Comments
 (0)