Skip to content

Commit 67772f5

Browse files
committed
fix(client): type getRuntimeStatus rows as the FlowRuntimeState contract
`client.automation.getRuntimeStatus()` declared its rows as a hand-typed `{ name, enabled, bound }`, narrower than what `GET /automation/_status` serves: the engine's `getFlowRuntimeStates()` rows, unchanged, typed by the declared contract `FlowRuntimeState` in `@objectstack/spec/contracts`. The optional `status`, `triggerType`, `object`, `reason` and `unboundCause` were therefore invisible to SDK callers. The return type is now `{ flows: FlowRuntimeState[]; total: number }`, with `FlowRuntimeState` added to the existing type-only contracts import. A type-level pin in return-type-precision.test.ts holds the answer equal to the contract and reads the five optional fields through the client's row type. Patch changeset for @objectstack/client. Card 23859. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0744f94 commit 67772f5

3 files changed

Lines changed: 60 additions & 1 deletion

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/client': patch
3+
---
4+
5+
Clause-②: no
6+
7+
fix(client): `client.automation.getRuntimeStatus()` types its rows as the declared contract `FlowRuntimeState` (#23859)
8+
9+
**What was wrong.** The method declared its answer as `{ flows: Array<{ name, enabled, bound }>; total }`. `GET /automation/_status` serves the automation engine's `getFlowRuntimeStates()` rows unchanged, and each row is the contract `FlowRuntimeState` from `@objectstack/spec/contracts`. The SDK's type therefore hid five optional fields the wire already carries: `status`, `triggerType`, `object`, `reason` (one sentence saying why an enabled, triggered flow is not armed) and `unboundCause` (which of the closed `FLOW_UNBOUND_CAUSES` that sentence describes). A caller that read `row.reason` or `row.unboundCause` got a type error and either cast past it or concluded the field did not exist.
10+
11+
**What changes.** The return type is now `Promise<{ flows: FlowRuntimeState[]; total: number }>`. `name`, `enabled` and `bound` stay required, so existing callers compile unchanged. Read `row.unboundCause` to classify an unarmed flow, and render `row.reason` without parsing it. The request, the route and the values on the wire are unchanged.

‎packages/client/src/index.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ import type {
163163
AudienceBindingSuggestionSync,
164164
AutomationResult,
165165
DelegableScope,
166+
FlowRuntimeState,
166167
ImportObjectResult,
167168
ObjectDraft,
168169
RecordShare,
@@ -5664,8 +5665,13 @@ export class ObjectStackClient {
56645665
/**
56655666
* Runtime enable/bound state for every flow — engine state, not
56665667
* persisted metadata (backs the Studio's Automations status badges).
5668+
*
5669+
* [#23859] Each row is the declared contract `FlowRuntimeState`: the
5670+
* route serves `getFlowRuntimeStates()` rows unchanged, so the optional
5671+
* `status` / `triggerType` / `object` / `reason` / `unboundCause` reach
5672+
* the caller. A hand-typed `{ name, enabled, bound }` hid them.
56675673
*/
5668-
getRuntimeStatus: async (): Promise<{ flows: Array<{ name: string; enabled: boolean; bound: boolean }>; total: number }> => {
5674+
getRuntimeStatus: async (): Promise<{ flows: FlowRuntimeState[]; total: number }> => {
56695675
const route = this.getRoute('automation');
56705676
const res = await this.fetch(`${this.baseUrl}${route}/_status`);
56715677
return this.unwrapResponse(res);

‎packages/client/src/return-type-precision.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ import type {
8484
AnalyticsResult,
8585
AutomationResult,
8686
DelegableScope,
87+
FlowRuntimeState,
88+
FlowUnboundCause,
8789
ImportObjectResult,
8890
RecordShare,
8991
RemoteTable,
@@ -1418,6 +1420,45 @@ export function metaResetResponseDeclaresTheWireReceipt(): void {
14181420
void metaResetBody.projectionApplied;
14191421
}
14201422

1423+
/**
1424+
* [#23859] `automation.getRuntimeStatus()` hand-typed its rows as
1425+
* `{ name, enabled, bound }`. `GET /automation/_status` serves the engine's
1426+
* `getFlowRuntimeStates()` rows unchanged (that pass-through is pinned in
1427+
* `packages/runtime/src/domain-handler-registry.test.ts`), and those rows are
1428+
* the declared contract `FlowRuntimeState`. The narrower annotation hid
1429+
* `status`, `triggerType`, `object`, `reason` and `unboundCause` from every SDK
1430+
* caller, who then cast past it or concluded the fields did not exist.
1431+
*
1432+
* Type-level for the reason this file's header gives: the runtime pin in
1433+
* `meta-automation-descriptors.test.ts` is green against either annotation.
1434+
*/
1435+
type RuntimeStatusRow = Awaited<ReturnType<ObjectStackClient['automation']['getRuntimeStatus']>>['flows'][number];
1436+
1437+
export async function returnTypePrecisionPins23859(): Promise<void> {
1438+
// ── the answer IS the contract, envelope and element ──────────────────
1439+
// RED BEFORE: the hand-typed element is not `FlowRuntimeState`.
1440+
expectTypeOf(await client.automation.getRuntimeStatus()).toEqualTypeOf<{
1441+
flows: FlowRuntimeState[];
1442+
total: number;
1443+
}>();
1444+
1445+
// ── the optional fields a caller can now read, with the contract's own
1446+
// optionality and vocabulary ──────────────────────────────────────
1447+
// RED BEFORE: each indexed access is TS2339 on the hand-typed row.
1448+
expectTypeOf<RuntimeStatusRow['reason']>().toEqualTypeOf<string | undefined>();
1449+
expectTypeOf<RuntimeStatusRow['unboundCause']>().toEqualTypeOf<FlowUnboundCause | undefined>();
1450+
expectTypeOf<RuntimeStatusRow['status']>().toEqualTypeOf<string | undefined>();
1451+
expectTypeOf<RuntimeStatusRow['triggerType']>().toEqualTypeOf<string | undefined>();
1452+
expectTypeOf<RuntimeStatusRow['object']>().toEqualTypeOf<string | undefined>();
1453+
1454+
// ⚠️ GREEN IN BOTH STATES, a compatibility guard rather than evidence: the
1455+
// three fields the old annotation named stay REQUIRED, so a caller that
1456+
// read them still compiles.
1457+
expectTypeOf<RuntimeStatusRow['name']>().toEqualTypeOf<string>();
1458+
expectTypeOf<RuntimeStatusRow['enabled']>().toEqualTypeOf<boolean>();
1459+
expectTypeOf<RuntimeStatusRow['bound']>().toEqualTypeOf<boolean>();
1460+
}
1461+
14211462
describe('client SDK return-type precision (#8140)', () => {
14221463
it('exposes the type-level pins to tsc without executing a request', () => {
14231464
// The assertions above are evaluated by `tsc` under
@@ -1439,6 +1480,7 @@ describe('client SDK return-type precision (#8140)', () => {
14391480
expect(typeof returnTypePrecisionPins13023).toBe('function');
14401481
expect(typeof deleteDataResponseIsNotTheMetaResetShape).toBe('function');
14411482
expect(typeof metaResetResponseDeclaresTheWireReceipt).toBe('function');
1483+
expect(typeof returnTypePrecisionPins23859).toBe('function');
14421484
expect(typeof commitRollbackResponseIsNotTheVersionRollbackShape).toBe('function');
14431485
// `environmentIsNotTheCloudWireRow` left with its subject (#16325): the
14441486
// guard is now the `@ts-expect-error`'d import at the top of the file.

0 commit comments

Comments
 (0)