Skip to content

Transform graph testing & verification - #40

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

Transform graph testing & verification#40
galdre wants to merge 12 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 transform-test CLI commands for discovering test inputs and running transform or card tests.
    • Supports YAML test suites, expected outputs, ignored columns, repeatable SQL assertions, and structured results.
    • Added bundled guidance for creating test plans and validating transform outputs.
  • Bug Fixes

    • Updated validation error reporting for measure and segment creation scenarios.
  • Tests

    • Expanded coverage for assertions, suites, subgraph testing, and bundled skills.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds the transform-test CLI command group with fixture discovery, expected-output comparison, SQL assertions, YAML suites, subgraph execution, result rendering, and supporting skills. Updates bundled skill expectations and two E2E error-message assertions.

Changes

Transform-test command feature

Layer / File(s) Summary
Domain and runtime support
packages/cli/src/commands/parse-id.ts, packages/cli/src/commands/runtime.ts
Adds comma-separated positive ID parsing and passes raw command arguments through command context.
Assertion and suite configuration
packages/cli/src/commands/transform-test/assert.ts, packages/cli/src/commands/transform-test/suite.ts, packages/cli/src/commands/transform-test/*.test.ts
Adds SQL file/glob assertion resolution, YAML suite validation, severity defaults, and parser coverage.
Subgraph inputs, execution, and rendering
packages/cli/src/commands/transform-test/subgraph.ts, packages/cli/src/commands/transform-test/subgraph.test.ts
Adds target and fixture parsing, test-run parameter construction, execution orchestration, result rendering, and failure handling.
CLI command wiring and integration
packages/cli/src/commands/transform-test/*, tests/e2e/skills.e2e.test.ts
Registers transform-test inputs and transform-test run, composes suite and flag inputs, fetches test inputs, and updates bundled skill expectations.
Transform-test skills and planning guidance
packages/cli/skill-data/transform-test*/**, packages/cli/skill-data/data-workflow/references/building-clean-tables.md
Documents fixture-based execution, assertions, suites, planning workflows, checklists, coverage, and correctness references.

E2E error expectation updates

Layer / File(s) Summary
Schema error expectations
tests/e2e/measure.e2e.test.ts, tests/e2e/segment.e2e.test.ts
Updates create failure assertions to expect "Value does not match schema" on stderr.

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

Sequence Diagram(s)

sequenceDiagram
  participant CLI as transform-test run
  participant Config as Suite and assertion parsers
  participant Runner as Subgraph execution
  participant API as Metabase API
  CLI->>Config: Load suite and resolve assertions
  Config-->>CLI: Return normalized test arguments
  CLI->>Runner: Execute target test with fixtures
  Runner->>API: Submit subgraph test run
  API-->>Runner: Return test result
  Runner-->>CLI: Render result and exit on failure
Loading

Suggested reviewers: ranquild

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the new transform graph testing and verification functionality introduced by the pull request.
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 💡 1
📝 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 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.

@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

🤖 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 `@skill-data/transform-test-plan/references/checklist.md`:
- Around line 41-45: Update
skill-data/transform-test-plan/references/checklist.md lines 41-45 to require
full-expression analysis of filters, projections, default mappings, and declared
key relationships before classifying orphan behavior as dropped, NULL-preserved,
or default-row. Update skill-data/transform-test-plan/references/checks.md lines
146-153 to detect unmatched keys with an explicit key-based predicate rather
than checking whether a copied attribute is NULL, so legitimate nullable
attributes are not misclassified.

In `@skill-data/transform-test-plan/references/checks.md`:
- Around line 44-50: Update the A2 Conservation reconciliation guidance to
require an independent COUNT(*) comparison alongside additive-measure SUM
comparisons for every declared tie. Ensure the count subqueries use the same
input, output, and declared exclusions, and report mismatched row counts using
the existing IS DISTINCT FROM assertion pattern.

In `@skill-data/transform-test-plan/SKILL.md`:
- Line 84: Update the fenced directory-tree block in SKILL.md by adding the text
language tag to its opening fence, changing the untyped fence to a text fence
while leaving the directory listing unchanged.
🪄 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 Plus

Run ID: 7c12c6af-2a71-4854-bf09-26f191cf4cea

📥 Commits

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

📒 Files selected for processing (6)
  • skill-data/data-workflow/references/building-clean-tables.md
  • skill-data/transform-test-plan/SKILL.md
  • skill-data/transform-test-plan/references/checklist.md
  • skill-data/transform-test-plan/references/checks.md
  • skill-data/transform-test/SKILL.md
  • tests/e2e/skills.e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • skill-data/transform-test/SKILL.md

Comment on lines +41 to +45
6. **Orphan handling per join: drop / keep-with-NULLs / default row — and is
an orphan tolerated (warn) or forbidden (error)?** (C3, I5)
Detect: join type. INNER = drop, LEFT = keep-with-NULLs, COALESCE to a
sentinel = default row. Tolerated-vs-forbidden is the owner's call —
profiling says whether orphans exist today, not whether they're acceptable.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make orphan handling depend on explicit key semantics, not heuristics.

The checklist and catalog can derive incorrect tests when join type or nullable copied columns are treated as definitive. Analyze filters, projections, default mappings, and the declared key relationship.

  • skill-data/transform-test-plan/references/checklist.md#L41-L45: require full-expression analysis before classifying drop, NULL preservation, or default-row behavior.
  • skill-data/transform-test-plan/references/checks.md#L146-L153: replace t.<copied attr> IS NULL with an explicit unmatched-key predicate that cannot flag legitimate NULL attributes.
📍 Affects 2 files
  • skill-data/transform-test-plan/references/checklist.md#L41-L45 (this comment)
  • skill-data/transform-test-plan/references/checks.md#L146-L153
🤖 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 `@skill-data/transform-test-plan/references/checklist.md` around lines 41 - 45,
Update skill-data/transform-test-plan/references/checklist.md lines 41-45 to
require full-expression analysis of filters, projections, default mappings, and
declared key relationships before classifying orphan behavior as dropped,
NULL-preserved, or default-row. Update
skill-data/transform-test-plan/references/checks.md lines 146-153 to detect
unmatched keys with an explicit key-based predicate rather than checking whether
a copied attribute is NULL, so legitimate nullable attributes are not
misclassified.

Comment on lines +44 to +50
**A2 — Conservation reconciliation.** Row counts and additive-measure sums tie
from input to output; catches dropped rows and join double-counting at once.
- Applies: per declared tie (which input, which declared exclusions).
- Assert (error): independent scalar subqueries compared with
`IS DISTINCT FROM`, returning the mismatched pair:
`SELECT (SELECT SUM(t.m) FROM test_output t) AS output_sum,
(SELECT SUM(s.m) FROM <input> s) AS input_sum WHERE (…) IS DISTINCT FROM (…)`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include row-count reconciliation, not only SUM comparisons.

A2 claims that row counts and additive-measure sums tie, but the specified assertion only compares SUM(...). Dropping zero- or NULL-valued rows can therefore pass the check. Add an independent COUNT(*) comparison for each declared tie, honoring exclusions.

🤖 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 `@skill-data/transform-test-plan/references/checks.md` around lines 44 - 50,
Update the A2 Conservation reconciliation guidance to require an independent
COUNT(*) comparison alongside additive-measure SUM comparisons for every
declared tie. Ensure the count subqueries use the same input, output, and
declared exclusions, and report mismatched row counts using the existing IS
DISTINCT FROM assertion pattern.

cell is the null policy's only enforcement.
7. **Write the suites**, one directory per target:

```

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

Add a language tag to the fenced directory tree.

The untyped fence triggers markdownlint MD040. Use text for this directory listing.

Proposed fix
-   ```
+   ```text
📝 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
```
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 84-84: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@skill-data/transform-test-plan/SKILL.md` at line 84, Update the fenced
directory-tree block in SKILL.md by adding the text language tag to its opening
fence, changing the untyped fence to a text fence while leaving the directory
listing unchanged.

Source: Linters/SAST tools

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/cli/skill-data/transform-test-plan/references/checks.md (1)

47-63: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add an explicit row-count reconciliation to A2.

A2 promises to tie row counts and additive sums, but the provided SQL shape compares only SUM(...). Dropped zero-valued rows or offsetting changes can therefore pass. Define a separate COUNT(*) scalar comparison, respecting declared exclusions, alongside the sum check.

🤖 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 `@packages/cli/skill-data/transform-test-plan/references/checks.md` around
lines 47 - 63, Update the A2 Conservation reconciliation guidance to require a
separate scalar COUNT(*) comparison alongside the existing SUM(...) comparison,
using the same declared exclusions and IS DISTINCT FROM mismatch pattern. Ensure
the row-count check compares input and output independently rather than through
a fact-to-fact join, while preserving the existing additive-sum reconciliation
and authoritative aggregate guidance.
packages/cli/src/commands/transform-test/assert.test.ts (1)

31-33: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the expected error text for failure paths.

These tests only check ConfigError. Assert the stable message (or exact stable message slice for filesystem errors) too, so CLI diagnostics remain covered.

As per coding guidelines, tests must “assert both error type and exact message slice.”

Also applies to: 57-59, 101-110

🤖 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 `@packages/cli/src/commands/transform-test/assert.test.ts` around lines 31 -
33, Update the failure-path tests for classifyAssertToken, including the cases
around the bare name and referenced lines 57-59 and 101-110, to assert both
ConfigError and the expected stable error-message text or exact stable message
slice. Preserve the existing error-type assertions while adding precise
diagnostic coverage for each filesystem-related and validation failure.

Source: Coding guidelines

packages/cli/src/commands/transform-test/assert.ts (1)

82-87: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exclude directory entries from assertion globs.

readdir() includes non-directory .sql entries, so a directory named something.sql matches the current name-only filter and makes the glob fail when readFile is called. Map entries with file/stat checks and keep only regular files; add a regression covering this edge case.

🤖 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 `@packages/cli/src/commands/transform-test/assert.ts` around lines 82 - 87,
Update the assertion-glob matching logic around matches to inspect each readdir
entry with the appropriate file/stat check, retaining only regular files before
sorting and mapping paths. Exclude directories even when their names satisfy the
prefix/suffix filter, and add a regression test covering a directory named with
the matching .sql pattern.
🧹 Nitpick comments (1)
packages/cli/src/commands/runtime.ts (1)

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

Keep these comments focused on non-obvious rationale.

Both comments describe implementation details and internal paths. Replace them with concise WHY-only comments.

  • packages/cli/src/commands/runtime.ts#L46-L48: explain only that raw arguments preserve repeated flags.
  • packages/cli/src/commands/runtime.ts#L97-L99: explain only that lazy loading avoids loading the client graph on paths that do not need it.

As per coding guidelines, comments must “only” explain a non-obvious WHY and must not describe WHAT or paths.

🤖 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 `@packages/cli/src/commands/runtime.ts` around lines 46 - 48, In
packages/cli/src/commands/runtime.ts lines 46-48, replace the comment above
rawArgs with a concise WHY-only explanation that raw arguments preserve repeated
flags, without mentioning implementation details or internal paths. In
packages/cli/src/commands/runtime.ts lines 97-99, replace the lazy-loading
comment with a concise WHY-only explanation that it avoids loading the client
graph on paths that do not need it; do not describe what is loaded or reference
paths.

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 `@packages/cli/skill-data/transform-test/SKILL.md`:
- Line 75: Update the summary documentation in the transform-test SKILL.md to
reflect that runSubgraph uses targetLabels(args.targetType) for the summary
prefix. Replace the fixed “Transform <id>” wording with a target-neutral
placeholder, or explicitly document both “Transform” and “Card” labels while
preserving the existing assertion breakdown and table details.
- Line 50: Remove the WHAT/path comments from the Markdown examples in SKILL.md,
including the header/rows annotation and the referenced sections around the
other affected examples. Preserve the examples unchanged, and move any necessary
context into surrounding prose while retaining only comments that explain
non-obvious reasons.

In `@packages/cli/src/commands/transform-test/subgraph.ts`:
- Around line 99-115: Thread the global interrupt signal from runSubgraph
through buildRunParams into every readFixtureFile call, including input and
expected fixtures. Extend readFixtureFile to accept the signal and pass it to
its asynchronous filesystem read operation, preserving existing fixture paths
and error labels while allowing Ctrl-C to cancel pending I/O.

---

Outside diff comments:
In `@packages/cli/skill-data/transform-test-plan/references/checks.md`:
- Around line 47-63: Update the A2 Conservation reconciliation guidance to
require a separate scalar COUNT(*) comparison alongside the existing SUM(...)
comparison, using the same declared exclusions and IS DISTINCT FROM mismatch
pattern. Ensure the row-count check compares input and output independently
rather than through a fact-to-fact join, while preserving the existing
additive-sum reconciliation and authoritative aggregate guidance.

In `@packages/cli/src/commands/transform-test/assert.test.ts`:
- Around line 31-33: Update the failure-path tests for classifyAssertToken,
including the cases around the bare name and referenced lines 57-59 and 101-110,
to assert both ConfigError and the expected stable error-message text or exact
stable message slice. Preserve the existing error-type assertions while adding
precise diagnostic coverage for each filesystem-related and validation failure.

In `@packages/cli/src/commands/transform-test/assert.ts`:
- Around line 82-87: Update the assertion-glob matching logic around matches to
inspect each readdir entry with the appropriate file/stat check, retaining only
regular files before sorting and mapping paths. Exclude directories even when
their names satisfy the prefix/suffix filter, and add a regression test covering
a directory named with the matching .sql pattern.

---

Nitpick comments:
In `@packages/cli/src/commands/runtime.ts`:
- Around line 46-48: In packages/cli/src/commands/runtime.ts lines 46-48,
replace the comment above rawArgs with a concise WHY-only explanation that raw
arguments preserve repeated flags, without mentioning implementation details or
internal paths. In packages/cli/src/commands/runtime.ts lines 97-99, replace the
lazy-loading comment with a concise WHY-only explanation that it avoids loading
the client graph on paths that do not need it; do not describe what is loaded or
reference paths.
🪄 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 Plus

Run ID: d7ad83ec-c741-4cd9-9f62-fb835de49dbb

📥 Commits

Reviewing files that changed from the base of the PR and between fd8eb71 and 840a35d.

📒 Files selected for processing (16)
  • packages/cli/skill-data/data-workflow/references/building-clean-tables.md
  • packages/cli/skill-data/transform-test-plan/SKILL.md
  • packages/cli/skill-data/transform-test-plan/references/checklist.md
  • packages/cli/skill-data/transform-test-plan/references/checks.md
  • packages/cli/skill-data/transform-test/SKILL.md
  • packages/cli/src/commands/parse-id.ts
  • packages/cli/src/commands/runtime.ts
  • packages/cli/src/commands/transform-test/assert.test.ts
  • packages/cli/src/commands/transform-test/assert.ts
  • packages/cli/src/commands/transform-test/index.ts
  • packages/cli/src/commands/transform-test/inputs.ts
  • packages/cli/src/commands/transform-test/run.ts
  • packages/cli/src/commands/transform-test/subgraph.test.ts
  • packages/cli/src/commands/transform-test/subgraph.ts
  • packages/cli/src/commands/transform-test/suite.test.ts
  • packages/cli/src/commands/transform-test/suite.ts

3,2024-01-03,3
CSV

# The expected output: header = the columns your transform SELECTs, rows = what it should produce.

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

Remove WHAT/path comments from Markdown examples.

These comments annotate values, paths, or behavior rather than explaining a non-obvious reason. Move that information into surrounding prose or retain only WHY comments.

As per coding guidelines, **/*.{ts,tsx,md} comments must explain only non-obvious WHY and must not describe WHAT or paths.

Also applies to: 116-133, 151-152

🤖 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 `@packages/cli/skill-data/transform-test/SKILL.md` at line 50, Remove the
WHAT/path comments from the Markdown examples in SKILL.md, including the
header/rows annotation and the referenced sections around the other affected
examples. Preserve the examples unchanged, and move any necessary context into
surrounding prose while retaining only comments that explain non-obvious
reasons.

Source: Coding guidelines


Reading the result:

- The plain (text) summary says `Transform <id> test run passed.` or `Transform <id> test run FAILED …`, followed — whenever assertions ran — by a one-line breakdown (`N assertions — A passed, B FAILED, C warn …`) and a per-assertion table (name / status / failing rows).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the target-specific summary label.

runSubgraph formats the summary through targetLabels(args.targetType), so card runs do not say Transform <id>. Use a target-neutral placeholder or document both Transform and Card.

🤖 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 `@packages/cli/skill-data/transform-test/SKILL.md` at line 75, Update the
summary documentation in the transform-test SKILL.md to reflect that runSubgraph
uses targetLabels(args.targetType) for the summary prefix. Replace the fixed
“Transform <id>” wording with a target-neutral placeholder, or explicitly
document both “Transform” and “Card” labels while preserving the existing
assertion breakdown and table details.

Comment on lines +99 to +115
export async function buildRunParams(args: SubgraphRunArgs): Promise<TestRunParams> {
const inputs = await Promise.all(
args.inputs.map(async ({ tableId, path }) => ({
table_id: tableId,
file: await readFixtureFile(path, `--input ${tableId}`),
})),
);
const params: TestRunParams = {
sources: args.sources,
inputs,
ignore_columns: args.ignoreColumns,
assertions: args.assertions,
};
if (args.expected !== undefined) {
params.expected = await readFixtureFile(args.expected, "--expected");
}
return params;

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -i 'subgraph\.ts$|flags\.ts$|runtime\.ts$' packages/cli/src | sed -n '1,80p'

echo
echo "== subgraph outline =="
ast-grep outline packages/cli/src/commands/transform-test/subgraph.ts --view compact || true

echo
echo "== subgraph relevant lines =="
cat -n packages/cli/src/commands/transform-test/subgraph.ts | sed -n '1,180p'

echo
echo "== readFixtureFile definitions/usages =="
rg -n "readFixtureFile|buildRunParams|TestRunParams|interruptSignal" packages/cli/src -S

Repository: metabase/metabase-cli

Length of output: 11309


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runtime/upload.ts =="
cat -n packages/cli/src/runtime/upload.ts | sed -n '1,160p'

echo
echo "== runtime/interrupt.ts =="
cat -n packages/cli/src/runtime/interrupt.ts | sed -n '1,80p'

echo
echo "== transform-test subprocess command flow =="
cat -n packages/cli/src/commands/transform-test/subgraph.ts | sed -n '180,280p'

echo
echo "== CommonContext and create command signatures =="
rg -n "interface CommonContext|export interface CommonContext|type CommonContext|buildRunParams|run\\(ctx" packages/cli/src/commands/transform-test/subgraph.ts packages/cli/src/commands/runtime.ts packages/cli/src -S -g '*.ts' | sed -n '1,160p'

Repository: metabase/metabase-cli

Length of output: 4693


Accept and pass interruptSignal into fixture reads.

readFixtureFile currently only uses readFile(path), so Ctrl-C during buildRunParams will not cancel pending fixture I/O before the API request starts. Thread an explicit signal from runSubgraph through buildRunParams to readFixtureFile, and pass it into the asynchronous filesystem operation so the command can exit promptly via the global interrupt signal.

🤖 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 `@packages/cli/src/commands/transform-test/subgraph.ts` around lines 99 - 115,
Thread the global interrupt signal from runSubgraph through buildRunParams into
every readFixtureFile call, including input and expected fixtures. Extend
readFixtureFile to accept the signal and pass it to its asynchronous filesystem
read operation, preserving existing fixture paths and error labels while
allowing Ctrl-C to cancel pending I/O.

Source: Coding guidelines

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant