11/** @vitest -environment node */
22import { describe , expect , it } from 'vitest'
3- import { OracleFusionProjectManagementBlock as block , OracleFusionProjectManagementBlockMeta as meta } from '@/blocks/blocks/oracle_fusion_project_management'
43import { NetSuiteIcon } from '@/components/icons'
54import { selectorManifest } from '@/lib/selectors/manifest'
5+ import {
6+ OracleFusionProjectManagementBlock as block ,
7+ OracleFusionProjectManagementBlockMeta as meta ,
8+ } from '@/blocks/blocks/oracle_fusion_project_management'
69
710const prefix = 'oracle_fusion_project_management_'
811const OPERATIONS = [
@@ -62,30 +65,45 @@ const OPERATIONS = [
6265] as const
6366
6467function 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
6875describe ( '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