Skip to content

Make three green assertions actually inspect what they claim — starting with the FLS guard whose filter half has never once run (epic #1579, from #1770) #1772

Closed

Description

@os-steve

Follow-up to #1770, whose report (5581335470) proved these by measurement. ⛔ #1770 was read-only by design and correctly filed nothing; the fixes are this card's.

⛔ This card adds no test and no assertion. Every item makes an assertion that already exists inspect the set it already claims to inspect. It cannot grow the gate farm; it makes three tests stop lying about their coverage.

1. ⭐ test/authorization-coverage.test.ts:327 — the reason this card exists

it('a masked (unreadable) field is never filtered or sorted on by a view') walks view nodes and reads:

for (const f of node.filters ?? []) { … }   // ← never iterates
for (const s of node.sort    ?? []) { … }   // ← works

Measured by #1770 with paired legs in the same file, same test, same run:

leg injection result
control masked field planted in a node's sort RED — undefined: sorts on masked field "crm_case.internal_notes"
defect the same masked field planted in the node's filter — the exact case the assertion names GREEN, 33 passed

node.filters is present on 0 of 103 walked nodes at run time.

⚠️ Verified by the PM on origin/main, and it is worse than "this app happens not to author that key"

filters (plural) appears 0 times across src/views/ and src/pages/; filter appears 39 times and sort 42. And in @objectstack/spec the view/page node schemas have no filters key at all — ListViewSchema.filter, ViewTabSchema.filter and friends are all singular arrays of ViewFilterRuleSchema.

⭐ Where filters does appear in the spec is in the alias tables, as a spelling the protocol normalises away:

packages/spec/src/ui/chart.zod.ts:436         where: 'filter', criteria: 'filter', filters: 'filter',
packages/spec/src/ui/app.zod.ts:1066          aliases: { …, filters: 'filter', where: 'filter' }
packages/spec/src/automation/time-relative-trigger.zod.ts:102   filters: 'filter'
packages/spec/src/data/analytics.zod.ts:384   '`filters` is not an AnalyticsQuery field — use `where` …'

⇒ The test author wrote the exact wrong-but-natural spelling the protocol anticipates and aliases, and because the test reads the authored object rather than the normalised one, the alias never rescues it. This is not a stale key; it is a key that was never right, on any conformant app, for as long as the assertion has existed.

⚠️ What makes it worth a card rather than a note: the assertion's own comment says querying a hidden field throws field_predicate_denied and "breaks the whole list." Half of that guard — the filter half, the half a real author is more likely to trip — has never once run.

The work: node.filters → node.filter. Then re-run.

⛔ If it goes RED on this repo's real metadata, that is the finding, not a problem to route around. Fix the offending view's metadata, or report it here with the site named. ⛔ Never weaken, narrow or delete the assertion to get back to green — the whole defect being repaired is an assertion that was green for the wrong reason.

2. test/flow-record-change.test.ts:259 — ⭐ a guard that looks like it protects the walk and does not

it('sends every SLA task to the lead owner') inspects 0 crm_task rows on both executions. The neighbouring anti-vacuity guard is:

expect(h.notifications.length + h.store.crm_task.length).toBeGreaterThan(0);

⭐ It is disjunctive. The notification alone satisfies it, so it goes green while the crm_task walk it appears to cover inspects nothing. ⚠️ Worth naming as a class, because it is the failure mode an author reaches for when they are already thinking about vacuity: a sum over two populations proves neither is non-empty.

The work: make the guard prove the population the walk actually consumes — expect(h.store.crm_task.length).toBeGreaterThan(0) as its own assertion (keep the notification check too if it is pinning something). Then make the walk reach a task, or state why the fixture cannot produce one.

⛔ Same rule as item 1: if separating the guard turns the file red, that is the finding.

3. test/docs-setup-navigation-names.test.ts — an it.each row that asserts nothing

The row for entry ['流程运行记录'] executes its walker and makes zero expect() calls, because NOT_BANNED contains exactly that string. A green parameterised case that pins no fact.

The work: either give the row an assertion, or drop it from the table if the entry is genuinely not a case. ⛔ Do not silence it by removing the counter — decide which it is.

Deliberately NOT in this card

Acceptance

  • Each of the three assertions, after the fix, reds on a defect planted in a case it now inspects — quote the firing assertion, and pair it with the pre-fix green as the control. ⭐ Item 1's proof is already written for you: the same two legs, now both RED.
  • pnpm test green on the final commit, with the pre-change run quoted alongside, same command, same session.
  • objectstack lint reproduces the base's verdict. ⛔ Re-derive that verdict on your own base SHA — do not carry a number from any card, this one included.
  • Restores and injections proved by git hash-object == git rev-parse HEAD:<path>, ⛔ never by exit code.
  • ⛔ No new test files, no new assertions beyond splitting item 2's disjunctive guard, no metadata change unless item 1 turns up a real violation — and then only the violation.
  • Changeset: empty frontmatter (tests only) unless a metadata fix lands, in which case describe that.
  • ⛔ content/docs/releases/ untouched.

Refs: #1770 (the measurement; report 5581335470) · #1579 (epic) · PR #1611 (the 61 → 47 precedent) · #1625 / PR #1629 (per-case gate 3).

Activity

  1. added
    pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
    on Sep 8, 2026
  2. self-assigned this
    on Sep 8, 2026
  3. os-steve commented on Sep 8, 2026

    @os-steve
    CollaboratorAuthor

    Claim: epic PM session_01DuzfS5chho38Yx1jxx9DEj → dev branch claude/issue-1772-vacuous-assertions-fix

    Dispatched by the epic PM for #1579. The assignee and this claim are set by the PM; the dev inherits both — ⛔ post no second claim and ⛔ never write the assignee field.

    ⛔ Confirm your own base SHA; ⛔ re-derive the lint baseline on it rather than carrying any number from this card or from #1770.


    Generated by Claude Code

  4. claude commented on Sep 8, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 1772,
      "status": "done",
      "branch": "claude/issue-1772-vacuous-assertions-fix",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/1774",
      "premise_still_valid": true,
      "summary": "All three assertions now inspect the set they claimed. (1) authorization-coverage.test.ts: node.filters -> node.filter. Re-measured on my own base rather than carried from the card: filters present on 0 walked view nodes, filter on 29, sort on 39. The newly-live filter half is GREEN on this repo's real metadata -- the trap the card warned about did not fire; the only readable:false field is crm_case.internal_notes and no view filters on it, so nothing was weakened and no metadata was touched. (2) flow-record-change.test.ts: the disjunctive guard notifications.length + crm_task.length is split onto the population the flow actually writes. FINDING, stated as the card directs: the fixture cannot produce a task and neither can any input -- lead_assignment authors update_record and notify and NO create_record node at all, so its 'SLA' is a next_followup_date stamp on the lead, never a task row. h.store.crm_task is empty by construction of the flow under test, so expect(crm_task.length).toBeGreaterThan(0) would be a permanently red assertion about a population this flow cannot have. The unreachable walk is left in place and stated at the site rather than deleted; what was removed is the guard that made it look already covered. Whether lead_assignment SHOULD create an SLA task is a product question this PR does not answer. (3) docs-setup-navigation-names.test.ts: the it.each row whose every spelling is exempt made zero expect() calls; the exemption was also a silencer, since adding a genuinely retired name to NOT_BANNED removed all judgement of it. Every spelling is now judged in the direction its entry claims, through the SAME single expect call site with its expected value flipped per exemption -- no assertion added. Assignee was already set by the PM and I did not touch it; the newest Claim: comment (5581390884) names my branch.",
      "tests": "Base 5b2ecdd, final commit cfb16f7. PRE-CHANGE, same command same session: pnpm test 'Test Files 164 passed (164)' / 'Tests 3438 passed | 1 skipped (3439)'; objectstack lint --json passed:true total:13 errors:0 warnings:1 suggestions:12 hiddenPlatform:2319 issues:13 conversions:0. FINAL COMMIT: byte-identical on both -- lint issue lists compared item by item, only 'duration' moves. Lint verdict RE-DERIVED on my own base SHA, not carried from #1772 or #1770; both payloads parsed with raw_decode from the first '{'. Also green: pnpm typecheck, pnpm hygiene, pnpm hygiene:tokens, pnpm lint:i18n-gate (token ratchet unmoved -- it counts src/ layers and this PR touches only test/ and .changeset/). PAIRED LEGS, control then defect, same file/command/run. 1A masked field in a walked node's sort: RED pre-fix, RED post-fix ('undefined: sorts on masked field \"crm_case.internal_notes\": expected [ Array(1) ] to deeply equal []'). 1B the same field in that node's filter, the exact case the assertion names: GREEN pre-fix ('Test Files 1 passed', 'Tests 33 passed (33)', VITEST-EXIT=0), RED post-fix ('undefined: filters on masked field \"crm_case.internal_notes\"'). Both of the card's legs are now RED. 2: edge e4 retargeted notify_hot->end plus one pre-existing crm_task row seeded in the store -- GREEN pre-fix ('Tests 1 passed | 43 skipped'), RED post-fix ('AssertionError: hot lead produced no follow-up alert: expected 0 to be greater than 0'). Scoped with -t 'routes a hot lead' because unscoped that injection also reds 'lead_assignment stamps the SLA on an integration-written lead', which pins expect(h.notifications).toHaveLength(1) independently -- declared and re-scoped, not silently retried. 3: 'Process Monitor', a genuinely retired name, added to NOT_BANNED -- GREEN pre-fix ('Tests 24 passed (24)'), RED post-fix (\"'Process Monitor' is exempted from the ban as a spelling that is correct somewhere, but the platform ships no navigation label by that name any more...: expected false to be true\"). INJECTION DISCIPLINE: six injections across four files, all restored in the same run. Each recorded git rev-parse HEAD:PATH first, refused to start on an already-dirty path, carried trap restore EXIT INT TERM with absolute paths, restored via 'git checkout HEAD -- PATH' (never a bare 'git checkout --', which takes the polluted index), and proved restore by git hash-object PATH == the HEAD blob hash AND git diff HEAD empty -- never by exit code; an empty hash was coded FAILURE, not 'nothing to compare'. Landing was proved on disk each time (anchor and injected-text grep -c before/after plus a changed blob hash); no injection was a no-op. Post-fix legs mutate against HEAD with the fix already committed. All exit codes captured by redirecting to a file BEFORE reading, never through a pipe; verdicts read from each runner's own output, not from a bare $?. Final tree: git status --porcelain empty. Heavy runs went through the shared lock (scripts/pm/os-verify-lock.sh, slot hotcrm-1772): VERDICT command-exit 0 on both full-suite runs.",
      "mcp_calls": "1 - one targeted MCP search_issues for dedup before filing #1773. DECLARED CHANNEL SWITCH: REST /search/* is 403 by design at the egress proxy (probed this session), so the card read, the comment read, issue creation, PR creation and every read-back went over repo-scoped REST; MCP was used only for that one search.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #1773: the masked-field FLS guard reports every violation as 'undefined:' -- no view in this app carries a top-level name (measured: 0 of 14), so the assertion can never name the view an author must go and fix. Different class from this card (the assertion fires correctly; its message cannot name the site), so not fixed here. No assignee, no labels.",
        "reported on #1772 and in PR #1774 rather than filed, because the card explicitly asks for it to be stated: lead_assignment has no create_record node, so the 'SLA task' its test walks cannot exist on any input. Whether the flow should create one is a product decision for PM triage."
      ]
    }

    Generated by Claude Code

  5. os-steve commented on Sep 8, 2026

    @os-steve
    CollaboratorAuthor

    ACCEPTED — PR #1774 ready, auto-merge armed. ⭐ The trap did not fire, and the two things the card did not ask for are the best part.

    Epic PM session_01DuzfS5chho38Yx1jxx9DEj, 08:2xZ. Base 5b2ecdd, head cfb16f7. 8 of 9 checks green, Build and Test (22.x) still running — auto-merge will hold until it lands.

    Verified first-hand, ⛔ not read off the report

    claim what I re-derived
    item 1 is one key, nothing weakened diff on test/authorization-coverage.test.ts is node.filters → node.filter plus a docblock. The bad.push message, the hidden set, the walk and the toEqual([]) are untouched.
    lead_assignment cannot produce a task node set is start · decision · update_record · notify · end, twice. create_record: 0. ⭐ Control: eight sibling flows in the same repo do author it (campaign-enrollment 2, contract-renewal 2, lead-conversion 3, demo-bootstrap, forecast-snapshot, opportunity-stagnation, quote-generation, schedule-followup 1 each) — so the zero is a real reading, not a grep that never fires.
    #1773's premise defineView({ list: { type, **name: 'all_accounts'**, label, data, columns … } }) — name sits on list, so v.name on the top-level record is genuinely undefined.
    scope 4 files: three tests + one changeset. No src/ change, consistent with "nothing was weakened, no metadata touched".

    ⭐ The trap did not fire, and the dev proved that rather than assuming it

    Making the filter half live for the first time ever could have gone red on real metadata. It did not: the only readable: false entry is crm_case.internal_notes, and no view filters on it. ⚠️ That is a fact about today's metadata, not a property of the guard — which is exactly why the guard needed to start running. Both of the card's legs are now RED, including the one that was green for years:

    1A  masked field in a node's sort   → RED pre-fix, RED post-fix
    1B  the same field in that node's filter → GREEN pre-fix (33 passed), RED post-fix
    

    ⭐ Item 3 turned out to be a better finding than the card described

    The card said: give the row an assertion, or drop it. The dev found the actual defect underneath — if (NOT_BANNED.has(wrong)) continue; meant the exemption list was a silencer, not an exemption. A name on it was not judged in either direction. So the zero-expect() row was a symptom; the disease was that adding a genuinely retired name to NOT_BANNED removed all judgement of it and nothing noticed.

    The fix judges every spelling through the same single assertion with the expected value flipped per exemption — .toBe(exempt), banned must resolve to nothing, exempt must still resolve. ⛔ No assertion added, and the paired leg proves it: 'Process Monitor' exempted → GREEN pre-fix at 24 passed, RED post-fix with a message that tells the author which of the two possibilities they are in.

    ⚠️ Item 2 — the right call, and the escape hatch used exactly as written

    The card offered "make the walk reach a task, or state why the fixture cannot produce one." The dev took the second and stated it: h.store.crm_task is empty by construction of the flow under test, so expect(crm_task.length).toBeGreaterThan(0) would be a permanently red assertion about a population that cannot exist.

    ⭐ What it did with the walk is the part worth recording. It did not delete the unreachable walk and did not force it green: it removed the guard that made the walk look already covered, split the live half onto the population the flow actually writes, and left the task walk in place with the reason at the site — "it is the guard that would catch a dot-walked owner the day a task node lands." Same shape I ruled acceptable for flow-decision-authority.test.ts:180: a self-declared, compensated dead branch is honest; a silently dead one is not.

    The paired leg is properly disambiguated too — the injection (edge e4 retargeted plus one seeded crm_task row) also reds a neighbouring test that pins expect(h.notifications).toHaveLength(1) independently, so the dev re-scoped with -t and declared it rather than reporting a confounded red.

    The naming that came out of this round

    ⭐ A guard summing two populations proves neither is non-empty. expect(notifications.length + crm_task.length).toBeGreaterThan(0) is what an author reaches for while already thinking about vacuity — it looks like it covers both walks and covers neither. Now recorded in the file itself.

    Out-of-scope, correctly routed

    ⚠️ For PM triage — ⛔ not this epic's, and ⛔ not the dev's to decide

    it('sends every SLA task to the lead owner, never a dot-walked manager') presupposes SLA task rows. The flow stamps next_followup_date and sends a notification, and structurally cannot make a task. So either the test's name overstates what the flow does, or the flow is missing a create_record node. That is a product question about hotcrm's business logic, not a test-farm question — filing it separately, unassigned, for whoever owns that decision.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions