Browse Source

fix(presets): isolate package lookup failures

imccyu 2 tuần trước cách đây
mục cha
commit
db098c8a24

+ 2 - 2
packages/preset/agent-presets/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/preset/agent-presets/README.md
-README.md: 07691afa0b0c37327e553df8e55d221bbb9a8bcf
-README.zh.md: 3fa2d633c0ffae84d843795a42ff6d3a5adcc39f
+README.md: d0eb1c108eaef091c776992c144ba389be2cf6c0
+README.zh.md: e9c1a35b22a1764e444c5757a1e34761bdc24fdf

+ 1 - 1
packages/preset/agent-presets/README.md

@@ -83,7 +83,7 @@ A session can switch to a different preset only while it has produced nothing 
 
 ### Failures and recovery
 
-A preset whose composition is missing, unparsable, not a list of named plugin rows, or naming a module that cannot be resolved is listed as broken with a reason naming the rows at fault; composing such a preset is refused up front, so a session never starts half-composed. What survives to session creation is a row whose module loads and then refuses — a plugin that throws, or one waiting for a service the composition never supplies — which fails the creation and rolls it back, naming every failed row including those inside a group. Fix the preset's file or delete it, then retry.
+A preset whose composition is missing, unparsable, not a list of named plugin rows, or naming a module that cannot be resolved is listed as broken with a reason naming the rows at fault; a package-lookup failure marks only the preset being checked as broken, while the rest of the roster remains available. Composing a broken preset is refused up front, so a session never starts half-composed. What survives to session creation is a row whose module loads and then refuses — a plugin that throws, or one waiting for a service the composition never supplies — which fails the creation and rolls it back, naming every failed row including those inside a group. Fix the preset's file or delete it, then retry.
 
 -----
 

+ 1 - 1
packages/preset/agent-presets/README.zh.md

@@ -83,7 +83,7 @@ agent-presets:
 
 ### 失败与恢复
 
-组装缺失、无法解析、不是具名插件行列表,或者引用了无法解析的模块的 preset 会被列为 broken,原因会指名出问题的行;组装此类 preset 会被提前拒绝,因此会话绝不会以半组装状态启动。能活到会话创建的,是模块能加载但随后拒绝的行——抛错的插件,或等待组装从未提供的服务的插件——它会让创建失败并回滚,且会指名每一个失败的行,包括组内的行。修复 preset 的文件或删除它,然后重试。
+组装缺失、无法解析、不是具名插件行列表,或者引用了无法解析的模块的 preset 会被列为 broken,原因会指名出问题的行;包查询失败只会把正在检查的 preset 标为 broken,名单中的其他 preset 仍然可用。组装 broken preset 会被提前拒绝,因此会话绝不会以半组装状态启动。能活到会话创建的,是模块能加载但随后拒绝的行——抛错的插件,或等待组装从未提供的服务的插件——它会让创建失败并回滚,且会指名每一个失败的行,包括组内的行。修复 preset 的文件或删除它,然后重试。
 
 -----
 

+ 9 - 1
packages/preset/agent-presets/src/discovery.ts

@@ -197,6 +197,8 @@ async function unresolvableRows(
  * loadable. Parsed with the loader's own YAML dialect ({@link entryListSchema},
  * the one carrying `!!js`), so health can never call a composition broken
  * that the loader would accept.
+ * A package-lookup failure becomes this composition's broken reason, so one
+ * preset cannot abort discovery of the rest of the roster.
  * @param path - absolute path of the composition file.
  * @param harnessBase - base URL a row's package name resolves against.
  * @returns one human-readable reason, or undefined when the file is loadable.
@@ -227,7 +229,13 @@ async function compositionProblem(
   // The composition's own directory, exactly as `Include` derives it, so a
   // row naming a file the preset ships resolves the way the mount will.
   const presetBase = new URL('.', pathToFileURL(path)).href
-  const unresolvable = await unresolvableRows(rows as readonly unknown[], presetBase, harnessBase, resolves)
+  let unresolvable: UnresolvableRow[]
+  try {
+    unresolvable = await unresolvableRows(rows as readonly unknown[], presetBase, harnessBase, resolves)
+  } catch (error) {
+    const full = error instanceof Error ? error.message : String(error)
+    return `the composition's plugins cannot be checked: ${full.replace(/\n[\s\S]*$/, '')}`
+  }
   const [first] = unresolvable
   if (first === undefined) return undefined
   if (unresolvable.length === 1) {

+ 32 - 0
packages/preset/agent-presets/tests/mount.spec.ts

@@ -396,6 +396,38 @@ describe('the preset roster', () => {
     expect(packageOf).toHaveBeenCalledWith('profile-package/plugin.js', pathToFileURL(FIXTURES).href + '/')
   })
 
+  it('isolates a package lookup failure to the preset being checked', async () => {
+    const root = await mkdtemp(join(tmpdir(), 'dsh-preset-package-failure-'))
+    roots.push(root)
+    await mkdir(join(root, 'healthy'))
+    await writeFile(join(root, 'healthy', COMPOSITION_FILE), '[]\n')
+    await mkdir(join(root, 'lookup-failure'))
+    await writeFile(
+      join(root, 'lookup-failure', COMPOSITION_FILE),
+      '- id: package\n  name: profile-package/plugin.js\n',
+    )
+    const scoped = await harness({
+      default: 'healthy', roots: [{ path: root, trust: 'user' }],
+      includeShippedRoot: false, includeUserRoot: false,
+    })
+    const packageOf = vi.spyOn(scoped.pluginPackages, 'packageOf').mockImplementation(() => {
+      throw new Error('profile package lookup failed')
+    })
+
+    const listed = await scoped.agentPresets.list()
+    expect(listed.find(preset => preset.id === 'lookup-failure')?.broken)
+      .toBe("the composition's plugins cannot be checked: profile package lookup failed")
+    expect(listed.find(preset => preset.id === 'healthy')?.broken).toBeUndefined()
+    await expect(agentOn(scoped, 'sess-after-package-failure')).resolves.toBeDefined()
+
+    packageOf.mockImplementation(() => {
+      throw 'raw profile package lookup failed'
+    })
+    const rawListed = await scoped.agentPresets.list()
+    expect(rawListed.find(preset => preset.id === 'lookup-failure')?.broken)
+      .toBe("the composition's plugins cannot be checked: raw profile package lookup failed")
+  })
+
   it('exposes the configured default id', () => {
     expect(ctx.agentPresets.defaultId).toBe('standard')
   })