Skip to content

Commit 622a8a9

Browse files
authored
refactor(errors): migrate input validation errors to use the modeled exception. (#1848)
* feat(errors): migrate input parsing errors to modeled validation errors * fix(errors): swap router tests to check for error type instead of message * fix(errors): migrate source resolver to use input validation
1 parent 21353dd commit 622a8a9

31 files changed

Lines changed: 116 additions & 67 deletions

File tree

src/handlers/config/config.test.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
55
import { createRootHandler } from "../index";
66
import { createSilentLogger, TestCoreClient, testIO } from "../../testing";
77
import { DefaultGlobalConfigAccessor } from "../../globalConfig";
8+
import { InputValidationError } from "../../errors";
89
import { FsReadWriteJson } from "../../io";
910

1011
describe("config", () => {
@@ -94,13 +95,11 @@ describe("config", () => {
9495
});
9596

9697
test("throws on invalid key", async () => {
97-
// TODO: swap to validation error.
98-
await expect(run(["nonexistent.key"])).rejects.toThrow(TypeError);
98+
await expect(run(["nonexistent.key"])).rejects.toThrow(InputValidationError);
9999
});
100100

101101
test("throws on invalid value for key", async () => {
102-
// TODO: swap to validation error
103-
await expect(run(["telemetry.enabled", "banana"])).rejects.toThrow(TypeError);
102+
await expect(run(["telemetry.enabled", "banana"])).rejects.toThrow(InputValidationError);
104103
});
105104

106105
test("coerces values based on schema", async () => {

src/handlers/config/handler.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import z from "zod";
22
import { createHandler, argument, GlobalConfigAccessorKey } from "../../router";
33
import { JsonRendererKey } from "../../tui";
4+
import { InputValidationError } from "../../errors";
45
import { DEFAULT_GLOBAL_CONFIG, type GlobalConfig } from "../../globalConfig";
56

67
/*
@@ -63,16 +64,14 @@ function coerceValue(current: unknown, raw: string, path: string): unknown {
6364
const normalized = raw.trim().toLowerCase();
6465
if (normalized === "true") return true;
6566
if (normalized === "false") return false;
66-
// TODO: mark as validation error.
67-
throw new TypeError(`Cannot coerce "${raw}" to boolean at "${path}"`);
67+
throw new InputValidationError(`Cannot coerce "${raw}" to boolean at "${path}"`);
6868
}
6969

7070
case "number": {
7171
const trimmed = raw.trim();
7272
const n = Number(trimmed);
7373
if (trimmed === "" || Number.isNaN(n)) {
74-
// TODO: mark as validation error.
75-
throw new TypeError(`Cannot coerce "${raw}" to number at "${path}"`);
74+
throw new InputValidationError(`Cannot coerce "${raw}" to number at "${path}"`);
7675
}
7776
return n;
7877
}
@@ -81,15 +80,14 @@ function coerceValue(current: unknown, raw: string, path: string): unknown {
8180
try {
8281
return JSON.parse(raw);
8382
} catch (e) {
84-
// TODO: mark as validation error.
85-
86-
throw new TypeError(`Cannot coerce "${raw}" to object at "${path}"`, { cause: e });
83+
throw new InputValidationError(`Cannot coerce "${raw}" to object at "${path}"`, {
84+
cause: e,
85+
});
8786
}
8887
}
8988

9089
default:
91-
// TODO: mark as validation error.
92-
throw new TypeError(`Unsupported target type "${typeof current}" at "${path}"`);
90+
throw new InputValidationError(`Unsupported target type "${typeof current}" at "${path}"`);
9391
}
9492
}
9593
/** Type guard that narrows `value` to a plain object record. */

src/handlers/harness/create/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { createHandler, flag } from "../../../router";
1313
import type { Core } from "../../types.tsx";
1414
import { coreOptsFromCtx, parseJsonFlag } from "../../utils.tsx";
1515
import { JsonRendererKey } from "../../../tui";
16+
import { InputValidationError } from "../../../errors";
1617
import { parameterHelp } from "../parameterHelp.tsx";
1718

1819
export const createCreateHarnessHandler = (core: Core) =>
@@ -93,7 +94,7 @@ export const createCreateHarnessHandler = (core: Core) =>
9394
// Required at runtime but declared optional so that a bare
9495
// `harness create` falls through to the TUI middleware instead.
9596
if (!flags["name"]) {
96-
throw new TypeError("required option '--name <name>' not specified");
97+
throw new InputValidationError("required option '--name <name>' not specified");
9798
}
9899

99100
const response = await core.harness.createHarness(

src/handlers/harness/delete/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../router";
33
import type { Core } from "../../types.tsx";
44
import { coreOptsFromCtx } from "../../utils.tsx";
55
import { JsonRendererKey } from "../../../tui";
6+
import { InputValidationError } from "../../../errors";
67

78
export const createDeleteHarnessHandler = (core: Core) =>
89
createHandler({
@@ -21,7 +22,7 @@ export const createDeleteHarnessHandler = (core: Core) =>
2122
// Required at runtime but declared optional so that a bare
2223
// `harness delete` falls through to the TUI middleware instead.
2324
if (!flags["id"]) {
24-
throw new TypeError("required option '--id <id>' not specified");
25+
throw new InputValidationError("required option '--id <id>' not specified");
2526
}
2627

2728
const response = await core.harness.deleteHarness(

src/handlers/harness/endpoint/create/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
33
import type { Core } from "../../../types.tsx";
44
import { coreOptsFromCtx, parseJsonFlag } from "../../../utils.tsx";
55
import { JsonRendererKey } from "../../../../tui";
6+
import { InputValidationError } from "../../../../errors";
67

78
export const createCreateEndpointHandler = (core: Core) =>
89
createHandler({
@@ -23,10 +24,10 @@ export const createCreateEndpointHandler = (core: Core) =>
2324
// Required at runtime but declared optional so that a bare
2425
// `harness endpoint create` falls through to the TUI middleware instead.
2526
if (!flags["id"]) {
26-
throw new TypeError("required option '--id <id>' not specified");
27+
throw new InputValidationError("required option '--id <id>' not specified");
2728
}
2829
if (!flags["name"]) {
29-
throw new TypeError("required option '--name <name>' not specified");
30+
throw new InputValidationError("required option '--name <name>' not specified");
3031
}
3132

3233
const response = await core.harness.createHarnessEndpoint(

src/handlers/harness/endpoint/delete/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
33
import type { Core } from "../../../types.tsx";
44
import { coreOptsFromCtx } from "../../../utils.tsx";
55
import { JsonRendererKey } from "../../../../tui";
6+
import { InputValidationError } from "../../../../errors";
67

78
export const createDeleteEndpointHandler = (core: Core) =>
89
createHandler({
@@ -17,10 +18,10 @@ export const createDeleteEndpointHandler = (core: Core) =>
1718
// Required at runtime but declared optional so that a bare
1819
// `harness endpoint delete` falls through to the TUI middleware instead.
1920
if (!flags["id"]) {
20-
throw new TypeError("required option '--id <id>' not specified");
21+
throw new InputValidationError("required option '--id <id>' not specified");
2122
}
2223
if (!flags["qualifier"]) {
23-
throw new TypeError("required option '--qualifier <qualifier>' not specified");
24+
throw new InputValidationError("required option '--qualifier <qualifier>' not specified");
2425
}
2526

2627
const response = await core.harness.deleteHarnessEndpoint(

src/handlers/harness/endpoint/get/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
33
import type { Core } from "../../../types.tsx";
44
import { coreOptsFromCtx } from "../../../utils.tsx";
55
import { JsonRendererKey } from "../../../../tui";
6+
import { InputValidationError } from "../../../../errors";
67

78
export const createGetEndpointHandler = (core: Core) =>
89
createHandler({
@@ -14,10 +15,10 @@ export const createGetEndpointHandler = (core: Core) =>
1415
],
1516
handle: async (ctx, flags) => {
1617
if (!flags["id"]) {
17-
throw new TypeError("required option '--id <id>' not specified");
18+
throw new InputValidationError("required option '--id <id>' not specified");
1819
}
1920
if (!flags["qualifier"]) {
20-
throw new TypeError("required option '--qualifier <qualifier>' not specified");
21+
throw new InputValidationError("required option '--qualifier <qualifier>' not specified");
2122
}
2223

2324
const endpoint = await core.harness.getHarnessEndpoint(

src/handlers/harness/endpoint/list/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
33
import type { Core } from "../../../types.tsx";
44
import { coreOptsFromCtx } from "../../../utils.tsx";
55
import { JsonRendererKey } from "../../../../tui";
6+
import { InputValidationError } from "../../../../errors";
67

78
export const createListEndpointsHandler = (core: Core) =>
89
createHandler({
@@ -15,7 +16,7 @@ export const createListEndpointsHandler = (core: Core) =>
1516
],
1617
handle: async (ctx, flags) => {
1718
if (!flags["id"]) {
18-
throw new TypeError("required option '--id <id>' not specified");
19+
throw new InputValidationError("required option '--id <id>' not specified");
1920
}
2021

2122
const endpoints = await core.harness.listHarnessEndpoints(

src/handlers/harness/endpoint/update/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
33
import type { Core } from "../../../types.tsx";
44
import { coreOptsFromCtx } from "../../../utils.tsx";
55
import { JsonRendererKey } from "../../../../tui";
6+
import { InputValidationError } from "../../../../errors";
67

78
export const createUpdateEndpointHandler = (core: Core) =>
89
createHandler({
@@ -18,10 +19,10 @@ export const createUpdateEndpointHandler = (core: Core) =>
1819
// Required at runtime but declared optional so that a bare
1920
// `harness endpoint update` falls through to the TUI middleware instead.
2021
if (!flags["id"]) {
21-
throw new TypeError("required option '--id <id>' not specified");
22+
throw new InputValidationError("required option '--id <id>' not specified");
2223
}
2324
if (!flags["qualifier"]) {
24-
throw new TypeError("required option '--qualifier <qualifier>' not specified");
25+
throw new InputValidationError("required option '--qualifier <qualifier>' not specified");
2526
}
2627

2728
const response = await core.harness.updateHarnessEndpoint(

src/handlers/harness/exec/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Core } from "../../types.tsx";
55
import { coreOptsFromCtx } from "../../utils.tsx";
66
import { JsonKey } from "../../keys.tsx";
77
import { JsonRendererKey, renderTuiAt } from "../../../tui";
8+
import { InputValidationError } from "../../../errors";
89
import { applyExecEvent, finishExec, newExecItem } from "../invoke/transcript.tsx";
910

1011
export const createExecHarnessHandler = (core: Core, io: AppIO) =>
@@ -34,15 +35,15 @@ export const createExecHarnessHandler = (core: Core, io: AppIO) =>
3435
// Required at runtime but declared optional so that a bare `harness exec`
3536
// falls through to the TUI middleware instead.
3637
if (!flags["id"]) {
37-
throw new TypeError("required option '--id <id>' not specified");
38+
throw new InputValidationError("required option '--id <id>' not specified");
3839
}
3940
// Without a command, open the interactive exec screen at this harness —
4041
// resuming the given session and targeting the given qualifier when
4142
// passed. The one-shot CLI run below needs --command (and is the only
4243
// shape JSON mode supports).
4344
if (!flags["command"]) {
4445
if (ctx.require(JsonKey)) {
45-
throw new TypeError("required option '--command <command>' not specified");
46+
throw new InputValidationError("required option '--command <command>' not specified");
4647
}
4748
let path = `${ctx.require(PathKey)}/${flags["id"]}`;
4849
if (flags["session-id"]) path += `/${flags["session-id"]}`;

0 commit comments

Comments
 (0)