Skip to content

Commit c577e66

Browse files
fix(create-objectstack): the blank starter wires every generator barrel, so os g scaffolds reach the stack (#20333) (#20363)
Fixes #20333 Clause-②: no ## Summary `npm create objectstack`'s blank starter imported `./src/objects` alone, so everything `os g view|action|flow|dashboard|app|skill` wrote was never loaded and `os validate` counted 0 of it. The starter now wires the seven generator barrels `os init` wires since PR #20329, in the lines `os init` renders: `exportsOf` over `export {};` barrels, and `requires: ['automation', 'triggers']`. The copy is bound to the CLI's single source (`SCAFFOLD_WIRED_BARRELS` / `SCAFFOLD_WIRED_REQUIRES`, derived from `GENERATOR_SCAFFOLD_TARGETS`) by a parity pin, so it is not a second wiring rule. A per-PR pin drives `npm create objectstack` → `os g object` (control) → `os g flow` → `os validate` and reads `Logic: 1 Flows`. ## What changed - `packages/create-objectstack/src/templates/blank/objectstack.config.ts`: imports every wired barrel, declares the `exportsOf` helper, and hands each barrel to its stack key. The objects import changes from `'./src/objects/index.js'` to `'./src/objects'`, the extensionless form `os init` renders, which the parity pin compares verbatim; the template's `moduleResolution: bundler` resolves the directory index, and a fresh scaffold type-checks. It carries `requires: ['automation', 'triggers']`. `automation` was already there for the three connector plugins, and its comment keeps that reason. - Six new `src/{views,actions,flows,dashboards,apps,skills}/index.ts` barrels, byte-identical to what `os init` writes. - Two pins in `packages/cli/test/` (below). The CLI is the only package that can call the renderer, and it already depends on `create-objectstack`. - `packages/cli/package.json` gains `@objectstack/connector-{rest,openapi,mcp}` as devDependencies (lockfile +9 lines, one importer block). They exist only so the scaffolded project the chain pin builds under the CLI's `node_modules` can resolve the blank config's connector imports, and so CI builds them in `@objectstack/cli#test`'s closure. - `scripts/cross-package-test-inputs.mjs` and `turbo.json` declare the blank config and `src/**` as inputs of `@objectstack/cli#test`, with a witness for the barrel glob the scan cannot name. - Docs this change made false (see below), and a `create-objectstack` patch changeset. ## Why a static copy, and what binds it `create-objectstack` cannot import the roster. The dependency edge runs the other way, and the npx entry must not pull the CLI's closure: the boundary `scripts/sync-scaffold-emission-policy.mjs` already documents. Measured options: - **Generate at build time.** The roster is computed from the `GENERATORS` literal in `generate.ts`. Reading it at `create-objectstack`'s build would need either text-parsing that literal, or evaluating the CLI's source before the CLI's own dependencies are built, which is a build-order cycle. - **Parity pin over a static copy.** Chosen as the least machinery. `create-objectstack-wiring-parity.test.ts` reads every expected line off the CLI: the barrel import lines, the `exportsOf` line and the stack-key lines of `TEMPLATES.app.configContent`, the `requires` tokens as a superset of `SCAFFOLD_WIRED_REQUIRES`, and each empty barrel byte for byte from `TEMPLATES.app.srcFiles`. A generator added to the roster, a renderer change or a hand edit of the template reddens it (ablations A1 to A3). ## Measured before and after, through the real commands The on-ramp's real `bin/` scaffolded `my-app --skip-install --skip-skills` into a directory where the config's imports resolve, then this repo's CLI ran. | step | `origin/main` `c74de10a9` | this branch | |:---|:---|:---| | `os g object order_line` (control) | exit 0, reaches the stack | exit 0, reaches the stack | | `os g flow order_line` | exit 0, **Not wired** | exit 0, reaches the stack | | `os validate` | exit 0, `Data: 2 Objects`, `Logic: 0 Flows` | exit 0, `Data: 2 Objects`, `Logic: 1 Flows` | - **`exportsOf` is required here too.** A fresh starter type-checks (`tsc --noEmit`, 6.0.3, exit 0). The same starter with `Object.values` on the empty barrels fails with 4 x TS2322 (actions, flows, dashboards, apps). After generating the object and the flow it still type-checks. - **`requires` boots.** `os dev --fresh` on a random port: the flow-less fresh starter was healthy after about 22s, `/api/v1/ready` answered 200, and `AutomationServicePlugin` and the record-change, schedule, time-relative and api trigger plugins loaded, resolved through the CLI's own dependencies. With the generated flow it was healthy after about 24s and reported `Flows: 1 flow(s) 1 bound to triggers`. Neither boot printed "not enabled" or "NOT installed". - **Census.** `src/templates/` holds one starter, `blank`, which is also the registry's only entry. ## Pins - `packages/cli/test/create-objectstack-wiring-parity.test.ts` (unit, per-PR): 20 cases, described above. - `packages/cli/test/create-objectstack-stack-reach.test.ts` (integration, per-PR, not `.e2e`): the chain above, with item names read off the generator roster. It asserts the exit codes, the named subjects, the absence of the wiring lines and of a `requires` line from `os g flow`, and the `Data: 2 Objects` / `Logic: 1 Flows` counts. No prose is pinned. ## Ablations Each ran after the fix was committed. Mutations went through `scripts/ablation-replace.mjs` in wrap mode, which verified the anchor count and the blob change and restored with blob equal to HEAD and an empty `git diff HEAD`. - **A1, the wiring reverted** (the `flows: exportsOf(flows),` line deleted, then `create-objectstack` rebuilt). `ablation-dist-preflight --absent` confirmed the line was gone from `dist/`. Chain pin: 2 failed, 2 passed. The control and the scaffold stayed green, and `os g flow` printed the wiring lines while validate read no `Logic: 1 Flows`. Parity pin: 1 failed, 19 passed, on the stack-key comparison. Direction: red. - **A1 restore.** Rebuilt; `ablation-dist-preflight` found the marker present in `dist/templates/blank/objectstack.config.ts`, and the whole tree was clean. Chain pin 4/4, parity pin 20/20. - **A2, a barrel dropped from the template** (the `skills` key deleted): parity 1 failed, 19 passed. Red. - **A3, one barrel's bytes drifted from what `os init` writes** (`views/index.ts` reworded): parity 1 failed, 19 passed. Red. - After A2 and A3 the whole tree was clean, and parity was 20/20. ## Verification Patch round 1, at HEAD `702a27775` (origin/main `a88a1bb39` merged at `df0c0c846`): the 124 derived gates, `check-issue-citations --base origin/main` and `check:scaffold-emission-policy` all exited 0 on the first pass (`--ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN), including `check:doc-anchors`, `check:docs-audit-scope` and `check-affected-docs`; `pnpm lint` exited 0; the parity pin 20/20, the chain pin 4/4, and `create-objectstack` 16 files, 232 passed. Round 0: all of the following ran at HEAD `d50d46fe0` (origin/main `26daf0b03` merged). - `pnpm --filter create-objectstack test`: 16 files, 232 passed. `typecheck`: exit 0. - `pnpm --filter @objectstack/cli typecheck`: exit 0, including `check:test-typecheck`, whose ledger is unchanged. - CLI `unit` project: 231 files, 3316 passed. - CLI `integration`: this chain pin plus `generate-stack-reach.test.ts`, 2 files, 11 passed. - `pnpm lint`: exit 0 over the whole repo, not narrowed. - `node scripts/check-issue-citations.mjs --base origin/main`: exit 0. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN. Three gates first exited 3 with PREREQUISITE NOT MET (`check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n-coverage`) and exited 0 after a full build. - `pnpm check:scaffold-emission-policy`: exit 0. ## Docs this change made false, and a surface note These published lines described an objects-only starter and are corrected in place: - the blank starter's `README.md` Layout, plus its app remedy, which now says to export the file from `src/apps/index.ts`; - the shipped `AGENTS.md` rule 3, which prescribed `Object.values()` (measured TS2322 on the now-empty barrels); - the package `README.md` tree; - `content/docs/getting-started/your-first-project.mdx`: its section-2 tree and config block; - `content/docs/getting-started/build-with-claude-code.mdx` (patch round 1): step 3 said the agent wires the action, view and app through `actions:` / `views:` / `apps:` keys in `defineStack()`. It now says each file is exported from its directory's barrel (`src/actions/index.ts`, `src/views/index.ts`, `src/apps/index.ts`), which the starter's config already hands to `defineStack()`, matching the shipped `AGENTS.md` rule 3. A sweep of `content/docs/` found no other sentence telling a starter author to add a collection key; - `content/docs/deployment/cli.mdx`. In `cli.mdx`, the `os generate` section's "Not wired" example named "the `npm create objectstack` starter", and its first-app walkthrough ran `os generate action approve`. On the wired starter that action is refused with exit 1: "Action 'approve' references object 'my_app_approve' which is not defined in objects". The walkthrough now runs `object customer`, then `flow customer`, then `action customer`, measured `UI: 1 Actions` and `Logic: 1 Flows`. Its fixture callout now names the extra action. `content/docs/**` and `packages/create-objectstack/README.md` were outside the claim's first file surface; the seat amended the claim in place to name them. They are edited under the agent contract's rule that a published line this change makes false is repaired in the same PR. PR #20341 edits `cli.mdx` around lines 1619 to 1690, disjoint from these hunks; PR #20258 edited lines 1 to 7 of `your-first-project.mdx` and `build-with-claude-code.mdx`, has since landed, and merged into this branch without conflict. `skills/objectstack-platform/SKILL.md` line 192 says the template declares `requires: ['automation']`. That is now stale, but `skills/**` is a governed Tier H surface, so it is **not** edited here; the seat files it for the skills lane once this PR lands. ## Acceptance notes - Byte-identical barrels inherit the article slip in `init.ts`'s `renderEmptyWiredBarrel` ("a action", "a app"). `init.ts` is read-only here. Whoever next edits that renderer carries it, and the parity pin will then require the starter to follow. - The old walkthrough's `os generate flow onboarding` also bound its flow to an undeclared object. That was not silent: `os dev` warned "the flow will never fire". The new walkthrough binds to the object it creates. - Measured in patch round 1, on a scaffolded starter holding the Build with Claude Code step-3 files: exporting each from its barrel, with the config untouched, gives `os validate` exit 0 with `Data: 2 Objects 6 Fields` and `UI: 1 Apps 1 Views 1 Actions`, the page's step-4 counts. Adding `actions:` / `views:` / `apps:` keys beside the wired ones instead still validates (the later key wins), but the starter's `tsc --noEmit` fails with 3 x TS1117, and a later `os g view customer` then reports Not wired, while the barrel-wired project reports it reaches the stack. - The `requires` pair is PR #20329's shape. The standing family cards for the rest of that seam are #20331 and #20332, both named on #20215. --- _Generated by [Claude Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent eee0974 commit c577e66

21 files changed

Lines changed: 510 additions & 33 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'create-objectstack': patch
3+
---
4+
5+
fix(create-objectstack): the blank starter wires every directory `os generate` writes into
6+
7+
`npm create objectstack` scaffolded an `objectstack.config.ts` that imported `./src/objects` alone. `os g view`, `action`, `flow`, `dashboard`, `app` and `skill` each wrote a file and a barrel `index.ts` that nothing imported, and `os validate` then exited 0 printing `Logic: 0 Flows`: the generated metadata was never loaded.
8+
9+
**What a new blank project now ships** is the wiring `os init` writes:
10+
11+
- `objectstack.config.ts` imports every directory `os generate` writes into (`src/objects`, `src/views`, `src/actions`, `src/flows`, `src/dashboards`, `src/apps`, `src/skills`) and hands each barrel's exports to `defineStack` under its key (`objects`, `views`, …). A file `os g` writes there is part of the stack with no edit to the config. The keys read the barrels through a small `exportsOf` helper declared in the config, because `Object.values` on an empty barrel does not type-check against `defineStack`'s collection types.
12+
- An `index.ts` containing only `export {};` in each of those directories except `src/objects`, which keeps the sample object.
13+
- `requires: ['automation', 'triggers']`. `automation` was already there for the three connector plugins. `triggers` fires a flow that starts on a record change, the kind `os g flow` writes, and without it the config stops loading as soon as it holds one. A project with no flow boots as before.
14+
15+
**Projects scaffolded by an earlier release** keep their config. `os g` says when a file it wrote is not wired, and prints the lines to add.

‎content/docs/deployment/cli.mdx‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,15 @@ This scaffolds a working project with `objectstack.config.ts`, a sample object,
3333

3434
```bash
3535
os generate object customer # Add a Customer object
36-
os generate action approve # Add an action
37-
os generate flow onboarding # Add an automation flow
36+
os generate flow customer # Add an automation flow on it
37+
os generate action customer # Add an action on it that runs the flow
3838
```
3939

40+
Each command writes a file and its export line, and the starter's config already
41+
wires every directory they write into, so all three are part of the stack. The
42+
flow and the action bind to the object named like them, which is why the object
43+
comes first: an action bound to an object the stack does not declare is refused.
44+
4045
### Launch the dev server
4146

4247
```bash
@@ -739,8 +744,8 @@ the view and the action listed under
739744
renamed out of that page's `support_desk_` namespace into `my_app_`. That page's `support`
740745
app is **not** added, and a zero count is never printed, which is why the `UI:` row reads
741746
`1 Views 1 Actions` with no `Apps`. The walkthrough's `os generate` commands are **not**
742-
part of the fixture — run those as well and the summary gains `my_app_customer` and a
743-
`Logic:` row. Timings are machine identity, and the rule count and artifact size track the
747+
part of the fixture — run those as well and the summary gains `my_app_customer`, a second
748+
action and a `Logic:` row. Timings are machine identity, and the rule count and artifact size track the
744749
CLI version; everything else is fixture identity and reproduces.
745750
</Callout>
746751
@@ -1410,7 +1415,8 @@ else a scaffold names (an action's, flow's or app's own `name`) is prefixed.
14101415
14111416
**Every scaffold reaches the stack, or the command says it does not.** The
14121417
`objectstack.config.ts` that `os init` writes for the `app` and `plugin`
1413-
templates wires every directory in the table below: it imports each
1418+
templates, and the one the `npm create objectstack` starter ships, wires every
1419+
directory in the table below: it imports each
14141420
`src/<dir>/index.ts` barrel and hands its exports to `defineStack` under the key
14151421
in the **Collected as** column, so a file `os g` writes there is part of the
14161422
stack with no edit to the config. It also declares
@@ -1421,8 +1427,8 @@ After writing, `os g` loads the config again and says which of these holds:
14211427
checks it.
14221428
- **Not wired**: the config loads and its stack does not carry the item, or
14231429
there is no config. This is what happens with a config that imports
1424-
`./src/objects` alone, as `os init` projects from earlier releases and the
1425-
`npm create objectstack` starter do. The file is written, the config is left
1430+
`./src/objects` alone, as projects that `os init` and `npm create objectstack`
1431+
scaffolded in earlier releases do. The file is written, the config is left
14261432
as it was, and the command prints the import and the `defineStack` key that
14271433
wire the directory.
14281434
- **Cannot run**: the stack carries a flow, and its `requires` lacks

‎content/docs/getting-started/build-with-claude-code.mdx‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,16 @@ export const SupportApp = defineApp({
223223
});
224224
```
225225

226-
The agent also **wires the new files into `objectstack.config.ts`** — the object
227-
through the `src/objects/index.ts` barrel, and the action, view and app via the
228-
`actions:` / `views:` / `apps:` arrays in `defineStack()`. There is no
229-
filename-suffix magic: metadata exists in the app only if the config imports it,
230-
so if a freshly-authored action doesn't show up, the wiring is the first thing to
231-
check.
226+
The agent also **exports each new file from its directory's barrel** — the object
227+
from `src/objects/index.ts`, and the action, view and app from
228+
`src/actions/index.ts`, `src/views/index.ts` and `src/apps/index.ts`. The
229+
starter's `objectstack.config.ts` already hands every barrel to `defineStack()`
230+
through `exportsOf()`, so that export line is the whole wiring and the config is
231+
not edited; adding an `actions:` / `views:` / `apps:` key for the file instead
232+
would duplicate the key and replace that barrel's wiring. There is no
233+
filename-suffix magic: metadata exists in the app only if its barrel exports it,
234+
so if a freshly-authored action doesn't show up, its export line is the first
235+
thing to check.
232236

233237
What it does **not** touch is the starter `note` object the `blank` template
234238
scaffolded in step 1 (`src/objects/note.object.ts`, two fields). Nothing asked

‎content/docs/getting-started/your-first-project.mdx‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,22 +91,41 @@ my-app/
9191
├── tsconfig.json
9292
├── AGENTS.md # conventions for coding agents
9393
└── src/
94-
└── objects/
95-
├── index.ts # barrel — re-exports every object
96-
└── note.object.ts # a sample object
94+
├── objects/
95+
│ ├── index.ts # barrel — re-exports every object
96+
│ └── note.object.ts # a sample object
97+
├── views/index.ts # an empty barrel for each directory
98+
├── actions/index.ts # `os generate` writes into, already
99+
├── flows/index.ts # wired into objectstack.config.ts
100+
├── dashboards/index.ts
101+
├── apps/index.ts
102+
└── skills/index.ts
97103
```
98104

99105
Two files matter most:
100106

101107
**`objectstack.config.ts`** wires everything together. There is no
102-
filename-suffix magic — metadata exists in the app only if it is imported here:
108+
filename-suffix magic — metadata exists in the app only if it is imported here.
109+
Every directory `os generate` writes into is already imported, so a generated
110+
view, action or flow adds a file and one export line to its directory's
111+
`index.ts` and is part of the stack with no edit to this file:
103112

104113
```typescript title="objectstack.config.ts"
105114
import { defineStack } from '@objectstack/spec';
106115
import { ConnectorRestPlugin } from '@objectstack/connector-rest';
107116
import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi';
108117
import { ConnectorMcpPlugin } from '@objectstack/connector-mcp';
109-
import * as objects from './src/objects/index.js';
118+
import * as objects from './src/objects';
119+
import * as views from './src/views';
120+
import * as actions from './src/actions';
121+
import * as flows from './src/flows';
122+
import * as dashboards from './src/dashboards';
123+
import * as apps from './src/apps';
124+
import * as skills from './src/skills';
125+
126+
// Object.values, typed by what the barrel exports, so an empty barrel still
127+
// type-checks.
128+
const exportsOf = <M extends object>(barrel: M): M[keyof M][] => Object.values(barrel);
110129

111130
export default defineStack({
112131
manifest: {
@@ -120,14 +139,22 @@ export default defineStack({
120139
},
121140
// `automation` runs flows and, per ADR-0097, materializes declarative
122141
// `connectors:` entries at boot. The three generic executors below register
123-
// their `rest` / `openapi` / `mcp` provider factories with it.
124-
requires: ['automation'],
142+
// their `rest` / `openapi` / `mcp` provider factories with it. `triggers`
143+
// fires a flow that starts on a record change, the kind `os generate flow`
144+
// writes.
145+
requires: ['automation', 'triggers'],
125146
plugins: [
126147
new ConnectorRestPlugin(),
127148
new ConnectorOpenApiPlugin(),
128149
new ConnectorMcpPlugin(),
129150
],
130-
objects: Object.values(objects),
151+
objects: exportsOf(objects),
152+
views: exportsOf(views),
153+
actions: exportsOf(actions),
154+
flows: exportsOf(flows),
155+
dashboards: exportsOf(dashboards),
156+
apps: exportsOf(apps),
157+
skills: exportsOf(skills),
131158
});
132159
```
133160

‎packages/cli/package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@
132132
"better-sqlite3": "^13.0.3"
133133
},
134134
"devDependencies": {
135+
"@objectstack/connector-mcp": "workspace:*",
136+
"@objectstack/connector-openapi": "workspace:*",
137+
"@objectstack/connector-rest": "workspace:*",
135138
"@objectstack/driver-turso": "workspace:*",
136139
"@objectstack/plugin-dev": "workspace:*",
137140
"@oclif/plugin-help": "^6.2.58",
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* PIN (#20333) — `npm create objectstack` → `os g flow` → `os validate`
5+
* counts the flow, with `os g object` as the control.
6+
*
7+
* ## What was measured before the fix
8+
*
9+
* On `origin/main` `c74de10a9`, a project scaffolded by the on-ramp's real
10+
* `bin/` entry, then `os g object order_line` and `os g flow order_line`:
11+
*
12+
* os g object exit 0, "Reaches the stack" (objects was always wired)
13+
* os g flow exit 0, "Not wired: … is not part of the stack"
14+
* os validate exit 0, `Data: 2 Objects` and `Logic: 0 Flows`
15+
*
16+
* The blank starter's config imported `./src/objects` alone, so the flow was
17+
* written and never loaded. The control is what makes the red readable: the
18+
* same chain counted the generated object, so a 0 for the flow is the wiring
19+
* and not a harness that counts nothing.
20+
*
21+
* ## The chain, through the real commands
22+
*
23+
* node create-objectstack/bin/create-objectstack.js my-app --skip-install --skip-skills
24+
* os g object order_line → exit 0, reaches the stack
25+
* os g flow order_line → exit 0, reaches the stack, no wiring lines to add
26+
* os validate → exit 0, `Data: 2 Objects`, `Logic: 1 Flows`
27+
*
28+
* Asserted: exit statuses, the named subjects, the absence of the wiring
29+
* lines `os g` prints for a scaffold that did not arrive (code an author
30+
* pastes), and the counts `os validate` prints. Prose is not pinned. The item
31+
* names are read off the generator roster, not written down.
32+
*
33+
* ## Why here, why a child process, and why this file is NOT named `.e2e`
34+
*
35+
* `os g` and `os validate` are this package's commands, and it already
36+
* depends on `create-objectstack`, so `@objectstack/cli#test`'s `^build`
37+
* builds the on-ramp's `dist/` — the tree its `bin/` copies from. The bin and
38+
* the blank template are declared cross-package inputs of this package
39+
* (scripts/cross-package-test-inputs.mjs, mirrored into turbo.json).
40+
*
41+
* The project lives under this package's `node_modules`, so the scaffolded
42+
* config's imports resolve to workspace copies without an install. Besides
43+
* `@objectstack/spec`, the blank config imports three connector packages;
44+
* they are this package's devDependencies for that reason alone, which is
45+
* also what puts them in this suite's build closure.
46+
*
47+
* An exit status is the contract, and `process.exit` inside a vitest worker
48+
* is not one, so the commands are spawned (the `integration` project). The
49+
* name keeps it in the per-PR run. The structural half — the blank config
50+
* wires exactly what `os init` wires — is `create-objectstack-wiring-parity.test.ts`.
51+
*/
52+
53+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
54+
import { execFile } from 'node:child_process';
55+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
56+
import { join, resolve } from 'node:path';
57+
import { fileURLToPath } from 'node:url';
58+
import { GENERATOR_SCAFFOLD_TARGETS } from '../src/commands/generate.js';
59+
import { childEnv } from './helpers/serve-process.js';
60+
61+
const HERE = resolve(fileURLToPath(import.meta.url), '..');
62+
const CLI = resolve(HERE, '../bin/run-dev.js');
63+
const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
64+
65+
// One `resolve(HERE, …)` call per line: `check:cross-package-test-inputs`
66+
// reconstructs this read by SOURCE SCAN.
67+
const ON_RAMP_BIN = resolve(HERE, '../../..', 'packages/create-objectstack/bin/create-objectstack.js');
68+
69+
/** One plain-node scaffold, then three oclif + tsx cold starts, sequential. */
70+
const RUN_TIMEOUT_MS = 240_000;
71+
72+
const PROJECT = 'my-app';
73+
const NS = 'my_app';
74+
const STEM = 'order_line';
75+
76+
interface Run {
77+
code: number;
78+
stdout: string;
79+
stderr: string;
80+
}
81+
82+
function run(file: string, args: string[], cwd: string): Promise<Run> {
83+
return new Promise((resolvePromise) => {
84+
execFile(
85+
file,
86+
args,
87+
{ cwd, maxBuffer: 8 * 1024 * 1024, env: childEnv({ NO_COLOR: '1' }) },
88+
(err, stdout, stderr) => {
89+
resolvePromise({
90+
// `err.code` is the real exit status; a signalled child has none and
91+
// is reported as 1, never as 0.
92+
code: err
93+
? typeof (err as { code?: unknown }).code === 'number'
94+
? (err as unknown as { code: number }).code
95+
: 1
96+
: 0,
97+
stdout: String(stdout),
98+
stderr: String(stderr),
99+
});
100+
},
101+
);
102+
});
103+
}
104+
105+
const os = (args: string[], cwd: string) => run(TSX, [CLI, ...args], cwd);
106+
const out = (r: Run) => r.stdout + r.stderr;
107+
108+
const target = (type: string) => {
109+
const t = GENERATOR_SCAFFOLD_TARGETS.find((g) => g.type === type);
110+
if (!t) throw new Error(`no '${type}' generator in the roster`);
111+
return t;
112+
};
113+
const OBJECT = target('object');
114+
const FLOW = target('flow');
115+
116+
let root: string;
117+
let project: string;
118+
let scaffold: Run;
119+
let genObject: Run;
120+
let genFlow: Run;
121+
let validate: Run;
122+
123+
beforeAll(async () => {
124+
root = mkdtempSync(join(HERE, '..', 'node_modules', '.create-objectstack-reach-'));
125+
project = join(root, PROJECT);
126+
scaffold = await run(process.execPath, [ON_RAMP_BIN, PROJECT, '--skip-install', '--skip-skills'], root);
127+
// Sequential on purpose: the object first, so the flow binds to something
128+
// declared, and cold starts in a container several agents share.
129+
genObject = await os(['g', 'object', STEM], project);
130+
genFlow = await os(['g', 'flow', STEM], project);
131+
validate = await os(['validate'], project);
132+
}, RUN_TIMEOUT_MS);
133+
134+
afterAll(() => {
135+
if (root) rmSync(root, { recursive: true, force: true });
136+
});
137+
138+
describe('[#20333] `npm create objectstack` → `os g flow` → `os validate`', () => {
139+
it('the on-ramp scaffolded the project, under the namespace this file assumes', () => {
140+
expect(scaffold.code, out(scaffold)).toBe(0);
141+
expect(readFileSync(join(project, 'objectstack.config.ts'), 'utf-8')).toContain(`namespace: '${NS}'`);
142+
});
143+
144+
it('CONTROL: `os g object` reaches the stack', () => {
145+
expect(genObject.code, out(genObject)).toBe(0);
146+
expect(genObject.stdout).toContain(`'${OBJECT.itemName(STEM, NS)}'`);
147+
expect(genObject.stdout).not.toContain(`import * as ${OBJECT.stackKey}`);
148+
});
149+
150+
it('`os g flow` reaches the stack, with no wiring lines to add', () => {
151+
expect(genFlow.code, out(genFlow)).toBe(0);
152+
expect(genFlow.stdout).toContain(`'${FLOW.itemName(STEM, NS)}'`);
153+
expect(genFlow.stdout).not.toContain(`import * as ${FLOW.stackKey}`);
154+
// Nor a `requires` line: the starter already declares what a flow runs on.
155+
expect(genFlow.stdout).not.toContain('requires: [');
156+
});
157+
158+
it('`os validate` exits 0 and counts the generated flow beside the control', () => {
159+
expect(validate.code, out(validate)).toBe(0);
160+
// The starter's own object plus the generated one.
161+
expect(validate.stdout).toMatch(/\bData: 2 Objects\b/);
162+
expect(validate.stdout).toMatch(/\bLogic: 1 Flows\b/);
163+
});
164+
});

0 commit comments

Comments
 (0)