diff --git a/src/commands/cloud.ts b/src/commands/cloud.ts index fc3f04f..204665d 100644 --- a/src/commands/cloud.ts +++ b/src/commands/cloud.ts @@ -12,7 +12,7 @@ import { writeJSONFile, } from '../methods.js'; import { DeviceValidationService } from '../services/device-validation.service.js'; -import { plan } from '../services/execution-plan.service.js'; +import { countExtendedRuntimeFlows, plan } from '../services/execution-plan.service.js'; import { isSubmitEndpointMissing, submitWithRetry, @@ -774,6 +774,17 @@ export const cloudCommand = defineCommand({ ), ); } + + // The 2x is charged per flow, so say it before the run, not on the bill. + const extendedFlows = countExtendedRuntimeFlows(flowOverrides); + if (extendedFlows > 0) { + submitRows.push( + '', + ui.note( + `${extendedFlows} flow${extendedFlows === 1 ? '' : 's'} with extended runtime: up to 20 min each, billed at 2×`, + ), + ); + } } out(ui.section('Submitting new job')); diff --git a/src/mcp/tools/run-cloud-test.ts b/src/mcp/tools/run-cloud-test.ts index 99d1437..d099fe3 100644 --- a/src/mcp/tools/run-cloud-test.ts +++ b/src/mcp/tools/run-cloud-test.ts @@ -115,7 +115,9 @@ export function registerRunCloudTest(server: McpServer): void { .min(30) .max(3600) .optional() - .describe('Max seconds to wait when wait is true (default 600)'), + .describe( + 'Max seconds to wait when wait is true (default 600). A flow with DEVICECLOUD_OVERRIDE_EXTENDED_RUNTIME: true in its env can run for up to 20 minutes, so raise this when the run includes one.', + ), }, annotations: { title: 'Run a cloud test', diff --git a/src/services/execution-plan.service.ts b/src/services/execution-plan.service.ts index 994c378..41ff84d 100644 --- a/src/services/execution-plan.service.ts +++ b/src/services/execution-plan.service.ts @@ -173,6 +173,19 @@ function extractDeviceCloudOverrides( return overrides; } +/** + * How many flows opt into the extended test runtime (up to 20 minutes, billed + * accepts: YAML `true` or the string `'true'`. + * accepts: YAML `true` or the string `'true'`. + */ +export function countExtendedRuntimeFlows( + flowOverrides: Record>, +): number { + return Object.values(flowOverrides).filter( + ({ EXTENDED_RUNTIME }) => EXTENDED_RUNTIME === true || EXTENDED_RUNTIME === 'true', + ).length; +} + /** * Generate execution plan for a single flow file * @param normalizedInput - Normalized path to the flow file diff --git a/test/unit/extended-runtime.test.ts b/test/unit/extended-runtime.test.ts new file mode 100644 index 0000000..56c6ac1 --- /dev/null +++ b/test/unit/extended-runtime.test.ts @@ -0,0 +1,32 @@ +import { expect } from 'chai'; + +import { countExtendedRuntimeFlows } from '../../src/services/execution-plan.service.js'; + +/** + * `dcd cloud` notes how many flows opted into the extended runtime, because + * the API bills each of them at 2x. It must count exactly the + * values the API treats as opted in: YAML `true` and the string 'true'. + */ +describe('countExtendedRuntimeFlows', () => { + it('counts flows whose override is true or "true"', () => { + expect( + countExtendedRuntimeFlows({ + 'a.yaml': { EXTENDED_RUNTIME: true }, + 'b.yaml': { EXTENDED_RUNTIME: 'true' }, + 'c.yaml': { DEVICE_LOCALE: 'de_DE' }, + 'd.yaml': {}, + }), + ).to.equal(2); + }); + + it('ignores values the API would not treat as opted in', () => { + expect( + countExtendedRuntimeFlows({ + 'a.yaml': { EXTENDED_RUNTIME: false }, + 'b.yaml': { EXTENDED_RUNTIME: 'false' }, + 'c.yaml': { EXTENDED_RUNTIME: 'TRUE' }, + 'd.yaml': { EXTENDED_RUNTIME: 1 }, + }), + ).to.equal(0); + }); +});