Skip to content

Commit 86021c7

Browse files
committed
fix(plane): honor connector caps and preserve inbox timestamp order
1 parent c5605a4 commit 86021c7

5 files changed

Lines changed: 56 additions & 26 deletions

File tree

‎apps/sim/connectors/plane/plane.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export const planeConnector: ConnectorConfig = {
111111
}
112112
const response = await requestPlane(token, config, resourcePath(config), {
113113
cursor,
114-
per_page: PAGE_SIZE,
114+
per_page: Math.min(PAGE_SIZE, remaining),
115115
fields: config.contentType === 'pages' ? PAGE_FIELDS : WORK_ITEM_FIELDS,
116116
...(config.contentType === 'work_items'
117117
? { expand: 'state,assignees,labels', order_by: 'created_at' }

‎apps/sim/lib/desktop/application/executor.integration.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
import { createDeferred } from '@sim/testing/helpers/deferred'
3232
import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock'
3333
import { generateId } from '@sim/utils/id'
34+
import { compareStrings } from '@sim/utils/string'
3435
import { eq, inArray, sql } from 'drizzle-orm'
3536
import { closeRedisConnection, getRedisClient } from '@/lib/core/config/redis'
3637
import type { DbTransaction } from '@/lib/db/types'
@@ -865,7 +866,7 @@ describe.runIf(Boolean(redisUrl))('desktop background executor protocol', () =>
865866
])
866867
})
867868

868-
it('lists pending calls in persistence order even when their doorbell was never heard', async () => {
869+
it('preserves sub-millisecond persistence order even when the doorbell was never heard', async () => {
869870
const desktop = await signedInDesktop()
870871
const run = await boundRun(desktop)
871872
const first = await pendingCall(run.runId, 'browser_navigate', { url: 'https://sim.ai' })
@@ -874,13 +875,23 @@ describe.runIf(Boolean(redisUrl))('desktop background executor protocol', () =>
874875
path: 'user-local/Project--mount-1',
875876
pattern: 'TODO',
876877
})
878+
const persisted = [first, second, vfs].sort(compareStrings).reverse()
879+
const createdAt = new Date(Date.now() - 1000).toISOString()
880+
for (const [index, toolCallId] of persisted.entries()) {
881+
await db
882+
.update(copilotAsyncToolCalls)
883+
.set({
884+
createdAt: sql`${createdAt}::timestamp + ${index} * interval '100 microseconds'`,
885+
})
886+
.where(eq(copilotAsyncToolCalls.toolCallId, toolCallId))
887+
}
877888
await pendingCall(run.runId, 'run_workflow', {})
878889
/** Rung while no stream was open: nobody heard it. */
879890
ringDesktopInbox(desktop.deviceId, 'call')
880891

881892
expect(
882893
(await inbox(desktop)).items.map((item) => item.kind === 'call' && item.toolCallId)
883-
).toEqual([first, second, vfs])
894+
).toEqual(persisted)
884895
})
885896

886897
it('counts the device online after a pull or a stream open, and keeps it online when the stream closes', async () => {

‎apps/sim/lib/desktop/executor/repository.ts‎

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -189,28 +189,25 @@ export async function listDesktopInboxRows(identity: Omit<DesktopDeviceIdentity,
189189
)
190190
.orderBy(asc(copilotAsyncToolCalls.createdAt), asc(copilotAsyncToolCalls.toolCallId))
191191
.limit(INBOX_ROW_LIMIT)
192-
const [waiting, cancelled] = await Promise.all([
193-
rowsWhere(
194-
and(
195-
eq(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.pending),
196-
isNull(copilotAsyncToolCalls.executionOwnerToken),
197-
or(sql`${copilotAsyncToolCalls.pickupDeadlineAt} > clock_timestamp()`, awaitingPermission),
198-
inArray(copilotRuns.status, LIVE_RUN_STATUSES),
199-
isNull(copilotRuns.toolAdmissionClosedAt)
200-
)
201-
),
202-
rowsWhere(
203-
and(
204-
isNotNull(copilotAsyncToolCalls.executionOwnerToken),
205-
isNull(copilotAsyncToolCalls.executionSettledAt),
206-
ne(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.running)
207-
)
208-
),
209-
])
210-
return [...waiting, ...cancelled].sort(
211-
(a, b) =>
212-
a.createdAt.getTime() - b.createdAt.getTime() || a.toolCallId.localeCompare(b.toolCallId)
192+
return rowsWhere(
193+
and(
194+
eq(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.pending),
195+
isNull(copilotAsyncToolCalls.executionOwnerToken),
196+
or(sql`${copilotAsyncToolCalls.pickupDeadlineAt} > clock_timestamp()`, awaitingPermission),
197+
inArray(copilotRuns.status, LIVE_RUN_STATUSES),
198+
isNull(copilotRuns.toolAdmissionClosedAt)
199+
)
213200
)
201+
.unionAll(
202+
rowsWhere(
203+
and(
204+
isNotNull(copilotAsyncToolCalls.executionOwnerToken),
205+
isNull(copilotAsyncToolCalls.executionSettledAt),
206+
ne(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.running)
207+
)
208+
)
209+
)
210+
.orderBy(asc(copilotAsyncToolCalls.createdAt), asc(copilotAsyncToolCalls.toolCallId))
214211
}
215212

216213
export type DesktopInboxRow = Awaited<ReturnType<typeof listDesktopInboxRows>>[number]

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,14 @@ try {
600600
(await planeConnector.validateConfig(apiKey, { ...config, maxDocuments: true })).valid,
601601
false
602602
)
603-
const capped = await planeConnector.listDocuments(apiKey, { ...config, maxDocuments: 1 })
603+
const cappedContext: Record<string, unknown> = { totalDocsFetched: 0 }
604+
const capped = await planeConnector.listDocuments(
605+
apiKey,
606+
{ ...config, maxDocuments: 1 },
607+
undefined,
608+
cappedContext
609+
)
610+
assert.equal(cappedContext.listingCapped, true)
604611
assert.equal(capped.documents.length, 1)
605612
assert.equal(capped.hasMore, false)
606613
assert.equal(capped.reconciliationSafe, false)

‎apps/sim/tools/plane/coverage.json‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@
779779
"checked": 102,
780780
"failures": 0
781781
},
782-
"liveVerification": "Disposable-project HTTP checks exercise v2 and v1 compatibility on Plane Cloud. A real signed webhook delivery exercises registration, signature verification, filtering and cleanup. Community Edition is source-audited; no separate self-hosted server was live-tested.",
782+
"liveVerification": "The recorded Plane Cloud validation covers v2 and v1 compatibility and a real signed webhook delivery. These Cloud checks precede the connector cap fix and have not been rerun for this follow-up. Official Community Edition v1.4.2 was also tested over real HTTP with production Sim actions and the knowledge connector, reverse-proxy paths, signed attachment storage, and actual Celery webhook deliveries through the production signature and event handler.",
783783
"liveResponses": {
784784
"checked": 145,
785785
"failures": 0
@@ -791,10 +791,25 @@
791791
"successfulV2Operations": 67,
792792
"successfulV1CompatibilityVariants": 56,
793793
"verifiedSignedDeliveries": 1
794+
},
795+
"communityChecks": {
796+
"release": "v1.4.2",
797+
"reference": "https://github.com/makeplane/plane/releases/tag/v1.4.2",
798+
"passed": 21,
799+
"failed": 0,
800+
"skipped": 1,
801+
"requests": 76,
802+
"successfulV1Operations": 51,
803+
"additionalReverseProxyChecksPassed": 8,
804+
"verifiedSignedDeliveries": 63,
805+
"webhookProcessingErrors": 0,
806+
"cleanupVerified": true,
807+
"boundary": "Production action transport and knowledge connector over real provider HTTP; actual provider webhook deliveries through the production handler. Full Sim route/workflow execution is covered separately by synthetic HTTP and database integration checks."
794808
}
795809
},
796810
"documentedLimitations": [
797811
"API availability depends on the Plane edition and version. Community Edition compatibility uses v1.",
812+
"Community Edition v1.4.2 does not expose the public project pages API or the v2 webhook management API. Work-item knowledge sync and manual webhook subscriptions were verified against the real Community server.",
798813
"Signed object-storage transfers are performed with the returned upload URL and form fields; asset initialization and confirmation are exposed as Plane actions.",
799814
"Expanded v2 objects without a published nested schema are preserved as JSON rather than assigning guessed fields.",
800815
"The knowledge connector uses v1 to access description content, which v2 work-item reads omit. Offset listings disable automatic deletion reconciliation.",

0 commit comments

Comments
 (0)