Skip to content

Commit 0a1f302

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix: tighten Oracle Project Management contracts and editor behavior
1 parent e7c29a4 commit 0a1f302

69 files changed

Lines changed: 7021 additions & 3557 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/oracle_fusion_project_management.mdx‎

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

‎apps/sim/blocks/blocks/oracle_fusion_project_management.test.ts‎

Lines changed: 129 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/** @vitest-environment node */
22
import { describe, expect, it } from 'vitest'
3-
import { OracleFusionProjectManagementBlock as block, OracleFusionProjectManagementBlockMeta as meta } from '@/blocks/blocks/oracle_fusion_project_management'
43
import { NetSuiteIcon } from '@/components/icons'
54
import { selectorManifest } from '@/lib/selectors/manifest'
5+
import {
6+
OracleFusionProjectManagementBlock as block,
7+
OracleFusionProjectManagementBlockMeta as meta,
8+
} from '@/blocks/blocks/oracle_fusion_project_management'
69

710
const prefix = 'oracle_fusion_project_management_'
811
const OPERATIONS = [
@@ -62,30 +65,45 @@ const OPERATIONS = [
6265
] as const
6366

6467
function map(operation: string, params: Record<string, unknown> = {}) {
65-
return block.tools.config!.params!({ operation: prefix + operation, oauthCredential: 'credential-1', ...params })
68+
return block.tools.config!.params!({
69+
operation: prefix + operation,
70+
oauthCredential: 'credential-1',
71+
...params,
72+
})
6673
}
6774

6875
describe('Oracle Project Management block', () => {
6976
it('has the complete 53-operation surface, a read-first default, and the established Oracle icon', () => {
7077
const operation = block.subBlocks.find((field) => field.id === 'operation')!
71-
const options = typeof operation.options === 'function' ? operation.options() : operation.options
78+
const options =
79+
typeof operation.options === 'function' ? operation.options() : operation.options
7280
expect(options?.map((option) => option.id)).toEqual(OPERATIONS.map((name) => prefix + name))
73-
expect(operation.value?.({})).toBe(prefix + 'list_projects')
81+
expect(operation.value?.({})).toBe(`${prefix}list_projects`)
7482
expect(block.icon).toBe(NetSuiteIcon)
7583
expect(meta.templates).toHaveLength(7)
7684
expect(meta.skills).toHaveLength(3)
7785
expect(new Set(block.subBlocks.map((field) => field.id)).size).toBe(block.subBlocks.length)
7886
})
7987

80-
it.each(OPERATIONS)('routes %s before resolving numeric or structured workflow references', (operation) => {
81-
const toolId = block.tools.config!.tool!({ operation: prefix + operation, projectId: '<project.ProjectId>', limit: '<agent.limit>', planningResources: '<agent.lines>' })
82-
expect(toolId).toBe(prefix + operation)
83-
expect(block.tools.access).toContain(toolId)
84-
expect(block.canvasPresentation?.sentences?.byOperation?.[prefix + operation]).toBeDefined()
85-
})
88+
it.each(OPERATIONS)(
89+
'routes %s before resolving numeric or structured workflow references',
90+
(operation) => {
91+
const toolId = block.tools.config!.tool!({
92+
operation: prefix + operation,
93+
projectId: '<project.ProjectId>',
94+
limit: '<agent.limit>',
95+
planningResources: '<agent.lines>',
96+
})
97+
expect(toolId).toBe(prefix + operation)
98+
expect(block.tools.access).toContain(toolId)
99+
expect(block.canvasPresentation?.sentences?.byOperation?.[prefix + operation]).toBeDefined()
100+
}
101+
)
86102

87103
it('rejects unsupported operations without attempting reference coercion', () => {
88-
expect(() => block.tools.config!.tool!({ operation: 'arbitrary', limit: '<agent.limit>' })).toThrow('Unsupported')
104+
expect(() =>
105+
block.tools.config!.tool!({ operation: 'arbitrary', limit: '<agent.limit>' })
106+
).toThrow('Unsupported')
89107
})
90108

91109
it.each([
@@ -96,15 +114,28 @@ describe('Oracle Project Management block', () => {
96114
['projectRole', 'roles', ['credential']],
97115
['deliverableTypeId', 'deliverableTypes', ['credential']],
98116
['assignmentId', 'laborAssignments', ['credential', 'projectId', 'taskId']],
99-
] as const)('keeps %s basic/manual inputs canonical and dependency-bound', (field, selector, dependencies) => {
100-
const basic = block.subBlocks.find((input) => input.id === field + 'Selector')!
101-
const manual = block.subBlocks.find((input) => input.id === field + 'Manual')!
102-
expect(basic).toMatchObject({ type: 'project-selector', canonicalParamId: field, mode: 'basic', selectorKey: 'oracleFusionProjectManagement.' + selector, dependsOn: [...dependencies] })
103-
expect(manual).toMatchObject({ type: 'short-input', canonicalParamId: field, mode: 'advanced' })
104-
expect(basic.required).toEqual(manual.required)
105-
expect(basic.condition).toEqual(manual.condition)
106-
expect(block.inputs[field]).toBeDefined()
107-
})
117+
] as const)(
118+
'keeps %s basic/manual inputs canonical and dependency-bound',
119+
(field, selector, dependencies) => {
120+
const basic = block.subBlocks.find((input) => input.id === `${field}Selector`)!
121+
const manual = block.subBlocks.find((input) => input.id === `${field}Manual`)!
122+
expect(basic).toMatchObject({
123+
type: 'project-selector',
124+
canonicalParamId: field,
125+
mode: 'basic',
126+
selectorKey: `oracleFusionProjectManagement.${selector}`,
127+
dependsOn: [...dependencies],
128+
})
129+
expect(manual).toMatchObject({
130+
type: 'short-input',
131+
canonicalParamId: field,
132+
mode: 'advanced',
133+
})
134+
expect(basic.required).toEqual(manual.required)
135+
expect(basic.condition).toEqual(manual.condition)
136+
expect(block.inputs[field]).toBeDefined()
137+
}
138+
)
108139

109140
it('requires both project and task context for the task-assignment selector', () => {
110141
expect(selectorManifest['oracleFusionProjectManagement.laborAssignments'].context).toEqual({
@@ -114,22 +145,90 @@ describe('Oracle Project Management block', () => {
114145
})
115146

116147
it('maps resolved canonical values without numeric ID coercion and clears stale inputs', () => {
117-
const result = map('update_task', { projectId: '999999999999999999', taskId: '9007199254740993', physicalPercentComplete: '50', milestoneFlag: 'false', projectName: 'stale', invoiceId: 'stale' })
118-
expect(result).toMatchObject({ oauthCredential: 'credential-1', projectId: '999999999999999999', taskId: '9007199254740993', physicalPercentComplete: 50, milestoneFlag: false, projectName: undefined, invoiceId: undefined })
148+
const result = map('update_task', {
149+
projectId: '999999999999999999',
150+
taskId: '9007199254740993',
151+
physicalPercentComplete: '50',
152+
milestoneFlag: 'false',
153+
projectName: 'stale',
154+
invoiceId: 'stale',
155+
})
156+
expect(result).toMatchObject({
157+
oauthCredential: 'credential-1',
158+
projectId: '999999999999999999',
159+
taskId: '9007199254740993',
160+
physicalPercentComplete: 50,
161+
milestoneFlag: false,
162+
projectName: undefined,
163+
invoiceId: undefined,
164+
})
119165
expect(JSON.parse(JSON.stringify(result))).not.toHaveProperty('invoiceId')
120-
expect(map('update_project', { projectId: '101', projectDescription: null }).projectDescription).toBeNull()
166+
expect(
167+
map('update_project', { projectId: '101', projectDescription: null }).projectDescription
168+
).toBeNull()
121169
})
122170

123171
it('keeps task-assignment context out of updates and parses documented budget arrays after resolution', () => {
124-
expect(map('update_task_labor_resource_assignment', { projectId: '101', taskId: '202', assignmentId: '707', resourceEmail: 'a@example.test' })).toMatchObject({ taskId: undefined, assignmentId: '707', resourceEmail: 'a@example.test' })
125-
const resources = [{ RbsElementId: '999999999999999999', TaskId: '202', PlanningAmounts: [{ Currency: 'USD', Quantity: 2 }] }]
126-
expect(map('create_project_budget', { planningResources: JSON.stringify(resources) }).planningResources).toEqual(resources)
172+
expect(
173+
map('update_task_labor_resource_assignment', {
174+
projectId: '101',
175+
taskId: '202',
176+
assignmentId: '707',
177+
resourceEmail: 'a@example.test',
178+
})
179+
).toMatchObject({ taskId: undefined, assignmentId: '707', resourceEmail: 'a@example.test' })
180+
const resources = [
181+
{
182+
RbsElementId: '999999999999999999',
183+
TaskId: '202',
184+
PlanningAmounts: [{ Currency: 'USD', Quantity: 2 }],
185+
},
186+
]
187+
expect(
188+
map('create_project_budget', { planningResources: JSON.stringify(resources) }).planningResources
189+
).toEqual(resources)
127190
})
128191

129192
it('retains only action-specific invoice inputs when operations or transitions change', () => {
130-
const result = map('transition_project_contract_invoice', { invoiceId: '110', action: 'approve', invoiceDate: '2026-09-04', unreleaseComments: 'stale', receivablesNumber: 'stale' })
131-
expect(result).toMatchObject({ invoiceId: '110', action: 'approve', invoiceDate: undefined, unreleaseComments: undefined, receivablesNumber: undefined })
132-
expect(map('transition_project_contract_invoice', { invoiceId: '110', action: 'release', invoiceDate: '2026-09-04' }).invoiceDate).toBe('2026-09-04')
193+
const result = map('transition_project_contract_invoice', {
194+
invoiceId: '110',
195+
action: 'approve',
196+
invoiceDate: '2026-09-04',
197+
unreleaseComments: 'stale',
198+
receivablesNumber: 'stale',
199+
})
200+
expect(result).toMatchObject({
201+
invoiceId: '110',
202+
action: 'approve',
203+
invoiceDate: undefined,
204+
unreleaseComments: undefined,
205+
receivablesNumber: undefined,
206+
})
207+
expect(
208+
map('transition_project_contract_invoice', {
209+
invoiceId: '110',
210+
action: 'release',
211+
invoiceDate: '2026-09-04',
212+
}).invoiceDate
213+
).toBe('2026-09-04')
133214
})
134-
})
135215

216+
it.each([
217+
['invoiceDate', 'release'],
218+
['unreleaseComments', 'unrelease'],
219+
])('shows %s only for its transition or the ordinary invoice update', (id, action) => {
220+
const field = block.subBlocks.find((input) => input.id === id)!
221+
const condition = field.condition
222+
expect(typeof condition).toBe('function')
223+
if (typeof condition !== 'function') throw new Error('Expected action-aware condition')
224+
expect(condition({ operation: `${prefix}transition_project_contract_invoice` })).toEqual({
225+
field: 'operation',
226+
value: `${prefix}transition_project_contract_invoice`,
227+
and: { field: 'action', value: action },
228+
})
229+
expect(condition({ operation: `${prefix}update_project_contract_invoice` })).toEqual({
230+
field: 'operation',
231+
value: `${prefix}update_project_contract_invoice`,
232+
})
233+
})
234+
})

0 commit comments

Comments
 (0)