(
opts: CreateOptions,
deps: TestDeps = {},
)
| 636 | * convention; no separate "envelope-only" output mode. |
| 637 | */ |
| 638 | export async function runCreate( |
| 639 | opts: CreateOptions, |
| 640 | deps: TestDeps = {}, |
| 641 | ): Promise<CliCreateTestResponse> { |
| 642 | // P1-2: validate idempotency key before any I/O — non-ASCII chars cause a |
| 643 | // ByteString TypeError at the HTTP transport layer (exit 10 UNAVAILABLE). |
| 644 | assertIdempotencyKey(opts.idempotencyKey); |
| 645 | // codex #128 P2: the `--run` chain derives `<key>:run` (see below); validate |
| 646 | // that derived key BEFORE the create POST so a near-limit base key fails fast |
| 647 | // (exit 5) instead of creating the test and THEN rejecting the 257+ char run |
| 648 | // key — which would orphan a created test with no run. |
| 649 | assertChainedRunKeyFits(opts.run, opts.idempotencyKey); |
| 650 | // Validate inputs before touching credentials or fs — matches the |
| 651 | // M2 read commands' "input gates first, then auth, then I/O" ordering. |
| 652 | requireProjectId(opts.projectId); |
| 653 | requireNonEmpty('name', opts.name); |
| 654 | // P1-3: client-side length checks matching server limits (name ≤200, |
| 655 | // description ≤2000) so the user gets instant, actionable errors instead |
| 656 | // of a cryptic server validation message. |
| 657 | if (opts.name !== undefined && opts.name.length > 200) { |
| 658 | throw localValidationError('name', 'must be at most 200 characters'); |
| 659 | } |
| 660 | if (opts.description !== undefined && opts.description.length > 2000) { |
| 661 | throw localValidationError('description', 'must be at most 2000 characters'); |
| 662 | } |
| 663 | // P2-11: extend the required-flag error message to suggest --plan-from for |
| 664 | // FE tests so operators who missed that flag get an actionable hint. |
| 665 | if (typeof opts.codeFile !== 'string' || opts.codeFile.length === 0) { |
| 666 | throw ApiError.fromEnvelope({ |
| 667 | error: { |
| 668 | code: 'VALIDATION_ERROR', |
| 669 | message: 'Invalid request.', |
| 670 | nextAction: |
| 671 | 'Flag `--code-file` is invalid: is required. ' + |
| 672 | 'For frontend tests you can also supply the plan with `--plan-from <plan.json>` instead of a code file.', |
| 673 | requestId: 'local', |
| 674 | details: { field: 'codeFile', reason: 'is required' }, |
| 675 | }, |
| 676 | }); |
| 677 | } |
| 678 | if (!['frontend', 'backend'].includes(opts.type)) { |
| 679 | throw localValidationError('type', 'must be one of: frontend, backend', [ |
| 680 | 'frontend', |
| 681 | 'backend', |
| 682 | ]); |
| 683 | } |
| 684 | if (opts.priority !== undefined && !CLI_CREATE_PRIORITIES.includes(opts.priority)) { |
| 685 | throw localValidationError('priority', `must be one of: ${CLI_CREATE_PRIORITIES.join(', ')}`, [ |
| 686 | ...CLI_CREATE_PRIORITIES, |
| 687 | ]); |
| 688 | } |
| 689 | |
| 690 | // M4 piece-2: --produces/--needs/--category are backend-only. FE plans have |
| 691 | // no wave model; fail-fast client-side to match the backend's 400 reject and |
| 692 | // save a round-trip. |
| 693 | if (opts.type === 'frontend') { |
| 694 | const depFlags: string[] = []; |
| 695 | if (opts.produces !== undefined && opts.produces.length > 0) depFlags.push('--produces'); |
no test coverage detected