Skip to content

Commit 1118499

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(oci-compute): align retries and selectors with Oracle schemas
1 parent 637ffef commit 1118499

69 files changed

Lines changed: 1975 additions & 1725 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/content/docs/integrations/oci_compute.mdx‎

Lines changed: 121 additions & 106 deletions
Large diffs are not rendered by default.

‎apps/sim/blocks/blocks/oci_compute.ts‎

Lines changed: 241 additions & 206 deletions
Large diffs are not rendered by default.

‎apps/sim/blocks/registry-maps.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,6 @@ import { MSSQLBlock, MSSQLBlockMeta } from '@/blocks/blocks/mssql'
239239
import { MySQLBlock, MySQLBlockMeta } from '@/blocks/blocks/mysql'
240240
import { Neo4jBlock, Neo4jBlockMeta } from '@/blocks/blocks/neo4j'
241241
import { NetSuiteBlock, NetSuiteBlockMeta } from '@/blocks/blocks/netsuite'
242-
import { OciComputeBlock, OciComputeBlockMeta } from '@/blocks/blocks/oci_compute'
243242
import { NeverBounceBlock, NeverBounceBlockMeta } from '@/blocks/blocks/neverbounce'
244243
import { NewRelicBlock, NewRelicBlockMeta } from '@/blocks/blocks/new_relic'
245244
import { NoteBlock } from '@/blocks/blocks/note'
@@ -250,6 +249,7 @@ import {
250249
NotionV2BlockMeta,
251250
} from '@/blocks/blocks/notion'
252251
import { ObsidianBlock, ObsidianBlockMeta } from '@/blocks/blocks/obsidian'
252+
import { OciComputeBlock, OciComputeBlockMeta } from '@/blocks/blocks/oci_compute'
253253
import { OktaBlock, OktaBlockMeta } from '@/blocks/blocks/okta'
254254
import { OneDriveBlock, OneDriveBlockMeta } from '@/blocks/blocks/onedrive'
255255
import { OnePasswordBlock, OnePasswordBlockMeta } from '@/blocks/blocks/onepassword'

‎apps/sim/lib/internal/oci-compute/execute-tool.test.ts‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
55
import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types'
66

77
const mocks = vi.hoisted(() => ({
8-
authorize: vi.fn(), createClient: vi.fn(), execute: vi.fn(),
8+
authorize: vi.fn(),
9+
createClient: vi.fn(),
10+
execute: vi.fn(),
911
}))
1012
vi.mock('@/lib/auth/credential-access', () => ({ authorizeCredentialUseForAuth: mocks.authorize }))
1113
vi.mock('@/lib/auth/hybrid', () => ({ AuthType: { INTERNAL_JWT: 'internal_jwt' } }))
1214
vi.mock('@/lib/api/server', () => ({ getValidationErrorMessage: () => 'Invalid input' }))
1315
vi.mock('@/lib/internal/oci/client.server', () => ({ createOciClient: mocks.createClient }))
14-
vi.mock('@/lib/internal/oci-compute/operations', () => ({ executeOciComputeOperation: mocks.execute }))
16+
vi.mock('@/lib/internal/oci-compute/operations', () => ({
17+
executeOciComputeOperation: mocks.execute,
18+
}))
1519

1620
import { executeOciComputeTool } from '@/lib/internal/oci-compute/execute-tool'
1721

@@ -28,7 +32,10 @@ function call(overrides: Partial<InternalToolOperationCall> = {}): InternalToolO
2832
beforeEach(() => {
2933
vi.clearAllMocks()
3034
mocks.authorize.mockResolvedValue({
31-
ok: true, resolvedCredentialId: 'authoritative', credentialType: 'service_account', workspaceId: 'workspace',
35+
ok: true,
36+
resolvedCredentialId: 'authoritative',
37+
credentialType: 'service_account',
38+
workspaceId: 'workspace',
3239
})
3340
mocks.createClient.mockResolvedValue({ bound: true })
3441
mocks.execute.mockResolvedValue({ success: true, output: { status: 200, requestId: 'request' } })
@@ -39,10 +46,16 @@ describe('OCI Compute trusted execution wiring', () => {
3946
const signal = new AbortController().signal
4047
expect((await executeOciComputeTool(call({ signal }))).status).toBe(200)
4148
expect(mocks.createClient).toHaveBeenCalledWith({
42-
credentialId: 'authoritative', workspaceId: 'workspace', serviceId: 'oci_compute', region: 'us-ashburn-1',
49+
credentialId: 'authoritative',
50+
workspaceId: 'workspace',
51+
serviceId: 'oci_compute',
52+
region: 'us-ashburn-1',
4353
})
4454
expect(mocks.execute).toHaveBeenCalledWith(
45-
{ bound: true }, 'get_instance', expect.objectContaining({ instanceId: 'instance' }), signal
55+
{ bound: true },
56+
'get_instance',
57+
expect.objectContaining({ instanceId: 'instance' }),
58+
signal
4659
)
4760
})
4861

‎apps/sim/lib/internal/oci-compute/execute-tool.ts‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import { authorizeCredentialUseForAuth } from '@/lib/auth/credential-access'
55
import { AuthType } from '@/lib/auth/hybrid'
66
import { createOciClient } from '@/lib/internal/oci/client.server'
77
import { executeOciComputeOperation } from '@/lib/internal/oci-compute/operations'
8-
import {
9-
type OciComputeOperation,
10-
ociComputeSchemas,
11-
} from '@/lib/internal/oci-compute/schema'
8+
import { type OciComputeOperation, ociComputeSchemas } from '@/lib/internal/oci-compute/schema'
129
import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types'
1310
import { OCI_COMPUTE_SERVICE_ID } from '@/tools/oci_compute/types'
1411

@@ -19,15 +16,24 @@ export const executeOciComputeTool: InternalToolOperationHandler = async (reques
1916
}
2017
const operation = request.toolId.slice('oci_compute_'.length)
2118
if (!Object.hasOwn(ociComputeSchemas, operation)) {
22-
return Response.json({ success: false, error: 'Unsupported OCI Compute operation' }, { status: 400 })
19+
return Response.json(
20+
{ success: false, error: 'Unsupported OCI Compute operation' },
21+
{ status: 400 }
22+
)
2323
}
2424
const { userId, workspaceId, workflowId } = request.context
2525
if (!userId || !workspaceId) {
26-
return Response.json({ success: false, error: 'Trusted execution scope is required' }, { status: 401 })
26+
return Response.json(
27+
{ success: false, error: 'Trusted execution scope is required' },
28+
{ status: 401 }
29+
)
2730
}
2831
try {
2932
if (Buffer.byteLength(JSON.stringify(request.input) ?? '') > DEFAULT_MAX_JSON_BODY_BYTES) {
30-
return Response.json({ success: false, error: 'OCI Compute input is too large' }, { status: 413 })
33+
return Response.json(
34+
{ success: false, error: 'OCI Compute input is too large' },
35+
{ status: 413 }
36+
)
3137
}
3238
} catch {
3339
return Response.json({ success: false, error: 'Invalid OCI Compute input' }, { status: 400 })
@@ -36,7 +42,10 @@ export const executeOciComputeTool: InternalToolOperationHandler = async (reques
3642
const parsed = ociComputeSchemas[key].safeParse(request.input)
3743
if (!parsed.success) {
3844
return Response.json(
39-
{ success: false, error: getValidationErrorMessage(parsed.error, 'Invalid OCI Compute input') },
45+
{
46+
success: false,
47+
error: getValidationErrorMessage(parsed.error, 'Invalid OCI Compute input'),
48+
},
4049
{ status: 400 }
4150
)
4251
}
@@ -68,9 +77,7 @@ export const executeOciComputeTool: InternalToolOperationHandler = async (reques
6877
serviceId: OCI_COMPUTE_SERVICE_ID,
6978
region: parsed.data.region,
7079
})
71-
return Response.json(
72-
await executeOciComputeOperation(client, key, parsed.data, request.signal)
73-
)
80+
return Response.json(await executeOciComputeOperation(client, key, parsed.data, request.signal))
7481
} catch (error) {
7582
request.signal?.throwIfAborted()
7683
return Response.json(

‎apps/sim/lib/internal/oci-compute/operations.test.ts‎

Lines changed: 122 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import {
88
executeOciComputeOperation,
99
resolveOciComputeRetryToken,
1010
} from '@/lib/internal/oci-compute/operations'
11-
import { ociComputeSchemas, type OciComputeOperation } from '@/lib/internal/oci-compute/schema'
11+
import { type OciComputeOperation, ociComputeSchemas } from '@/lib/internal/oci-compute/schema'
1212

1313
const auth = { oauthCredential: 'credential', region: 'us-ashburn-1' }
1414
function harness(data: unknown = {}, status = 200, headers: Record<string, string> = {}) {
1515
const request = vi.fn().mockResolvedValue({
16-
status, headers: { 'opc-request-id': 'request', ...headers },
16+
status,
17+
headers: { 'opc-request-id': 'request', ...headers },
1718
body: new TextEncoder().encode(status === 204 ? '' : JSON.stringify(data)),
1819
})
1920
const client = {
@@ -24,7 +25,9 @@ function harness(data: unknown = {}, status = 200, headers: Record<string, strin
2425
}
2526
async function run(operation: OciComputeOperation, input: Record<string, unknown>, h = harness()) {
2627
const result = await executeOciComputeOperation(
27-
h.client, operation, ociComputeSchemas[operation].parse({ ...auth, ...input })
28+
h.client,
29+
operation,
30+
ociComputeSchemas[operation].parse({ ...auth, ...input })
2831
)
2932
return { ...h, result }
3033
}
@@ -33,21 +36,69 @@ function body(request: OciRequest) {
3336
}
3437

3538
describe('OCI Compute requests', () => {
39+
it('preserves token and payload across re-entry of a lifecycle invocation', async () => {
40+
const input = {
41+
instanceId: 'instance', action: 'STOP',
42+
deliveryIdentity: { executionId: 'execution', blockId: 'block', invocationId: 'call' },
43+
}
44+
const first = await run('instance_action', input)
45+
const second = await run('instance_action', input)
46+
expect(first.request.mock.calls[0][0].retry).toEqual(second.request.mock.calls[0][0].retry)
47+
expect(body(first.request.mock.calls[0][0])).toEqual(body(second.request.mock.calls[0][0]))
48+
})
49+
50+
it('builds configuration creation and deferred launch overrides distinctly', async () => {
51+
const created = await run('create_instance_configuration', {
52+
compartmentId: 'destination', configurationSource: 'INSTANCE', instanceId: 'source',
53+
})
54+
expect(body(created.request.mock.calls[0][0])).toEqual({
55+
compartmentId: 'destination', source: 'INSTANCE', instanceId: 'source',
56+
})
57+
const launched = await run('launch_instance_configuration', {
58+
instanceConfigurationId: 'configuration',
59+
instanceDetails: { instanceType: 'compute', blockVolumes: [{ volumeId: 'existing' }] },
60+
})
61+
expect(body(launched.request.mock.calls[0][0])).toEqual({
62+
instanceType: 'compute', blockVolumes: [{ volumeId: 'existing' }],
63+
})
64+
})
65+
66+
it('returns optional work headers and sends explicit member-detachment choices', async () => {
67+
const { request, result } = await run('detach_instance_pool_instance', {
68+
instancePoolId: 'pool', instanceId: 'instance', isAutoTerminate: false, isDecrementSize: false,
69+
retryToken: 'member-delivery',
70+
}, harness(undefined, 202, { 'opc-work-request-id': 'work' }))
71+
expect(body(request.mock.calls[0][0])).toEqual({
72+
instanceId: 'instance', isAutoTerminate: false, isDecrementSize: false,
73+
})
74+
expect(result.output).toMatchObject({ workRequestId: 'work', retryToken: 'member-delivery' })
75+
})
76+
3677
it.each([
3778
['image', { imageId: 'image' }, { sourceType: 'image', imageId: 'image' }],
38-
['imageFilter', { imageFilter: { compartmentId: 'images' } }, {
39-
sourceType: 'image', instanceSourceImageFilterDetails: { compartmentId: 'images' },
40-
}],
79+
[
80+
'imageFilter',
81+
{ imageFilter: { compartmentId: 'images' } },
82+
{
83+
sourceType: 'image',
84+
instanceSourceImageFilterDetails: { compartmentId: 'images' },
85+
},
86+
],
4187
['bootVolume', { bootVolumeId: 'boot' }, { sourceType: 'bootVolume', bootVolumeId: 'boot' }],
4288
])('builds the %s launch source and retains its token', async (sourceMode, source, expected) => {
4389
const { request, result } = await run('launch_instance', {
44-
compartmentId: 'compartment', availabilityDomain: 'AD', shape: 'shape',
90+
compartmentId: 'compartment',
91+
availabilityDomain: 'AD',
92+
shape: 'shape',
4593
createVnicDetails: { subnetId: 'subnet', assignPublicIp: false },
46-
sourceMode, ...source, retryToken: 'explicit-token',
94+
sourceMode,
95+
...source,
96+
retryToken: 'explicit-token',
4797
})
4898
expect(body(request.mock.calls[0][0])).toMatchObject({ sourceDetails: expected })
4999
expect(request.mock.calls[0][0]).toMatchObject({
50-
method: 'POST', encodedPath: '/20160918/instances/',
100+
method: 'POST',
101+
encodedPath: '/20160918/instances/',
51102
retry: { kind: 'tokenized', maxAttempts: 2, retryToken: 'explicit-token' },
52103
maxResponseBytes: 2_000_000,
53104
})
@@ -57,45 +108,84 @@ describe('OCI Compute requests', () => {
57108

58109
it.each([
59110
['START', {}, undefined],
60-
['RESET', { allowDenseRebootMigration: false }, { actionType: 'reset', allowDenseRebootMigration: false }],
61-
['SOFTRESET', { allowDenseRebootMigration: true }, { actionType: 'softreset', allowDenseRebootMigration: true }],
62-
['REBOOTMIGRATE', { deleteLocalStorage: false }, { actionType: 'rebootMigrate', deleteLocalStorage: false }],
111+
[
112+
'RESET',
113+
{ allowDenseRebootMigration: false },
114+
{ actionType: 'reset', allowDenseRebootMigration: false },
115+
],
116+
[
117+
'SOFTRESET',
118+
{ allowDenseRebootMigration: true },
119+
{ actionType: 'softreset', allowDenseRebootMigration: true },
120+
],
121+
[
122+
'REBOOTMIGRATE',
123+
{ deleteLocalStorage: false },
124+
{ actionType: 'rebootMigrate', deleteLocalStorage: false },
125+
],
63126
])('discriminates the %s lifecycle body', async (action, fields, expected) => {
64-
const { request } = await run('instance_action', { instanceId: 'instance', action, ...fields, ifMatch: 'etag' })
127+
const { request } = await run('instance_action', {
128+
instanceId: 'instance',
129+
action,
130+
...fields,
131+
ifMatch: 'etag',
132+
})
65133
expect(body(request.mock.calls[0][0])).toEqual(expected)
66134
expect(request.mock.calls[0][0]).toMatchObject({
67-
queryPairs: [['action', action]], headers: { 'if-match': 'etag' },
135+
queryPairs: [['action', action]],
136+
headers: { 'if-match': 'etag' },
68137
})
69-
expect(request.mock.calls[0][0].retry).toBeUndefined()
138+
expect(request.mock.calls[0][0].retry).toMatchObject({ kind: 'tokenized', maxAttempts: 2 })
70139
})
71140

72141
it('uses a pool action path and a single attempt', async () => {
73-
const { request } = await run('instance_pool_action', { instancePoolId: 'pool', action: 'SOFTSTOP' })
142+
const { request } = await run('instance_pool_action', {
143+
instancePoolId: 'pool',
144+
action: 'SOFTSTOP',
145+
})
74146
expect(request.mock.calls[0][0]).toMatchObject({
75-
method: 'POST', encodedPath: '/20160918/instancePools/pool/actions/softstop',
147+
method: 'POST',
148+
encodedPath: '/20160918/instancePools/pool/actions/softstop',
76149
})
77150
expect(body(request.mock.calls[0][0])).toBeUndefined()
78-
expect(request.mock.calls[0][0].retry).toBeUndefined()
151+
expect(request.mock.calls[0][0].retry).toMatchObject({ kind: 'tokenized', maxAttempts: 2 })
79152
})
80153

81154
it('terminates once with explicit preservation choices and accepts an empty response', async () => {
82-
const { request, result } = await run('terminate_instance', {
83-
instanceId: 'instance', preserveBootVolume: false, preserveDataVolumesCreatedAtLaunch: true,
84-
ifMatch: 'etag',
85-
}, harness(undefined, 204))
155+
const { request, result } = await run(
156+
'terminate_instance',
157+
{
158+
instanceId: 'instance',
159+
preserveBootVolume: false,
160+
preserveDataVolumesCreatedAtLaunch: true,
161+
ifMatch: 'etag',
162+
},
163+
harness(undefined, 204)
164+
)
86165
expect(result.success).toBe(true)
87166
expect(request.mock.calls[0][0]).toMatchObject({
88-
method: 'DELETE', headers: { 'if-match': 'etag' },
89-
queryPairs: [['preserveBootVolume', 'false'], ['preserveDataVolumesCreatedAtLaunch', 'true']],
167+
method: 'DELETE',
168+
headers: { 'if-match': 'etag' },
169+
queryPairs: [
170+
['preserveBootVolume', 'false'],
171+
['preserveDataVolumesCreatedAtLaunch', 'true'],
172+
],
90173
})
91174
expect(request.mock.calls[0][0].retry).toBeUndefined()
92175
expect(request).toHaveBeenCalledTimes(1)
93176
})
94177

95178
it('retains pagination on an empty page and encodes opaque IDs once', async () => {
96-
const { request, result } = await run('list_instance_pool_instances', {
97-
instancePoolId: 'pool/a', compartmentId: 'compartment', page: 'opaque+/=', limit: 50,
98-
}, harness([], 200, { 'opc-next-page': 'next' }))
179+
const { request, result } = await run(
180+
'list_instance_pool_instances',
181+
{
182+
instancePoolId: 'pool/a',
183+
compartmentId: 'compartment',
184+
page: 'opaque+/=',
185+
limit: 50,
186+
},
187+
harness([], 200, { 'opc-next-page': 'next' })
188+
)
99189
expect(result.output).toMatchObject({ poolInstances: [], nextPage: 'next' })
100190
expect(request.mock.calls[0][0]).toMatchObject({
101191
encodedPath: '/20160918/instancePools/pool%2Fa/instances',
@@ -108,7 +198,11 @@ describe('OCI Compute requests', () => {
108198
const h = harness()
109199
h.request.mockRejectedValue(new OciClientError('request_failed'))
110200
const { result } = await run('terminate_instance', { instanceId: 'instance' }, h)
111-
expect(result).toMatchObject({ success: false, retryable: false, output: { outcome: 'unknown' } })
201+
expect(result).toMatchObject({
202+
success: false,
203+
retryable: false,
204+
output: { outcome: 'unknown' },
205+
})
112206
expect(h.request).toHaveBeenCalledTimes(1)
113207
})
114208

0 commit comments

Comments
 (0)