Skip to content

Commit 799d3dd

Browse files
committed
TML-2683: add sql-orm-client whole-shape-assertions rule + index + golden-rule pointer
1 parent 1506f21 commit 799d3dd

3 files changed

Lines changed: 79 additions & 0 deletions

File tree

.agents/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Rules below are listed by bare filename; the canonical file is `.agents/rules/<n
5757
- `vitest-expect-typeof.mdc` — Type test patterns
5858
- `test-mocking-patterns.mdc` — Test-only assertions and mocking patterns
5959
- `prefer-object-matcher.mdc` — Prefer object matchers over multiple individual expect().toBe() calls
60+
- `sql-orm-client-whole-shape-assertions.mdc` — In sql-orm-client tests, assert the whole result shape (`toEqual`/snapshot) with explicit `select`
6061
- `prefer-to-throw.mdc` — Use `expect().toThrow()` instead of manual try/catch blocks
6162
- `no-tautological-tests.mdc` — Avoid tests that only restate fixture input structure
6263
- `use-ast-factories.mdc` — Use factory functions for creating AST nodes instead of manual object creation
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
description: Prefer whole-result-shape assertions with explicit select projections in sql-orm-client tests
3+
globs: ["packages/3-extensions/sql-orm-client/**/*.test.ts", "test/integration/test/sql-orm-client/**/*.test.ts"]
4+
alwaysApply: false
5+
---
6+
7+
# Assert the whole result shape, with explicit projections
8+
9+
In `sql-orm-client` tests (unit and integration), assert the **entire** result with `toEqual`
10+
(or an inline/file snapshot), and pin the projected columns with explicit `.select(...)` (varargs:
11+
`.select('id', 'name')`) on the root collection **and** on every included relation. Order
12+
deterministically with
13+
`.orderBy((c) => c.<baseColumn>.asc())` so `toEqual` on arrays is reliable.
14+
15+
Avoid partial matchers (`toMatchObject`, `toHaveProperty` / `not.toHaveProperty`, lone
16+
single-field `toBe`/`toEqual`) as the primary assertion for a query result, and avoid `toEqual`
17+
on a full model row with no `select`.
18+
19+
## Why
20+
21+
Two failure modes this prevents:
22+
23+
1. **Partial matchers pass silently on wrong shapes.** `toMatchObject({ id: 1, role: 'admin' })`
24+
succeeds even if the row carries an extra field it shouldn't, a sibling-variant field that
25+
should have been dropped, or a misspelled key elsewhere. The result's *shape* is the contract;
26+
assert all of it.
27+
2. **`toEqual` without `select` couples every test to the full model field set.** Adding one field
28+
to a model then breaks every test that asserted a full row of it — tests far from the change.
29+
An explicit `.select(...)` makes the asserted columns intentional, so unrelated field
30+
additions don't ripple into unrelated tests, while `toEqual` still catches any wrong/missing
31+
value *within* the selected shape.
32+
33+
Together: `select` + `toEqual` is both **complete** (catches extra/missing/wrong fields in the
34+
projected shape) and **stable** (immune to unrelated model growth).
35+
36+
## Good
37+
38+
```ts
39+
const rows = await db.orm.Account
40+
.select('id', 'name')
41+
.orderBy((a) => a.id.asc())
42+
.include('members', (m) => m.select('id', 'kind', 'role', 'plan').orderBy((u) => u.id.asc()))
43+
.all();
44+
45+
expect(rows).toEqual([
46+
{ id: 1, name: 'Acme', members: [
47+
{ id: 1, kind: 'admin', role: 'superadmin' }, // variant fields surface per the row's variant;
48+
{ id: 2, kind: 'regular', plan: 'free' }, // pinning them locks the variant shape
49+
] },
50+
{ id: 2, name: 'Empty', members: [] },
51+
]);
52+
```
53+
54+
## Avoid
55+
56+
```ts
57+
const member = members.find((m) => m.id === 1)!;
58+
expect(member).toMatchObject({ id: 1, role: 'admin' }); // passes even if `member` has stray fields
59+
expect(member).not.toHaveProperty('plan'); // enumerating absences ≠ asserting the shape
60+
```
61+
62+
## Notes
63+
64+
- **Polymorphic includes:** variant-specific fields surface according to each row's variant. Pin
65+
them with `select` + `toEqual` so the per-variant shape (e.g. admin rows carry `role`, regular
66+
rows carry `plan`) is asserted, not assumed.
67+
- **Determinism:** order by a **base-table** column (typically `id`). Don't order by a variant
68+
table's column on a variant-narrowed collection unless that path is the thing under test.
69+
- **Snapshots** (`toMatchInlineSnapshot`) are an acceptable alternative to a hand-written `toEqual`
70+
for large shapes, but still pair them with explicit `select` so the snapshot is stable.
71+
- This is about *result-shape* assertions. Asserting a single scalar (a count, a thrown error
72+
code, a boolean) with `toBe`/`expect().rejects` is fine and expected.
73+
- **Relationship to `prefer-object-matcher.mdc`:** that rule consolidates scattered
74+
`expect().toBe()` calls into one matcher repo-wide. This rule is the stricter, sql-orm-client
75+
specialization for *query results*: the result row is the contract, so assert it **completely**
76+
with `toEqual` + `select` rather than partially with `toMatchObject`. `toMatchObject` is still
77+
fine for the non-result, constructed-object cases `prefer-object-matcher` targets.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ The repo keeps a single canonical home for each kind of agent surface, with pres
5353
- Don't reexport from one file in another, except in `exports/` folders.
5454
- Don't branch on target; use adapters: `.agents/rules/no-target-branches.mdc`.
5555
- Keep tests concise; omit "should": `.agents/rules/omit-should-in-tests.mdc`.
56+
- In sql-orm-client tests, assert the whole result shape (`toEqual`/snapshot) with explicit `select`: `.agents/rules/sql-orm-client-whole-shape-assertions.mdc`.
5657
- Keep docs current (READMEs, rules, links): `.agents/rules/doc-maintenance.mdc`.
5758
- Prefer links to canonical docs over long comments.
5859

0 commit comments

Comments
 (0)