(
opts: UpdateOptions,
deps: TestDeps = {},
)
| 1204 | * matches piece-2's pattern. |
| 1205 | */ |
| 1206 | export async function runUpdate( |
| 1207 | opts: UpdateOptions, |
| 1208 | deps: TestDeps = {}, |
| 1209 | ): Promise<CliUpdateTestResponse> { |
| 1210 | assertIdempotencyKey(opts.idempotencyKey); |
| 1211 | requireNonEmpty('test-id', opts.testId); |
| 1212 | // P1-3: client-side length checks matching server limits. |
| 1213 | if (opts.name !== undefined && opts.name.length > 200) { |
| 1214 | throw localValidationError('name', 'must be at most 200 characters'); |
| 1215 | } |
| 1216 | if (opts.description !== undefined && opts.description.length > 2000) { |
| 1217 | throw localValidationError('description', 'must be at most 2000 characters'); |
| 1218 | } |
| 1219 | if (opts.priority !== undefined && !CLI_CREATE_PRIORITIES.includes(opts.priority)) { |
| 1220 | throw localValidationError('priority', `must be one of: ${CLI_CREATE_PRIORITIES.join(', ')}`, [ |
| 1221 | ...CLI_CREATE_PRIORITIES, |
| 1222 | ]); |
| 1223 | } |
| 1224 | |
| 1225 | // No-op rejection: requires at least one of the three patchable |
| 1226 | // fields. Caught before fetching credentials or building the |
| 1227 | // request so the user gets the cheapest possible error. |
| 1228 | const hasName = opts.name !== undefined; |
| 1229 | const hasDescription = opts.description !== undefined; |
| 1230 | const hasPriority = opts.priority !== undefined; |
| 1231 | if (!hasName && !hasDescription && !hasPriority) { |
| 1232 | throw localValidationError( |
| 1233 | 'fields', |
| 1234 | 'at least one of --name / --description / --priority must be set', |
| 1235 | ['name', 'description', 'priority'], |
| 1236 | ); |
| 1237 | } |
| 1238 | |
| 1239 | const idempotencyKey = opts.idempotencyKey ?? `cli-update-${randomUUID()}`; |
| 1240 | if (opts.idempotencyKey === undefined && (opts.output === 'json' || opts.verbose || opts.debug)) { |
| 1241 | const stderr = deps.stderr ?? ((line: string) => process.stderr.write(`${line}\n`)); |
| 1242 | stderr(`idempotency-key: ${idempotencyKey}`); |
| 1243 | } |
| 1244 | |
| 1245 | // Body carries only the fields the caller passed. Sending |
| 1246 | // `{ name: undefined }` would JSON-serialize to omit the key, which |
| 1247 | // is the intended wire shape — but we build the body deliberately |
| 1248 | // so the contract is auditable rather than dependent on |
| 1249 | // JSON.stringify undefined-skipping. |
| 1250 | const body: Record<string, string> = {}; |
| 1251 | if (hasName) body.name = opts.name!; |
| 1252 | if (hasDescription) body.description = opts.description!; |
| 1253 | if (hasPriority) body.priority = opts.priority!; |
| 1254 | |
| 1255 | const client = makeClient(opts, deps); |
| 1256 | const out = makeOutput(opts.output, deps); |
| 1257 | const response = await client.put<CliUpdateTestResponse>( |
| 1258 | `/tests/${encodeURIComponent(opts.testId)}`, |
| 1259 | { |
| 1260 | body, |
| 1261 | headers: { 'idempotency-key': idempotencyKey }, |
| 1262 | }, |
| 1263 | ); |
no test coverage detected