Skip to content

Transform graph testing & verification#40

Open
galdre wants to merge 10 commits into
mainfrom
galdre/test-transforms/v1
Open

Transform graph testing & verification#40
galdre wants to merge 10 commits into
mainfrom
galdre/test-transforms/v1

Conversation

@galdre

@galdre galdre commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added a new transform-test command with inputs and run subcommands for validating transform/card behavior.
    • Supports suite-based YAML runs, file and glob SQL assertions (with error/warn severity), expected-output diffs, and --ignore-columns.
    • Improved repeated flag parsing and comma-separated ID handling for smoother CLI usage.
    • transform-test now appears in the bundled visible skills list and command help.
  • Bug Fixes

    • More resilient handling of server error payloads when error details aren’t simple strings.
  • Documentation

    • Added transform-test skill/command documentation, including supported workflow and limitations.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e4a9d67-8f1b-496b-8297-d0ebd50f849b

📥 Commits

Reviewing files that changed from the base of the PR and between c9976fc and 218731b.

📒 Files selected for processing (4)
  • src/runtime/command-help.test.ts
  • tests/e2e/measure.e2e.test.ts
  • tests/e2e/segment.e2e.test.ts
  • tests/e2e/skills.e2e.test.ts

📝 Walkthrough

Walkthrough

Adds a new mb transform-test command with inputs and run subcommands, YAML suite and SQL assertion parsing, subgraph execution against the Metabase API, and updated command/domain/runtime support.

Changes

Transform-test command feature

Layer / File(s) Summary
Domain schemas for test run inputs/results
src/domain/transform-test-run.ts, src/domain/transform-test-run.test.ts
Adds TestRunInput, TestRunResult, AssertionResult Zod schemas plus compact variants and resource views for table rendering.
Runtime helpers
src/commands/parse-id.ts, src/commands/runtime.ts, src/runtime/citty.ts, src/runtime/citty.test.ts, src/runtime/upload.ts, src/core/http/errors.ts
Adds parseIdList, exposes rawArgs on command context, adds collectRepeatedFlag for repeated CLI flags, adds readFilePart for multipart uploads, and relaxes ErrorEnvelope.error typing.
Assertion token parsing
src/commands/transform-test/assert.ts, src/commands/transform-test/assert.test.ts
Classifies --assert tokens as file/glob, expands globs, reads SQL files, and resolves them into AssertionDef objects.
YAML suite parsing
src/commands/transform-test/suite.ts, src/commands/transform-test/suite.test.ts
Parses suite YAML into normalized SuiteArgs, resolving inline/file-based assertions with severity handling.
Subgraph fetch, form build, run and rendering
src/commands/transform-test/subgraph.ts, src/commands/transform-test/subgraph.test.ts
Implements target-type/input parsing, fetches subgraph inputs, builds multipart form data, submits runs, computes pass/fail and summaries, and renders results.
CLI command wiring
src/commands/transform-test/index.ts, inputs.ts, run.ts, src/main.ts, skill-data/transform-test/SKILL.md
Registers transform-test command with inputs/run subcommands in the main CLI and documents usage in SKILL.md.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as run.ts command
  participant Suite as parseSuite/assert
  participant Subgraph as subgraph.ts
  participant Server as Metabase API

  CLI->>Suite: loadSuite / resolveAssertions
  CLI->>Subgraph: runSubgraph(client, args, ctx)
  Subgraph->>Subgraph: buildSubgraphForm(args)
  Subgraph->>Server: POST subgraph run endpoint (FormData)
  Server-->>Subgraph: TestRunResult
  Subgraph->>Subgraph: shouldFail(result)
  Subgraph->>CLI: renderRunResult(result, ctx)
Loading

Suggested reviewers: ranquild

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the new transform-test graph testing and verification functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch galdre/test-transforms/v1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@galdre galdre marked this pull request as ready for review July 8, 2026 18:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/domain/transform-test-run.test.ts (1)

13-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use full deep equality assertions for schema parse tests.

Tests 1, 2, and 4 use .success checks and piecemeal property assertions instead of toEqual. Per coding guidelines, assertions must be full deep equality where possible.

♻️ Proposed refactor
   it("accepts a schema-qualified input table", () => {
-    expect(TestRunInput.safeParse(base).success).toBe(true);
+    expect(TestRunInput.parse(base)).toEqual(base);
   });

   it("accepts a null schema (engines without schemas)", () => {
-    const parsed = TestRunInput.safeParse({ ...base, schema: null });
-    expect(parsed.success).toBe(true);
-    expect(parsed.data?.schema).toBeNull();
+    expect(TestRunInput.parse({ ...base, schema: null })).toEqual({ ...base, schema: null });
   });

   it("rejects a missing schema key", () => {
     const { schema: _schema, ...rest } = base;
     expect(TestRunInput.safeParse(rest).success).toBe(false);
   });

   it("compact pick preserves a null schema", () => {
-    expect(TestRunInputCompact.parse({ ...base, schema: null }).schema).toBeNull();
+    expect(TestRunInputCompact.parse({ ...base, schema: null })).toEqual({
+      table_id: 229,
+      schema: null,
+      name: "orders",
+      columns: ["id", "total"],
+    });
   });

Additionally, consider adding tests for AssertionResult, TestRunResult, and their compact variants to match the coverage depth of TestRunInput.

As per coding guidelines: "Assertions in unit tests must be full deep equality where possible (toEqual(full object/array)), not piecemeal property checks."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/domain/transform-test-run.test.ts` around lines 13 - 30, The schema parse
tests currently rely on `.success` and single-property checks instead of full
object comparisons, so update the assertions in the `TestRunInput` and
`TestRunInputCompact` cases to use deep equality with the parsed object where
applicable. In the `TestRunInput.safeParse` and `TestRunInputCompact.parse`
tests, assert the entire parsed result/object rather than checking `success` or
only `schema`. Also add similar full-equality coverage for `AssertionResult`,
`TestRunResult`, and their compact variants to match the same validation depth.

Source: Coding guidelines

src/commands/transform-test/assert.ts (2)

15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Name the discriminated union members per coding guidelines.

The guideline requires inline object types in unions to be named with interface or type. Extract each member:

♻️ Proposed refactor
-export type AssertToken = { kind: "file"; path: string } | { kind: "glob"; pattern: string };
+export interface AssertFileToken {
+  kind: "file";
+  path: string;
+}
+
+export interface AssertGlobToken {
+  kind: "glob";
+  pattern: string;
+}
+
+export type AssertToken = AssertFileToken | AssertGlobToken;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/transform-test/assert.ts` at line 15, The AssertToken union in
assert.ts uses inline object members, which violates the naming guideline.
Extract each union member into separately named interfaces or type aliases, then
update AssertToken to reference those named members while keeping the same
discriminant field (`kind`) and existing properties (`path`, `pattern`).

Source: Coding guidelines


14-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove WHAT comments per coding guidelines.

These comments restate what the code already expresses clearly. The guideline says to avoid comments unless the WHY is non-obvious and to never write WHAT comments.

♻️ Proposed refactor
-// An --assert value is a `.sql` file or a glob of them; inline SQL is not supported.
 export type AssertToken = AssertFileToken | AssertGlobToken;
 
-// basename with `*` ⇒ glob; plain `.sql` ⇒ file; anything else rejected.
 export function classifyAssertToken(token: string): AssertToken {

And at line 94:

-// Each `.sql` file → one assertion, named by basename without extension; severity defaults to error.
 export async function resolveAssertions(tokens: AssertToken[]): Promise<AssertionDef[]> {

Also applies to: 17-17, 94-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/transform-test/assert.ts` at line 14, Remove the WHAT-style
comments in assert.ts that simply restate the code, including the top-level note
about --assert inputs and the similar comment near the assert handling logic;
keep only comments that explain non-obvious WHY, and use the surrounding symbols
in this file (such as the assert parsing/validation code and related helpers) to
preserve readability without redundant commentary.

Source: Coding guidelines

src/commands/transform-test/run.ts (1)

143-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider exporting and reusing DEFAULT_TARGET_TYPE instead of the inline "transform" literal.

Line 145 uses ?? "transform" as a type-narrowing fallback that never triggers at runtime (when idGiven is true, targetType is always defined from line 143). The literal "transform" duplicates DEFAULT_TARGET_TYPE from subgraph.ts (currently not exported). Exporting and reusing it would eliminate the repeated magic literal.

♻️ Optional refactor

In subgraph.ts:

-const DEFAULT_TARGET_TYPE: TargetType = "transform";
+export const DEFAULT_TARGET_TYPE: TargetType = "transform";

In run.ts:

 import {
+  DEFAULT_TARGET_TYPE,
   parseColumnList,
   parseInputPairs,
   parseTargetType,
   runSubgraph,
   type SubgraphRunArgs,
   targetLabels,
   targetTypeFlag,
 } from "./subgraph";

Then:

-      ? parseId(args.id, targetLabels(targetType ?? "transform").positionalLabel)
+      ? parseId(args.id, targetLabels(targetType ?? DEFAULT_TARGET_TYPE).positionalLabel)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/transform-test/run.ts` around lines 143 - 146, The `run.ts`
target parsing uses an inline "transform" fallback in `targetLabels(targetType
?? "transform")`, but that fallback is unreachable and duplicates the default
from `subgraph.ts`. Export `DEFAULT_TARGET_TYPE` from `subgraph.ts` and reuse it
in `run.ts` for both `parseTargetType` defaults and `targetLabels`, replacing
the magic string and keeping `parseId`/`targetLabels` aligned with the shared
constant.

Source: Coding guidelines

src/commands/transform-test/index.ts (1)

1-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use defineCommandGroup for this dispatcher. src/commands/transform-test/index.ts should follow the other parent command indexes so the shared augment metadata — including explicit capabilities: null — is attached automatically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/transform-test/index.ts` around lines 1 - 13, The transform-test
dispatcher is using the wrong command helper, so the parent command metadata
augmentation is not being applied. Update the default export in transform-test’s
index module to use defineCommandGroup instead of defineCommand, matching the
other parent command indexes so shared augment metadata such as explicit
capabilities: null is attached automatically.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/transform-test/suite.test.ts`:
- Line 101: The tests in parseSuite should assert the exact ConfigError message
instead of only matching the error type. Update the failing rejection checks in
suite.test.ts around parseSuite so they verify the known stable error string for
each case, using the parseSuite helper and ConfigError as the reference points.

In `@src/core/http/errors.ts`:
- Around line 41-43: parseEnvelopeMessage is still treating envelope.error as an
opaque value, so object-shaped errors like { message: "..." } fall through to
later fallbacks. Update the error extraction logic in the envelope parsing flow
to detect when envelope.error is an object and prefer its message field before
checking error-message or generic fallback text, while keeping the existing
z.unknown().optional() schema intact.

---

Nitpick comments:
In `@src/commands/transform-test/assert.ts`:
- Line 15: The AssertToken union in assert.ts uses inline object members, which
violates the naming guideline. Extract each union member into separately named
interfaces or type aliases, then update AssertToken to reference those named
members while keeping the same discriminant field (`kind`) and existing
properties (`path`, `pattern`).
- Line 14: Remove the WHAT-style comments in assert.ts that simply restate the
code, including the top-level note about --assert inputs and the similar comment
near the assert handling logic; keep only comments that explain non-obvious WHY,
and use the surrounding symbols in this file (such as the assert
parsing/validation code and related helpers) to preserve readability without
redundant commentary.

In `@src/commands/transform-test/index.ts`:
- Around line 1-13: The transform-test dispatcher is using the wrong command
helper, so the parent command metadata augmentation is not being applied. Update
the default export in transform-test’s index module to use defineCommandGroup
instead of defineCommand, matching the other parent command indexes so shared
augment metadata such as explicit capabilities: null is attached automatically.

In `@src/commands/transform-test/run.ts`:
- Around line 143-146: The `run.ts` target parsing uses an inline "transform"
fallback in `targetLabels(targetType ?? "transform")`, but that fallback is
unreachable and duplicates the default from `subgraph.ts`. Export
`DEFAULT_TARGET_TYPE` from `subgraph.ts` and reuse it in `run.ts` for both
`parseTargetType` defaults and `targetLabels`, replacing the magic string and
keeping `parseId`/`targetLabels` aligned with the shared constant.

In `@src/domain/transform-test-run.test.ts`:
- Around line 13-30: The schema parse tests currently rely on `.success` and
single-property checks instead of full object comparisons, so update the
assertions in the `TestRunInput` and `TestRunInputCompact` cases to use deep
equality with the parsed object where applicable. In the
`TestRunInput.safeParse` and `TestRunInputCompact.parse` tests, assert the
entire parsed result/object rather than checking `success` or only `schema`.
Also add similar full-equality coverage for `AssertionResult`, `TestRunResult`,
and their compact variants to match the same validation depth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 721583b8-8d38-4c56-85f6-2e53e93d5f87

📥 Commits

Reviewing files that changed from the base of the PR and between f31632d and c9976fc.

📒 Files selected for processing (19)
  • skill-data/transform-test/SKILL.md
  • src/commands/parse-id.ts
  • src/commands/runtime.ts
  • src/commands/transform-test/assert.test.ts
  • src/commands/transform-test/assert.ts
  • src/commands/transform-test/index.ts
  • src/commands/transform-test/inputs.ts
  • src/commands/transform-test/run.ts
  • src/commands/transform-test/subgraph.test.ts
  • src/commands/transform-test/subgraph.ts
  • src/commands/transform-test/suite.test.ts
  • src/commands/transform-test/suite.ts
  • src/core/http/errors.ts
  • src/domain/transform-test-run.test.ts
  • src/domain/transform-test-run.ts
  • src/main.ts
  • src/runtime/citty.test.ts
  • src/runtime/citty.ts
  • src/runtime/upload.ts

Comment on lines +87 to +99
it("expands a glob to one assertion per matching .sql file, sorted by name", async () => {
const out = await resolveAssertions([{ kind: "glob", pattern: join(dir, "*.sql") }]);
expect(out.map((a) => a.name)).toEqual(["has_rows", "no_negatives"]);
expect(out.every((a) => a.severity === "error")).toBe(true);
});

it("resolves multiple file tokens preserving order, each named by basename", async () => {
const out = await resolveAssertions([
{ kind: "file", path: join(dir, "has_rows.sql") },
{ kind: "file", path: join(dir, "no_negatives.sql") },
]);
expect(out.map((a) => a.name)).toEqual(["has_rows", "no_negatives"]);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use full deep equality assertions per coding guidelines.

The glob expansion and multiple-file tests check only a.name and a.severity piecemeal. The guideline requires full deep equality with toEqual where possible. The SQL content is known from the temp files, so full assertions are feasible.

💚 Proposed fix for glob expansion test
   it("expands a glob to one assertion per matching .sql file, sorted by name", async () => {
     const out = await resolveAssertions([{ kind: "glob", pattern: join(dir, "*.sql") }]);
-    expect(out.map((a) => a.name)).toEqual(["has_rows", "no_negatives"]);
-    expect(out.every((a) => a.severity === "error")).toBe(true);
+    expect(out).toEqual([
+      {
+        name: "has_rows",
+        sql: "SELECT * FROM test_output WHERE 1=0",
+        severity: "error",
+      },
+      {
+        name: "no_negatives",
+        sql: "SELECT * FROM test_output WHERE revenue < 0",
+        severity: "error",
+      },
+    ]);
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("expands a glob to one assertion per matching .sql file, sorted by name", async () => {
const out = await resolveAssertions([{ kind: "glob", pattern: join(dir, "*.sql") }]);
expect(out.map((a) => a.name)).toEqual(["has_rows", "no_negatives"]);
expect(out.every((a) => a.severity === "error")).toBe(true);
});
it("resolves multiple file tokens preserving order, each named by basename", async () => {
const out = await resolveAssertions([
{ kind: "file", path: join(dir, "has_rows.sql") },
{ kind: "file", path: join(dir, "no_negatives.sql") },
]);
expect(out.map((a) => a.name)).toEqual(["has_rows", "no_negatives"]);
});
it("expands a glob to one assertion per matching .sql file, sorted by name", async () => {
const out = await resolveAssertions([{ kind: "glob", pattern: join(dir, "*.sql") }]);
expect(out).toEqual([
{
name: "has_rows",
sql: "SELECT * FROM test_output WHERE 1=0",
severity: "error",
},
{
name: "no_negatives",
sql: "SELECT * FROM test_output WHERE revenue < 0",
severity: "error",
},
]);
});
it("resolves multiple file tokens preserving order, each named by basename", async () => {
const out = await resolveAssertions([
{ kind: "file", path: join(dir, "has_rows.sql") },
{ kind: "file", path: join(dir, "no_negatives.sql") },
]);
expect(out.map((a) => a.name)).toEqual(["has_rows", "no_negatives"]);
});

Source: Coding guidelines

assertions:
- name: bad
`;
await expect(parseSuite(yaml, "suite.yaml")).rejects.toThrow(ConfigError);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert exact error messages per coding guidelines.

The guideline requires exact error strings when checking failures. Both rejection cases produce a known, stable ConfigError message — assert it.

💚 Proposed fix
   it("rejects an assertion with neither sql nor file", async () => {
     ...
-    await expect(parseSuite(yaml, "suite.yaml")).rejects.toThrow(ConfigError);
+    await expect(parseSuite(yaml, "suite.yaml")).rejects.toThrow(
+      "Suite assertion 'bad' must set exactly one of 'sql' or 'file'.",
+    );
   });

   it("rejects an assertion with both sql and file", async () => {
     ...
-    await expect(parseSuite(yaml, "suite.yaml")).rejects.toThrow(ConfigError);
+    await expect(parseSuite(yaml, "suite.yaml")).rejects.toThrow(
+      "Suite assertion 'bad' must set exactly one of 'sql' or 'file'.",
+    );
   });

Also applies to: 114-114

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/transform-test/suite.test.ts` at line 101, The tests in
parseSuite should assert the exact ConfigError message instead of only matching
the error type. Update the failing rejection checks in suite.test.ts around
parseSuite so they verify the known stable error string for each case, using the
parseSuite helper and ConfigError as the reference points.

Source: Coding guidelines

Comment thread src/core/http/errors.ts
Comment on lines +41 to +43
// `error` may be a structured object, not a string; accept any shape so a non-string
// doesn't break parsing the whole envelope.
error: z.unknown().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for test cases or API documentation showing structured error shapes
rg -n "error.*message" --type=ts src/core/http/ -g '!*.test.ts' -C3
rg -n "envelope\.error" --type=ts src/core/http/ -C3

Repository: metabase/metabase-cli

Length of output: 2015


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,280p' src/core/http/errors.ts
printf '\n---\n'
rg -n "parseEnvelopeMessage|formatErrorTree|error-message|specific-errors|via|errors:" src -C 2
printf '\n---\n'
rg -n "structured error|error-message|specific-errors|parseEnvelopeMessage" src -g '!*.snap' -C 3

Repository: metabase/metabase-cli

Length of output: 34950


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for tests or fixtures that exercise error envelopes and structured error payloads.
fd -a -t f "errors" src test . | sed -n '1,120p'
printf '\n---\n'
rg -n "parseEnvelopeMessage|formatErrorTree|error-message|specific-errors|envelope\.error|via:" src test -g '!*.snap' -C 2

Repository: metabase/metabase-cli

Length of output: 7026


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the envelope schema and message extraction logic.
sed -n '1,260p' src/core/http/errors.ts

Repository: metabase/metabase-cli

Length of output: 7889


Unwrap object-shaped envelope.error values
parseEnvelopeMessage still drops structured envelope.error payloads like { message: "..." }. Extract error.message here before the fallback chain so those messages surface instead of falling through to error-message or generic text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/http/errors.ts` around lines 41 - 43, parseEnvelopeMessage is still
treating envelope.error as an opaque value, so object-shaped errors like {
message: "..." } fall through to later fallbacks. Update the error extraction
logic in the envelope parsing flow to detect when envelope.error is an object
and prefer its message field before checking error-message or generic fallback
text, while keeping the existing z.unknown().optional() schema intact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant