Digital-Alchemy-TS / Digital-Alchemy-TS/core
LibraryGroup (rollup) wiring crashes under TestRunner — addLibrary missing isRollup guard on depends path
- Dominant language
- TypeScript
- Stars
- 53
- Forks
- 1
- Avg merge
- 1m
- Merged PRs (30d)
- 1
Description
## Summary
`LibraryGroup({ members })` works correctly when placed directly in `CreateApplication({ libraries: [...] }).bootstrap()`, but crashes with `TypeError: i[WIRE_PROJECT] is not a function` when the same app is handed to `TestRunner({ target: app })`.
## Affected version
`@digital-alchemy/core` **26.6.21** (peer `@digital-alchemy/symbols` 26.6.20)
Runtime: node v24.17.0
## Symptom
```
[INFO] (resolveLibraryMembership): tr_group auto-pulled into membership by tr_app
>>>MARKER<<< name=tr_lib_a stage=construct ← members start wiring...
bootstrap failed
TypeError: i[WIRE_PROJECT] is not a function
at .../src/services/wiring.service.mts:824:28 (sourcemap)
at eachSeries (.../src/helpers/async.mts:57:11)
at async bootstrap (.../src/services/wiring.service.mts:821:5)
```
Boots fine via a direct `CreateApplication(...).bootstrap()`. Broken only when the same app definition is fed to `TestRunner`.
## Why TestRunner triggers it
`TestRunner.buildApp` does not keep the target app's `libraries` as top-level libraries. It re-expresses them as the `depends` of a synthesized carrier library (`dist/testing/test-module.mjs` lines 43–48, 53, 66–76):
1. `getLibraries(target)` returns `target.libraries` — which here is `[group]`, the rollup.
2. The target is wrapped as `CreateLibrary({ depends: [group], … })`.
So at bootstrap, the rollup is sitting on a `depends` edge rather than a top-level `libraries` position.
## Root cause
In `src/helpers/wiring.mts` (dist: `dist/helpers/wiring.mjs`), inside `flattenLibraries` → the hoisted `addLibrary` walker, the `depends`/closure-as-membership walk (roughly lines 227–230 of the dist) **lacks the `isRollup` guard**:
```js
// dist/helpers/wiring.mjs ~227-230 — NO isRollup guard
const deps = lib.depends ?? [];
if (!is.empty(deps)) {
deps.forEach(dep => addLibrary(dep, `${path} -> depends(${lib.name})`, true, lib.name));
}
```
`addLibrary` (line 204) treats its argument as a plain library — it records `lib.name`, then `seen.add(lib); out.push(lib)` — so the rollup carrier is pushed into the wired membership set. Bootstrap then iterates membership and calls `[WIRE_PROJECT]` on every entry. A rollup carrier has `[IS_ROLLUP]` but **no `[WIRE_PROJECT]`**, producing the TypeError.
## The asymmetry
Both other expansion paths guard against this correctly by routing through `visit`, which calls `isRollup(entry)` before deciding whether to expand or wire:
- **Top-level** declared libraries — `declared.forEach(... visit(entry, …))` (~lines 256–263)
- **`implies`** bundles — `implied.forEach(member => visit(member, …))` (~line 237)
Only the **`depends` walk** calls `addLibrary` directly, bypassing the guard. The guard already exists in a sibling function: `addCarrierDepends` (dist ~lines 140–151) does `if (isRollup(member))` before recursing — the check simply went missing in `flattenLibraries`'s depends walk.
## Proposed minimal fix
Route a rollup dep through the rollup-safe `visit` path, keeping closure semantics (`fromClosure=true`, `puller`) for plain-library deps. Both `isRollup` and `visit` are already in scope in the same module.
**Before** (`src/helpers/wiring.mts`, deps walk inside `flattenLibraries → addLibrary`):
```ts
const deps = lib.depends ?? [];
if (!is.empty(deps)) {
deps.forEach(dep => addLibrary(dep, `${path} -> depends(${lib.name})`, true, lib.name));
}
```
**After:**
```ts
const deps = lib.depends ?? [];
if (!is.empty(deps)) {
deps.forEach(dep => {
// A rollup reached via a `depends` edge must be expanded into its members
// via the rollup-safe `visit` path — a rollup carrier has no [WIRE_PROJECT].
// Mirrors the top-level and `implies` expansion paths.
if (isRollup(dep)) {
visit(dep, `${path} -> depends(${lib.name})`);
return;
}
addLibrary(dep, `${path} -> depends(${lib.name})`, true, lib.name);
});
}
```
Alternative (smaller diff): add the guard at the **top of `addLibrary`** — `if (isRollup(lib)) { visit(lib, path); return; }` immediately before `note(lib.name, path)`. This catches the rollup-in-`depends` case (the only unguarded entry into `addLibrary`).
## Suggested test
A unit test in `testing/wiring.spec.mts` (or equivalent): a library with `depends: [LibraryGroup({ members: [a, b, c] })]` must flatten to the individual members `[a, b, c]`, never to the rollup carrier.
Also verify via `TestRunner`: an app with `libraries: [LibraryGroup({ members })]` passed to `TestRunner({ target: app })` should boot all members without throwing.
## Workaround (until fixed)
Carry the `LibraryGroup` on a library's **`implies`** instead of an app's top-level `libraries`. The `implies` path routes through `visit` and is rollup-safe:
```ts
// Safe under TestRunner
const carrier = CreateLibrary({ name: "carrier", implies: [LibraryGroup({ name: "group", members: [a, b, c] })] });
const app = CreateApplication({ libraries: [carrier] });
TestRunner({ target: app }); // works
```
Contributor guide
Research direction
Start in src/helpers/wiring.mts at flattenLibraries and its depends walk, then compare the top-level and implies paths that use visit. Add or verify a regression in testing/wiring.spec.mts for a depends edge containing LibraryGroup, and run it with the TestRunner case; done means the group expands to its members without a WIRE_PROJECT error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- backend, testing
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100