Digital-Alchemy-TS / Digital-Alchemy-TS/core

LibraryGroup (rollup) wiring crashes under TestRunner — addLibrary missing isRollup guard on depends path

Open Beginner friendly
#341 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.