fix review findings: don't treat disabled entries as load failures; scope the verify-package-paths lib skip to a real package root

assertEntriesLoaded() flagged ANY fiber-less entry as a failed import, but a
`disabled: true` entry settles without a fiber by design (Entry.refresh() skips
init() when disabled) — a valid "plugin off" config, not a broken import. Both
app bins now filter `fiber === undefined && !entry.disabled`. The stdio built-bin
smoke gains a disabled-(unresolvable)-entry config that must still boot.

The verify-package-paths lib-skip was unconditional and ran before the
moved-package check, so a stale group-less `packages/acp-agent/lib/bin.js` (the
exact drift this gate catches) was silently ignored just for containing `lib`.
Scope the skip: only exempt `lib` when it is the segment after an EXISTING
`packages/<group>/<pkg>` root, so a real-but-unbuilt `lib/bin.js` is still exempt
while a stale package path flags.
This commit is contained in:
Tianyi Cui
2026-06-21 19:47:22 +08:00
parent 9f5e2b352c
commit 4d7726ecd3
4 changed files with 49 additions and 13 deletions

View File

@@ -85,9 +85,13 @@ export function installFailLoud(): void {
* `fiber` and producing no rejection — so the process would otherwise exit 0. A
* started entry has a `fiber`; throw on any entry still missing one so `boot()`
* rejects.
*
* A `disabled` entry is the one legitimate fiber-less state: `Entry.refresh()`
* deliberately skips `init()` for it, so it settles without a fiber by design —
* a valid "plugin turned off" config, not a failed import. Exclude it.
*/
function assertEntriesLoaded(ctx: Context): void {
const failed = [...ctx.loader.entries()].filter(entry => entry.fiber === undefined)
const failed = [...ctx.loader.entries()].filter(entry => entry.fiber === undefined && !entry.disabled)
if (failed.length > 0) {
const names = failed.map(entry => entry.options.name).join(', ')
throw new Error(`dsh-acp-agent: plugin(s) failed to load: ${names} (see the error(s) logged above)`)

View File

@@ -66,9 +66,14 @@ export function installFailLoud(): void {
* entry with `fiber === undefined` after the tree settled never loaded. Throw on
* any such entry so `boot()` rejects (and the top-level `await` fails the process
* non-zero) instead of returning a half-empty context.
*
* A `disabled` entry is the one legitimate fiber-less state: `Entry.refresh()`
* deliberately skips `init()` for it, so it settles without a fiber by design.
* That is a valid config (a consumer turning an optional plugin off), not a
* failed import — exclude it so the guard catches only real load failures.
*/
function assertEntriesLoaded(ctx: Context): void {
const failed = [...ctx.loader.entries()].filter(entry => entry.fiber === undefined)
const failed = [...ctx.loader.entries()].filter(entry => entry.fiber === undefined && !entry.disabled)
if (failed.length > 0) {
const names = failed.map(entry => entry.options.name).join(', ')
throw new Error(`dsh-stdio-agent: plugin(s) failed to load: ${names} (see the error(s) logged above)`)

View File

@@ -53,8 +53,13 @@ async function pkgName(absDir: string): Promise<string> {
* Build a temp consumer dir: `node_modules` with the workspace + vendor packages
* symlinked in, a `src/` carrying the example mock backend, and a `cordis.yml`
* that wires them onto the stdio app. Returns the dir (caller removes it).
*
* `disabledBrokenEntry` appends an entry that points at a non-existent plugin but
* is marked `disabled: true`. The Loader leaves a disabled entry fiber-less by
* design, so it exercises that the fail-loud entry-load guard does NOT mistake a
* valid disabled entry for a failed import.
*/
async function makeConsumer(welcome: string): Promise<string> {
async function makeConsumer(welcome: string, disabledBrokenEntry = false): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'stdio-built-bin-'))
const nm = join(dir, 'node_modules')
for (const rel of dshPackages) {
@@ -88,6 +93,9 @@ async function makeConsumer(welcome: string): Promise<string> {
' model: mock-echo',
' systemPrompt: \'demo\'',
` welcome: '${welcome}'`,
...disabledBrokenEntry
? ['- id: off', ' name: \'./src/does-not-exist.ts\'', ' disabled: true']
: [],
'',
].join('\n'))
return dir
@@ -143,6 +151,18 @@ describe.skipIf(!existsSync(stdioBin))('dsh-stdio-agent BUILT bin (node lib/bin.
expect(code).toBe(0)
}, 30_000)
it('boots cleanly when the config disables an (otherwise unresolvable) entry', async () => {
// A `disabled: true` entry settles without a fiber by design; the fail-loud
// entry-load guard must NOT mistake it for a failed import. Even though its
// plugin path does not exist, the app boots and the round-trip works.
consumer = await makeConsumer('DISABLED-OK ready.', true)
const { stdout, code, stderr } = await runBuiltBin(consumer, './cordis.yml', 'echo hi')
expect(stderr).not.toContain('failed to load')
expect(stdout).toContain('DISABLED-OK ready.')
expect(stdout).toContain('[tool result] ECHO: HI')
expect(code).toBe(0)
}, 30_000)
it('fails LOUD (non-zero exit + stderr) on a config whose directory does not exist', async () => {
// A consumer who typos the config path must get a clear failure, not silent
// success. This dir does not exist, so the include PLUGIN itself fails to