* Read + validate the `--steps` file. Returns the parsed `planSteps` * array on success; throws a typed `VALIDATION_ERROR` envelope on any * schema problem with a `details.field` pointer the caller can act on. * * Stat-first guard mirrors piece-2's `readCodeFileGuarded`: oversize * payloads fai
(path: string)
| 1075 | * 256 KB (vs. 350 KB for code) per the piece-6 spec. |
| 1076 | */ |
| 1077 | function readPlanStepsFileGuarded(path: string): CliPlanStep[] { |
| 1078 | const absolute = resolveAbsolute(path); |
| 1079 | |
| 1080 | let stat; |
| 1081 | try { |
| 1082 | stat = statSync(absolute); |
| 1083 | } catch (err) { |
| 1084 | const code = (err as NodeJS.ErrnoException).code; |
| 1085 | if (code === 'ENOENT') { |
| 1086 | throw localValidationError('steps', `file does not exist: ${path}`); |
| 1087 | } |
| 1088 | if (code === 'EACCES') { |
| 1089 | throw localValidationError('steps', `permission denied reading ${path}`); |
| 1090 | } |
| 1091 | const reason = err instanceof Error ? err.message : 'unknown error'; |
| 1092 | throw localValidationError('steps', `cannot stat ${path}: ${reason}`); |
| 1093 | } |
| 1094 | if (stat.size > MAX_PLAN_STEPS_BODY_BYTES) { |
| 1095 | throw ApiError.fromEnvelope({ |
| 1096 | error: { |
| 1097 | code: 'PAYLOAD_TOO_LARGE', |
| 1098 | message: `Plan-steps body exceeds the 256 KB CLI cap (${stat.size} bytes).`, |
| 1099 | nextAction: 'Split into multiple smaller tests or trim step descriptions.', |
| 1100 | requestId: 'local', |
| 1101 | details: { |
| 1102 | field: 'steps', |
| 1103 | sizeBytes: stat.size, |
| 1104 | maxBytes: MAX_PLAN_STEPS_BODY_BYTES, |
| 1105 | }, |
| 1106 | }, |
| 1107 | }); |
| 1108 | } |
| 1109 | |
| 1110 | let raw; |
| 1111 | try { |
| 1112 | raw = stripBom(readFileSync(absolute, 'utf8')); |
| 1113 | } catch (err) { |
| 1114 | const reason = err instanceof Error ? err.message : 'unknown error'; |
| 1115 | throw localValidationError('steps', `cannot read ${path}: ${reason}`); |
| 1116 | } |
| 1117 | |
| 1118 | let parsed: unknown; |
| 1119 | try { |
| 1120 | parsed = JSON.parse(raw); |
| 1121 | } catch (err) { |
| 1122 | const reason = err instanceof Error ? err.message : 'unknown error'; |
| 1123 | throw localValidationError('steps', `not valid JSON: ${reason}`); |
| 1124 | } |
| 1125 | |
| 1126 | return assertPlanStepsShape(parsed); |
| 1127 | } |
| 1128 | |
| 1129 | /** |
| 1130 | * Type-narrow + validate a parsed `{ planSteps: [...] }` envelope. |
no test coverage detected