Skip to content

Fixtures that stand in for objectstack build cannot reuse lowerCallablesJSON.stringify(stack) silently drops every callable instead #6293

Description

@qq9340100

Observation-class finding, surfaced while implementing #4976. One site exists today and it is fixed in that PR; this issue records the shape, because the next fixture that needs "the stack as the build would emit it" walks into the same trap.

The trap

lowerCallables lives in packages/cli/src/utils/lower-callables.ts and is not exported from @objectstack/cli's public entry. A test that needs a built-shaped artifact therefore cannot call it, and the reachable substitute is JSON.stringify(stack) — which does only half of what the build does:

real objectstack build JSON.stringify(stack)
bare callable fn: () = > … lowered to 'fn', callable into the sidecar .mjs key omitted entirely
declared { handler: fn, effect } { handler: 'fn', effect }, callable into the sidecar { effect } — a headless husk

Why it hides

The bare-form column is the dangerous one: the entry vanishes key and all, so the artifact holds functions: {} and parses green. packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts had been writing its artifact that way, and it had never carried either of the showcase's two functions — the boot it advertises as "the way a deployment boots it" ingested zero of them, invisibly, for as long as the fixture has existed.

Measured:

> JSON.stringify({ functions: { a: () => 1, b: { handler: () => 2, effect: 'writes' } } })
{"functions":{"b":{"effect":"writes"}}}

> JSON.stringify({ functions: { summarizeCompletedTask: fn, sweepProjectHealth: fn } })
{"functions":{}}

The declared-form column is what finally made a noise: #4976 restored the showcase's honest { handler: sweepProjectHealth, effect: 'writes' } spelling, the husk { effect: 'writes' } reached the parse, and FlowFunctionEntrySchema refused it in all four members — correctly, since an entry declaring an effect for a function it does not carry is not a thing the runtime can honour. That red CI job is the only reason anybody learned the artifact was empty.

So the failure mode is the one this repo keeps paying for: a fixture that silently degrades and reports success (see AGENTS.md "Absence must be loud" — a verifier that silently degrades is worse than no verifier).

Not urgent

Nothing a user hits: this is test infrastructure, and the single existing site is fixed in #4976's PR by declaring the omission explicitly (drop functions, say why, note that the live stack handed to bootStack is what actually supplies them). Hence finding, not pm:queue.

The question worth deciding, when it next comes up

Whether the lowering should be reachable outside the CLI at all — so that a fixture standing in for the build agrees with the build by construction rather than by re-derivation. Three readings, none obviously right:

  1. Export it (move lowerCallables to a shared package, or re-export from @objectstack/cli). Fixtures get the real thing; the surface grows for a consumer set of one test.
  2. Leave it CLI-private and let fixtures declare what they omit — what functions: { fn: { handler, effect: 'writes' } } cannot survive objectstack build — lowering emits a shape FlowFunctionEntrySchema rejects #4976's PR does. Cheapest, and honest, but each new fixture must remember.
  3. Have fixtures consume a real built artifact rather than synthesising one. Truest, heaviest.

Per the startup-focus principle there is no measured pull for (1) today — one site, now explicit. Recording it so the second site is a decision rather than a rediscovery.

Filed unassigned per Prime Directive #10.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions