Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/commands/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'));
Expand Down
4 changes: 3 additions & 1 deletion src/mcp/tools/run-cloud-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions src/services/execution-plan.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, unknown>>,
): 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
Expand Down
32 changes: 32 additions & 0 deletions test/unit/extended-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading