From b6ff78b905d4049adbc15d7f902113ebdc8f8c0d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 14:11:27 +0000 Subject: [PATCH] test(lint): pin the time-relative fixture to a descriptor the runtime can bind (#4966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lint-flow-patterns.test.ts`'s user-less-trigger case described a flow the runtime would never sweep: `{ object: 'task', field: 'due_at', offsetDays: -1 }` fails `TimeRelativeTriggerSchema` on both counts — `field` is a diagnostic alias, not an accepted key (`dateField` is declared), and `offsetDays` is `z.array(z.number().int()).min(1)`, not a scalar. `TimeRelativeTriggerPlugin .start()` `safeParse`s that descriptor at bind time, so the fixture's flow would warn and stay unbound. The fixture stayed green because the rule decides `time-relative` from `startCfg.timeRelative != null` alone and never reads the shape, so nothing in the suite could see it. Correct the spelling to the bindable shape, keep an in-place comment explaining why it must stay bindable, and pin it against the same schema the bind path parses so it cannot rot back. No production behaviour changes — test-only. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D --- packages/lint/src/lint-flow-patterns.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/lint/src/lint-flow-patterns.test.ts b/packages/lint/src/lint-flow-patterns.test.ts index 3e1f8076a6..56c58a8556 100644 --- a/packages/lint/src/lint-flow-patterns.test.ts +++ b/packages/lint/src/lint-flow-patterns.test.ts @@ -1,6 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect } from 'vitest'; +import { TimeRelativeTriggerSchema } from '@objectstack/spec/automation'; import { lintFlowPatterns, FLOW_TIME_RELATIVE_ANTIPATTERN, @@ -339,8 +340,23 @@ describe('lintFlowPatterns — user-less runAs unscoped (#1888 / ADR-0049 / ADR- // boundary: these triggers build an AutomationContext with no `userId` at all, // so they hit the identical refusal. it('flags the OTHER provably user-less triggers too — time-relative and api', () => { + // #4966 — the descriptor must be one the runtime can actually BIND. This rule + // decides `time-relative` from `startCfg.timeRelative != null` alone + // (`lint-flow-patterns.ts`), never from the shape, so this fixture used to + // read `{ object, field: 'due_at', offsetDays: -1 }` and stayed green while + // describing a flow that never gets a sweep: `TimeRelativeTriggerPlugin.start()` + // `safeParse`s `config.timeRelative` against `TimeRelativeTriggerSchema` and + // refuses on both counts — `field` is a diagnostic alias, not an accepted key + // (the declared one is `dateField`), and `offsetDays` is `z.array(int).min(1)`, + // not a scalar. A fixture the runtime would refuse teaches the wrong shape to + // every reader of this file (#4001's first-class finding class). Hence the + // parse assertion below: the fixture is pinned against the same schema the + // bind path uses, so it cannot rot back into an unbindable descriptor. + const timeRelativeDescriptor = { object: 'task', dateField: 'due_at', offsetDays: [-1] }; + expect(TimeRelativeTriggerSchema.safeParse(timeRelativeDescriptor).success).toBe(true); + const timeRelative = lintFlowPatterns( - scheduledDataFlow({ flowType: 'autolaunched', startConfig: { timeRelative: { object: 'task', field: 'due_at', offsetDays: -1 } } }), + scheduledDataFlow({ flowType: 'autolaunched', startConfig: { timeRelative: timeRelativeDescriptor } }), ); expect(timeRelative.map((f) => f.rule)).toContain(FLOW_RUNAS_UNSCOPED); expect(timeRelative[0].message).toMatch(/time-relative/);