Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/23859-client-runtime-status-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@objectstack/client': patch
---

Clause-②: no

fix(client): `client.automation.getRuntimeStatus()` types its rows as the declared contract `FlowRuntimeState` (#23859)

**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.

**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.
8 changes: 7 additions & 1 deletion packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ import type {
AudienceBindingSuggestionSync,
AutomationResult,
DelegableScope,
FlowRuntimeState,
ImportObjectResult,
ObjectDraft,
RecordShare,
Expand Down Expand Up @@ -5664,8 +5665,13 @@ export class ObjectStackClient {
/**
* Runtime enable/bound state for every flow — engine state, not
* persisted metadata (backs the Studio's Automations status badges).
*
* [#23859] Each row is the declared contract `FlowRuntimeState`: the
* route serves `getFlowRuntimeStates()` rows unchanged, so the optional
* `status` / `triggerType` / `object` / `reason` / `unboundCause` reach
* the caller. A hand-typed `{ name, enabled, bound }` hid them.
*/
getRuntimeStatus: async (): Promise<{ flows: Array<{ name: string; enabled: boolean; bound: boolean }>; total: number }> => {
getRuntimeStatus: async (): Promise<{ flows: FlowRuntimeState[]; total: number }> => {
const route = this.getRoute('automation');
const res = await this.fetch(`${this.baseUrl}${route}/_status`);
return this.unwrapResponse(res);
Expand Down
42 changes: 42 additions & 0 deletions packages/client/src/return-type-precision.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ import type {
AnalyticsResult,
AutomationResult,
DelegableScope,
FlowRuntimeState,
FlowUnboundCause,
ImportObjectResult,
RecordShare,
RemoteTable,
Expand Down Expand Up @@ -1418,6 +1420,45 @@ export function metaResetResponseDeclaresTheWireReceipt(): void {
void metaResetBody.projectionApplied;
}

/**
* [#23859] `automation.getRuntimeStatus()` hand-typed its rows as
* `{ name, enabled, bound }`. `GET /automation/_status` serves the engine's
* `getFlowRuntimeStates()` rows unchanged (that pass-through is pinned in
* `packages/runtime/src/domain-handler-registry.test.ts`), and those rows are
* the declared contract `FlowRuntimeState`. The narrower annotation hid
* `status`, `triggerType`, `object`, `reason` and `unboundCause` from every SDK
* caller, who then cast past it or concluded the fields did not exist.
*
* Type-level for the reason this file's header gives: the runtime pin in
* `meta-automation-descriptors.test.ts` is green against either annotation.
*/
type RuntimeStatusRow = Awaited<ReturnType<ObjectStackClient['automation']['getRuntimeStatus']>>['flows'][number];

export async function returnTypePrecisionPins23859(): Promise<void> {
// ── the answer IS the contract, envelope and element ──────────────────
// RED BEFORE: the hand-typed element is not `FlowRuntimeState`.
expectTypeOf(await client.automation.getRuntimeStatus()).toEqualTypeOf<{
flows: FlowRuntimeState[];
total: number;
}>();

// ── the optional fields a caller can now read, with the contract's own
// optionality and vocabulary ──────────────────────────────────────
// RED BEFORE: each indexed access is TS2339 on the hand-typed row.
expectTypeOf<RuntimeStatusRow['reason']>().toEqualTypeOf<string | undefined>();
expectTypeOf<RuntimeStatusRow['unboundCause']>().toEqualTypeOf<FlowUnboundCause | undefined>();
expectTypeOf<RuntimeStatusRow['status']>().toEqualTypeOf<string | undefined>();
expectTypeOf<RuntimeStatusRow['triggerType']>().toEqualTypeOf<string | undefined>();
expectTypeOf<RuntimeStatusRow['object']>().toEqualTypeOf<string | undefined>();

// ⚠️ GREEN IN BOTH STATES, a compatibility guard rather than evidence: the
// three fields the old annotation named stay REQUIRED, so a caller that
// read them still compiles.
expectTypeOf<RuntimeStatusRow['name']>().toEqualTypeOf<string>();
expectTypeOf<RuntimeStatusRow['enabled']>().toEqualTypeOf<boolean>();
expectTypeOf<RuntimeStatusRow['bound']>().toEqualTypeOf<boolean>();
}

describe('client SDK return-type precision (#8140)', () => {
it('exposes the type-level pins to tsc without executing a request', () => {
// The assertions above are evaluated by `tsc` under
Expand All @@ -1439,6 +1480,7 @@ describe('client SDK return-type precision (#8140)', () => {
expect(typeof returnTypePrecisionPins13023).toBe('function');
expect(typeof deleteDataResponseIsNotTheMetaResetShape).toBe('function');
expect(typeof metaResetResponseDeclaresTheWireReceipt).toBe('function');
expect(typeof returnTypePrecisionPins23859).toBe('function');
expect(typeof commitRollbackResponseIsNotTheVersionRollbackShape).toBe('function');
// `environmentIsNotTheCloudWireRow` left with its subject (#16325): the
// guard is now the `@ts-expect-error`'d import at the top of the file.
Expand Down
Loading