Skip to content

Commit eb596d3

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(oracle-epm-planning): repair contract test and CI formatting
1 parent 552557b commit eb596d3

7 files changed

Lines changed: 366 additions & 130 deletions

File tree

‎apps/sim/blocks/blocks/oracle_epm_planning.ts‎

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

‎apps/sim/lib/internal/oracle-epm-planning/execute-tool.test.ts‎

Lines changed: 124 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,9 @@ describe('Planning operation contracts through the real foundation', () => {
11721172
expect(result.status).toBe(status)
11731173
expect(result.result.success).toBe(false)
11741174
expect(result.result.output.updated).toBeUndefined()
1175+
expect(mocks.fetch).toHaveBeenCalledTimes(1)
1176+
}
1177+
)
11751178
it('accepts only empty 204 as user-variable write confirmation', async () => {
11761179
const entry = CASES.find((item) => item.operation === 'set_user_variable_values')!
11771180
respond(null, 204)
@@ -1180,7 +1183,11 @@ describe('Planning operation contracts through the real foundation', () => {
11801183
expect((await invoke(entry.operation, entry.input)).result.success).toBe(false)
11811184
})
11821185
it('returns empty bounded lists without manufacturing completion flags', async () => {
1183-
for (const operation of ['list_user_variable_values', 'list_planning_units', 'get_planning_unit_history']) {
1186+
for (const operation of [
1187+
'list_user_variable_values',
1188+
'list_planning_units',
1189+
'get_planning_unit_history',
1190+
]) {
11841191
const entry = CASES.find((item) => item.operation === operation)!
11851192
respond({ items: [] })
11861193
const { result } = await invoke(operation, entry.input)
@@ -1190,40 +1197,81 @@ describe('Planning operation contracts through the real foundation', () => {
11901197
}
11911198
})
11921199
it('rejects malformed user-variable values and oversized batches before requests', async () => {
1193-
for (const userVariableValues of [[], [{ name: 'CurrentEntity' }], Array(1001).fill({ userName: 'planner', name: 'CurrentEntity', dimension: 'Entity', member: 'Marketing' })]) {
1194-
expect((await invoke('set_user_variable_values', { application: 'Vision', userVariableValues })).status).toBe(400)
1200+
for (const userVariableValues of [
1201+
[],
1202+
[{ name: 'CurrentEntity' }],
1203+
Array(1001).fill({
1204+
userName: 'planner',
1205+
name: 'CurrentEntity',
1206+
dimension: 'Entity',
1207+
member: 'Marketing',
1208+
}),
1209+
]) {
1210+
expect(
1211+
(await invoke('set_user_variable_values', { application: 'Vision', userVariableValues })).status
1212+
).toBe(400)
11951213
}
11961214
expect(mocks.fetch).not.toHaveBeenCalled()
11971215
})
11981216
it('preserves documented null planning-unit metadata and annotation sequences', async () => {
11991217
const units = CASES.find((item) => item.operation === 'list_planning_units')!
12001218
respond(units.response)
12011219
expect((await invoke(units.operation, units.input)).result.output.planningUnits?.[0]).toMatchObject({
1202-
name: null, secMember: null, scenarioAlias: null, versionAlias: null, puId: 50410,
1220+
name: null,
1221+
secMember: null,
1222+
scenarioAlias: null,
1223+
versionAlias: null,
1224+
puId: 50410,
12031225
})
12041226
const history = CASES.find((item) => item.operation === 'get_planning_unit_history')!
12051227
respond(history.response)
1206-
expect((await invoke(history.operation, history.input)).result.output.planningUnitHistory?.[0]).toMatchObject({
1207-
hasHistory: false, logSeq: -1, parentAnntSeq: 1,
1228+
expect(
1229+
(await invoke(history.operation, history.input)).result.output.planningUnitHistory?.[0]
1230+
).toMatchObject({
1231+
hasHistory: false,
1232+
logSeq: -1,
1233+
parentAnntSeq: 1,
12081234
})
12091235
})
12101236
it('does not confuse approval confirmation with a job or echo unconfirmed input', async () => {
12111237
const entry = CASES.find((item) => item.operation === 'change_planning_unit_status')!
12121238
respond(entry.response)
12131239
expect((await invoke(entry.operation, entry.input)).result.output).toEqual({
1214-
planningUnitAction: { pmMembers: '"Marketing"', action: 'PROMOTE', comments: '"Ready & reviewed + approved"' },
1240+
planningUnitAction: {
1241+
pmMembers: '"Marketing"',
1242+
action: 'PROMOTE',
1243+
comments: '"Ready & reviewed + approved"',
1244+
},
12151245
})
12161246
for (const response of [{}, { links: [] }, { jobId: 42, status: 0 }]) {
12171247
respond(response)
12181248
expect((await invoke(entry.operation, entry.input)).result.success).toBe(false)
12191249
}
12201250
})
12211251
it.each([
1222-
['https://other.example.com/HyperionPlanning/rest/v3/applications/Vision/planningunits/Forecast/actions', 'POST'],
1223-
['https://epm.example.com/HyperionPlanning/rest/v2/applications/Vision/planningunits/Forecast/actions', 'POST'],
1224-
['https://epm.example.com/HyperionPlanning/rest/v3/applications/Vision/planningunits/Forecast/actions', 'GET'],
1252+
[
1253+
'https://other.example.com/HyperionPlanning/rest/v3/applications/Vision/planningunits/Forecast/actions',
1254+
'POST',
1255+
],
1256+
[
1257+
'https://epm.example.com/HyperionPlanning/rest/v2/applications/Vision/planningunits/Forecast/actions',
1258+
'POST',
1259+
],
1260+
[
1261+
'https://epm.example.com/HyperionPlanning/rest/v3/applications/Vision/planningunits/Forecast/actions',
1262+
'GET',
1263+
],
12251264
])('rejects invalid approval confirmation links without replay: %s %s', async (href, action) => {
1226-
respond({ links: [{ rel: 'self', href, action, data: { pmMembers: 'Marketing', action: 'PROMOTE', comments: '' } }] })
1265+
respond({
1266+
links: [
1267+
{
1268+
rel: 'self',
1269+
href,
1270+
action,
1271+
data: { pmMembers: 'Marketing', action: 'PROMOTE', comments: '' },
1272+
},
1273+
],
1274+
})
12271275
const entry = CASES.find((item) => item.operation === 'change_planning_unit_status')!
12281276
expect((await invoke(entry.operation, entry.input)).result.success).toBe(false)
12291277
expect(mocks.fetch).toHaveBeenCalledTimes(1)
@@ -1232,33 +1280,72 @@ describe('Planning operation contracts through the real foundation', () => {
12321280
const entry = CASES.find((item) => item.operation === 'get_insights')!
12331281
respond(entry.response)
12341282
const { result } = await invoke(entry.operation, entry.input)
1235-
expect(result.output).toMatchObject({ insights: [{ id: '426', outlierValue: 0 }], totalResults: 2, hasMore: true })
1283+
expect(result.output).toMatchObject({
1284+
insights: [{ id: '426', outlierValue: 0 }],
1285+
totalResults: 2,
1286+
hasMore: true,
1287+
})
12361288
expect(mocks.fetch).toHaveBeenCalledTimes(1)
12371289
respond({ items: [], totalResults: 0, hasMore: false })
1238-
expect((await invoke(entry.operation, entry.input)).result.output).toEqual({ insights: [], totalResults: 0, hasMore: false })
1290+
expect((await invoke(entry.operation, entry.input)).result.output).toEqual({
1291+
insights: [],
1292+
totalResults: 0,
1293+
hasMore: false,
1294+
})
12391295
respond({ items: [], totalResults: 0 })
12401296
expect((await invoke(entry.operation, entry.input)).result.success).toBe(false)
12411297
})
12421298
it('requires the distinct insight slice and an explicit calendar for recomputation', async () => {
12431299
const entry = CASES.find((item) => item.operation === 'get_insights')!
1244-
expect((await invoke(entry.operation, { ...entry.input, retrievalMode: 'FORCE_RECOMPUTE' })).status).toBe(400)
1245-
expect((await invoke(entry.operation, { ...entry.input, insightSlice: { pov: [], columns: [], rows: [] } })).status).toBe(400)
1300+
expect(
1301+
(await invoke(entry.operation, { ...entry.input, retrievalMode: 'FORCE_RECOMPUTE' })).status
1302+
).toBe(400)
1303+
expect(
1304+
(
1305+
await invoke(entry.operation, {
1306+
...entry.input,
1307+
insightSlice: { pov: [], columns: [], rows: [] },
1308+
})
1309+
).status
1310+
).toBe(400)
12461311
expect(mocks.fetch).not.toHaveBeenCalled()
12471312
respond(entry.response)
1248-
await invoke(entry.operation, { ...entry.input, retrievalMode: 'FORCE_RECOMPUTE', calendar: 'Fiscal' })
1249-
expect(JSON.parse(mocks.fetch.mock.calls[0][2].body)).toMatchObject({ retrievalMode: 'FORCE_RECOMPUTE', calendar: 'Fiscal' })
1313+
await invoke(entry.operation, {
1314+
...entry.input,
1315+
retrievalMode: 'FORCE_RECOMPUTE',
1316+
calendar: 'Fiscal',
1317+
})
1318+
expect(JSON.parse(mocks.fetch.mock.calls[0][2].body)).toMatchObject({
1319+
retrievalMode: 'FORCE_RECOMPUTE',
1320+
calendar: 'Fiscal',
1321+
})
12501322
expect(mocks.fetch).toHaveBeenCalledTimes(1)
12511323
})
12521324
it('sends only the selected summary mode and always requests text', async () => {
12531325
const get = CASES.find((item) => item.operation === 'get_insights')!
12541326
respond({ summary: 'Variance summary', warnings: [], resolvedNarrative: { ignored: true } })
1255-
const ids = await invoke('summarize_insights', { ...get.input, summaryInputMode: 'ids', insightIds: ['426'], retrievalMode: 'FORCE_RECOMPUTE', calendar: 'Stale', summarySize: 80 })
1327+
const ids = await invoke('summarize_insights', {
1328+
...get.input,
1329+
summaryInputMode: 'ids',
1330+
insightIds: ['426'],
1331+
retrievalMode: 'FORCE_RECOMPUTE',
1332+
calendar: 'Stale',
1333+
summarySize: 80,
1334+
})
12561335
expect(ids.result.output).toEqual({ summary: 'Variance summary' })
1257-
expect(JSON.parse(mocks.fetch.mock.calls[0][2].body)).toEqual({ ids: ['426'], format: 'text', size: 80 })
1336+
expect(JSON.parse(mocks.fetch.mock.calls[0][2].body)).toEqual({
1337+
ids: ['426'],
1338+
format: 'text',
1339+
size: 80,
1340+
})
12581341
await invoke('summarize_insights', { ...get.input, summaryInputMode: 'slice', insightIds: ['999'] })
12591342
expect(JSON.parse(mocks.fetch.mock.calls[1][2].body)).toEqual({
1260-
dataSourceType: 'CUBE', location: 'Plan1', slice: get.input.insightSlice,
1261-
retrievalMode: 'USE_EXISTING', format: 'text', size: 100,
1343+
dataSourceType: 'CUBE',
1344+
location: 'Plan1',
1345+
slice: get.input.insightSlice,
1346+
retrievalMode: 'USE_EXISTING',
1347+
format: 'text',
1348+
size: 100,
12621349
})
12631350
})
12641351
it('rejects incomplete summary inputs and undocumented JSON summaries', async () => {
@@ -1268,14 +1355,27 @@ describe('Planning operation contracts through the real foundation', () => {
12681355
{ summaryInputMode: 'slice', cube: 'Plan1' },
12691356
{ summaryInputMode: 'slice', insightSlice: {} },
12701357
{ summaryInputMode: 'unknown', insightIds: ['426'] },
1271-
]) expect((await invoke('summarize_insights', { application: 'Vision', ...input })).status).toBe(400)
1358+
])
1359+
expect((await invoke('summarize_insights', { application: 'Vision', ...input })).status).toBe(400)
12721360
expect(mocks.fetch).not.toHaveBeenCalled()
12731361
respond({ summary: { text: 'Not a documented text result' } })
1274-
expect((await invoke('summarize_insights', { application: 'Vision', summaryInputMode: 'ids', insightIds: ['426'] })).result.success).toBe(false)
1362+
expect(
1363+
(
1364+
await invoke('summarize_insights', {
1365+
application: 'Vision',
1366+
summaryInputMode: 'ids',
1367+
insightIds: ['426'],
1368+
})
1369+
).result.success
1370+
).toBe(false)
12751371
})
12761372
it('rejects oversized insight summaries clearly instead of truncating them', async () => {
12771373
respond({ summary: 'x'.repeat(16 * 1024 * 1024) })
1278-
const result = await invoke('summarize_insights', { application: 'Vision', summaryInputMode: 'ids', insightIds: ['426'] })
1374+
const result = await invoke('summarize_insights', {
1375+
application: 'Vision',
1376+
summaryInputMode: 'ids',
1377+
insightIds: ['426'],
1378+
})
12791379
expect(result.status).toBe(413)
12801380
expect(result.result.success).toBe(false)
12811381
expect(result.result.output.summary).toBeUndefined()

‎apps/sim/lib/internal/oracle-epm-planning/schema.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,11 @@ export const planningSummarizeInsightsInputSchema = z
216216
.superRefine((input, context) => {
217217
if (input.summaryInputMode === 'ids') {
218218
if (!input.insightIds)
219-
context.addIssue({ code: 'custom', path: ['insightIds'], message: 'Insight IDs are required' })
219+
context.addIssue({
220+
code: 'custom',
221+
path: ['insightIds'],
222+
message: 'Insight IDs are required',
223+
})
220224
} else {
221225
if (!input.cube)
222226
context.addIssue({
@@ -296,7 +300,9 @@ export const planningUnitStatusSchema = z.object({
296300
rel: z.string(),
297301
href: z.string(),
298302
action: z.string(),
299-
data: z.object({ pmMembers: z.string(), action: z.string(), comments: z.string() }).optional(),
303+
data: z
304+
.object({ pmMembers: z.string(), action: z.string(), comments: z.string() })
305+
.optional(),
300306
})
301307
)
302308
.max(1000),
@@ -305,7 +311,12 @@ export const insightsSchema = z.object({
305311
items: z.array(
306312
z.object({
307313
id: count.transform(String),
308-
type: z.enum(['ANOMALY', 'MOVEMENT_VARIANCE_INSIGHTS', 'HISTORICAL_INSIGHTS', 'FUTURE_INSIGHTS']),
314+
type: z.enum([
315+
'ANOMALY',
316+
'MOVEMENT_VARIANCE_INSIGHTS',
317+
'HISTORICAL_INSIGHTS',
318+
'FUTURE_INSIGHTS',
319+
]),
309320
accountName: z.string().optional(),
310321
sourceAccountName: z.string().optional(),
311322
planType: z.string().optional(),

‎apps/sim/tools/oracle_epm_planning/list_planning_units.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ export const oracleEpmPlanningListPlanningUnitsTool: InternalToolConfig<
3131
outputs: {
3232
planningUnits: {
3333
type: 'array',
34-
description:
35-
'One page of owned planning units; numeric puId is not a compound identifier',
34+
description: 'One page of owned planning units; numeric puId is not a compound identifier',
3635
items: {
3736
type: 'object',
3837
properties: {

‎apps/sim/tools/oracle_epm_planning/oracle_epm_planning.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,11 @@ function operationMatches(
268268
): boolean {
269269
if (value === true || value === undefined) return true
270270
if (!value) return false
271-
if (typeof value === 'function') return operationMatches(value({ operation, summaryInputMode: 'slice', retrievalMode: 'FORCE_RECOMPUTE' }), operation)
271+
if (typeof value === 'function')
272+
return operationMatches(
273+
value({ operation, summaryInputMode: 'slice', retrievalMode: 'FORCE_RECOMPUTE' }),
274+
operation
275+
)
272276
return (Array.isArray(value.value) ? value.value : [value.value]).includes(operation)
273277
}
274278
describe('Planning integration surface (NetSuite whole-integration precedent)', () => {

‎apps/sim/tools/oracle_epm_planning/run_data_map.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ export const oracleEpmPlanningRunDataMapTool: InternalToolConfig<
2525
jobName: { ...oracleEpmPlanningParamFields.jobName, required: true },
2626
clearData: { ...oracleEpmPlanningParamFields.clearData, required: true },
2727
overrideMembersMap: { ...oracleEpmPlanningParamFields.overrideMembersMap, required: false },
28-
overrideExclusionMembersMap: { ...oracleEpmPlanningParamFields.overrideExclusionMembersMap, required: false },
28+
overrideExclusionMembersMap: {
29+
...oracleEpmPlanningParamFields.overrideExclusionMembersMap,
30+
required: false,
31+
},
2932
},
3033
operation: { input: createInternalToolOperationInput },
3134
outputs: {

0 commit comments

Comments
 (0)