Skip to content

Commit d1a2940

Browse files
committed
fix(buffer): preserve optional inputs and sort overrides
1 parent 5ef3deb commit d1a2940

7 files changed

Lines changed: 150 additions & 20 deletions

File tree

‎apps/sim/blocks/blocks/buffer.ts‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@ const ORG_OPS = [
1414
'get_aggregated_post_metrics',
1515
]
1616

17-
function jsonWand(toolId: string, field: string, example: string, placeholder: string) {
17+
function jsonWand(
18+
toolId: string,
19+
field: string,
20+
example: string,
21+
placeholder: string
22+
): NonNullable<SubBlockConfig['wandConfig']> {
1823
return {
1924
enabled: true,
20-
generationType: 'json-object' as const,
25+
generationType: example.trim().startsWith('[') ? 'json-array' : 'json-object',
2126
prompt: `Generate the Buffer ${field} input requested by the user. Follow this input contract: ${getToolMetadata(toolId)?.params[field]?.description ?? ''}. Example: ${example}. Use only resource IDs and URLs supplied by the user. Return ONLY the JSON.`,
2227
placeholder,
2328
}
@@ -663,12 +668,18 @@ export const BufferBlock: BlockConfig = {
663668
continue
664669
if (value === undefined || value === null || value === '') continue
665670
if ((key === 'mode' || key === 'schedulingType') && value === 'default') {
666-
if (params.operation === 'create_post') {
667-
result[key] = key === 'mode' ? 'addToQueue' : 'automatic'
668-
}
671+
result[key] =
672+
params.operation === 'create_post'
673+
? key === 'mode'
674+
? 'addToQueue'
675+
: 'automatic'
676+
: undefined
677+
continue
678+
}
679+
if (key === 'approvalChange' && value === 'default') {
680+
result[key] = undefined
669681
continue
670682
}
671-
if (key === 'approvalChange' && value === 'default') continue
672683
if (key === 'limit') {
673684
const limit = Number(value)
674685
if (Number.isFinite(limit)) result.limit = limit

‎apps/sim/executor/handlers/generic/generic-handler.test.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import '@sim/testing/mocks/executor'
22

33
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
4+
import { executeBufferTool } from '@/lib/internal/buffer/execute-tool'
5+
import { BufferBlock } from '@/blocks/blocks/buffer'
46
import { HarmonicBlock } from '@/blocks/blocks/harmonic'
57
import { McpBlock } from '@/blocks/blocks/mcp'
68
import { getBlock } from '@/blocks/index'
@@ -9,6 +11,8 @@ import type { ExecutionContext } from '@/executor/types'
911
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
1012
import type { SerializedBlock } from '@/serializer/types'
1113
import { executeTool } from '@/tools'
14+
import { bufferEditPostTool } from '@/tools/buffer/edit_post'
15+
import type { BufferEditPostParams } from '@/tools/buffer/types'
1216
import { mcpRunOperationTool } from '@/tools/mcp/run-operation'
1317
import type { ToolConfig } from '@/tools/types'
1418
import { getTool } from '@/tools/utils'
@@ -76,6 +80,50 @@ describe('GenericBlockHandler', () => {
7680
mockExecuteTool.mockResolvedValue({ success: true, output: { customResult: 'OK' } })
7781
})
7882

83+
it('preserves the current Buffer schedule and approval state when editing only text', async () => {
84+
mockGetBlock.mockReturnValue(BufferBlock)
85+
mockGetTool.mockReturnValue(bufferEditPostTool)
86+
const providerInputs: Record<string, unknown>[] = []
87+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (_input, init) => {
88+
const body: { variables: { input: Record<string, unknown> } } = JSON.parse(String(init?.body))
89+
providerInputs.push(body.variables.input)
90+
return Response.json({
91+
data: { editPost: { __typename: 'PostActionSuccess', post: { id: 'post-1' } } },
92+
})
93+
})
94+
mockExecuteTool.mockImplementation(async (toolId: string, params: BufferEditPostParams) => {
95+
const response = await executeBufferTool({
96+
toolId,
97+
input: bufferEditPostTool.operation.input(params),
98+
headers: new Headers(),
99+
context: { workflowId: mockContext.workflowId, userId: 'user-1' },
100+
requestId: 'request-1',
101+
})
102+
if (!bufferEditPostTool.transformResponse) throw new Error('Missing response transformer')
103+
return bufferEditPostTool.transformResponse(response)
104+
})
105+
106+
await handler.execute(
107+
mockContext,
108+
{
109+
...mockBlock,
110+
metadata: { id: 'buffer', name: 'Buffer' },
111+
config: { tool: 'buffer_edit_post', params: {} },
112+
},
113+
{
114+
operation: 'edit_post',
115+
apiKey: 'buffer-key',
116+
postId: 'post-1',
117+
text: 'Updated caption',
118+
mode: 'default',
119+
schedulingType: 'default',
120+
approvalChange: 'default',
121+
}
122+
)
123+
124+
expect(providerInputs).toEqual([{ id: 'post-1', text: 'Updated caption' }])
125+
})
126+
79127
it('executes the standalone stable MCP action after argument resolution with trusted block scope', async () => {
80128
mockGetBlock.mockReturnValue(McpBlock)
81129
mockGetTool.mockReturnValue(mcpRunOperationTool)

‎apps/sim/lib/internal/buffer/input.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function validateDueAt(
6767
export const bufferCreatePostInputSchema = z
6868
.object({
6969
...sharedFields,
70+
assets: sharedFields.assets.transform((assets) => assets ?? undefined),
7071
channelId: z.string().min(1, 'channelId is required'),
7172
mode: z.enum(['addToQueue', 'shareNext', 'shareNow', 'customScheduled']),
7273
schedulingType: z.enum(['automatic', 'notification']).default('automatic'),

‎apps/sim/lib/internal/buffer/operations.test.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,43 @@ import { executeBufferTool } from '@/lib/internal/buffer/execute-tool'
1212
import { createBufferPost } from '@/lib/internal/buffer/operations'
1313

1414
describe('Buffer operations', () => {
15+
it('creates a text post when optional assets are null', async () => {
16+
const providerInputs: Record<string, unknown>[] = []
17+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (_input, init) => {
18+
const body: { variables: { input: Record<string, unknown> } } = JSON.parse(String(init?.body))
19+
providerInputs.push(body.variables.input)
20+
return Response.json({
21+
data: { createPost: { __typename: 'PostActionSuccess', post: { id: 'post-1' } } },
22+
})
23+
})
24+
25+
const response = await executeBufferTool({
26+
toolId: 'buffer_create_post',
27+
input: {
28+
apiKey: 'buffer-key',
29+
channelId: 'channel-1',
30+
mode: 'addToQueue',
31+
text: 'Caption',
32+
assets: null,
33+
},
34+
headers: new Headers(),
35+
context: { workflowId: 'workflow-1', userId: 'user-1' },
36+
requestId: 'request-1',
37+
})
38+
39+
expect(response.status).toBe(200)
40+
expect(providerInputs).toEqual([
41+
{
42+
channelId: 'channel-1',
43+
mode: 'addToQueue',
44+
schedulingType: 'automatic',
45+
text: 'Caption',
46+
assets: [],
47+
needsApproval: false,
48+
},
49+
])
50+
})
51+
1552
it.each([
1653
{ scheduling: {}, expected: {} },
1754
{ scheduling: { mode: null, schedulingType: null }, expected: {} },

‎apps/sim/scripts/test-buffer-e2e.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,15 @@ async function run() {
142142
if (process.env.BUFFER_CHANNEL_ID)
143143
assert(channel, 'BUFFER_CHANNEL_ID must identify an unlocked channel in this organization')
144144
if (!channel) {
145-
await check('Get Posts from organization without channels', async () => {
145+
await check('Get Posts with bounded draft pagination', async () => {
146146
const output = await tool('buffer_get_posts', {
147147
organizationId,
148148
limit: 1,
149149
filter: { status: ['draft'] },
150150
sort: [{ field: 'createdAt', direction: 'desc' }],
151151
})
152-
assert.deepEqual(output.posts, [])
152+
assert(Array.isArray(output.posts))
153+
assert(output.posts.length <= 1)
153154
assert('hasPreviousPage' in toRecord(output.pageInfo))
154155
})
155156
await check('Aggregate metrics for an empty channel selection', async () => {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { toRecord } from '@sim/utils/object'
2+
import { describe, expect, it } from 'vitest'
3+
import { bufferGetPostsTool } from '@/tools/buffer/get_posts'
4+
5+
describe('Buffer post ordering', () => {
6+
it.each([
7+
{
8+
sort: [{ field: 'createdAt', direction: 'desc' }],
9+
sortBy: 'unused-field',
10+
sortDirection: 'asc',
11+
},
12+
{
13+
sort: '[{"field":"createdAt","direction":"desc"}]',
14+
sortBy: 'dueAt',
15+
sortDirection: 'unused-direction',
16+
},
17+
])('uses explicit ordering despite unused legacy values: $sortBy / $sortDirection', (sorting) => {
18+
const body = bufferGetPostsTool.request.body
19+
if (typeof body !== 'function') throw new Error('Missing post query builder')
20+
21+
const request = toRecord(
22+
body({
23+
apiKey: 'buffer-key',
24+
organizationId: 'organization-1',
25+
...sorting,
26+
})
27+
)
28+
const input = toRecord(toRecord(request.variables).input)
29+
30+
expect(input.sort).toEqual([{ field: 'createdAt', direction: 'desc' }])
31+
})
32+
})

‎apps/sim/tools/buffer/get_posts.ts‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,19 @@ export const bufferGetPostsTool: ToolConfig<BufferGetPostsParams, BufferPostsRes
142142
}
143143
if (Object.keys(filter).length > 0) input.filter = filter
144144

145-
const sortBy = params.sortBy || 'dueAt'
146-
if (!['dueAt', 'createdAt'].includes(sortBy)) {
147-
throw new Error('sortBy must be either "dueAt" or "createdAt"')
148-
}
149-
const sortDirection = params.sortDirection || 'asc'
150-
if (!['asc', 'desc'].includes(sortDirection)) {
151-
throw new Error('sortDirection must be either "asc" or "desc"')
145+
if (params.sort) {
146+
input.sort = typeof params.sort === 'string' ? JSON.parse(params.sort) : params.sort
147+
} else {
148+
const sortBy = params.sortBy || 'dueAt'
149+
if (!['dueAt', 'createdAt'].includes(sortBy)) {
150+
throw new Error('sortBy must be either "dueAt" or "createdAt"')
151+
}
152+
const sortDirection = params.sortDirection || 'asc'
153+
if (!['asc', 'desc'].includes(sortDirection)) {
154+
throw new Error('sortDirection must be either "asc" or "desc"')
155+
}
156+
input.sort = [{ field: sortBy, direction: sortDirection }]
152157
}
153-
input.sort = params.sort
154-
? typeof params.sort === 'string'
155-
? JSON.parse(params.sort)
156-
: params.sort
157-
: [{ field: sortBy, direction: sortDirection }]
158158

159159
return {
160160
query: GET_POSTS_QUERY,

0 commit comments

Comments
 (0)