Kaynağa Gözat

refactor(app-boot): drop the root Include lookup by id

reconcileProfilePatches finds the root Include only through the registry
the module that mounted it keeps, as it did before the Web page: a tree
mounted by another copy of app-boot was a test-harness situation, not a
product one. The web e2e scaffold now mounts and resolves the profile
through the built app-boot, loaded with Node's own require so it shares
the module map with the Host plugins the profile loads, instead of the
source copy a bare import resolves to.
Yichen Jiang 3 hafta önce
ebeveyn
işleme
c3c7a4f412

+ 11 - 16
apps/web/tests/scaffold.ts

@@ -56,21 +56,7 @@ import {
   writesCurrentSessionFixtures,
   type NormalizeContext,
 } from '@deepseek-ai/dsh-session-snapshot'
-import {
-  auditStartupEntries,
-  composeEntries,
-  createProfileResolutionGeneration,
-  healProfilesModuleFallback,
-  initProfile,
-  mountRootInclude,
-  readProfileManifest,
-  readProfilePatches,
-  loadOverlayPatches,
-  PluginPackages,
-  type Profile,
-  type ProfileContext,
-  type ProfileResolutionMode,
-} from '@deepseek-ai/dsh-app-boot'
+import type { Profile, ProfileContext, ProfileResolutionMode } from '@deepseek-ai/dsh-app-boot'
 import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
 import { LlmAdapter } from '@deepseek-ai/dsh-llm'
 import type {
@@ -96,7 +82,16 @@ import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 import type {} from '@deepseek-ai/dsh-host-webserver'
 import type {} from '@deepseek-ai/dsh-agent'
 import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
-import { REPO_ROOT, requireDist } from './support.ts'
+import { REPO_ROOT, requireBuilt, requireDist } from './support.ts'
+
+// The launcher's own module, as built: the manager and HMR plugins the profile
+// loads reload the tree through this copy's registry of the root Include, so
+// the scaffold mounts through the same copy rather than the source import.
+const appBoot = requireBuilt('@deepseek-ai/dsh-app-boot') as typeof import('@deepseek-ai/dsh-app-boot')
+const {
+  auditStartupEntries, composeEntries, createProfileResolutionGeneration, healProfilesModuleFallback, initProfile,
+  mountRootInclude, readProfileManifest, readProfilePatches, loadOverlayPatches, PluginPackages,
+} = appBoot
 
 // Host-side web e2e cannot import a browser package: doing so would pull that
 // package's complete TS project into this graph. Mirrored from

+ 20 - 0
apps/web/tests/support.ts

@@ -1,6 +1,7 @@
 // Shared plumbing for the web smoke tests (dist location, free port, failure shots).
 import { existsSync, mkdirSync } from 'node:fs'
 import { createServer } from 'node:net'
+import { createRequire } from 'node:module'
 import { join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import type { Browser, Locator, Page } from 'playwright'
@@ -10,6 +11,25 @@ export const DIST_INDEX = fileURLToPath(new URL('../dist/index.html', import.met
 
 export const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
 
+const installationRequire = createRequire(join(REPO_ROOT, 'apps/cli/package.json'))
+
+/**
+ * The built copy of a workspace package, as the dsh installation resolves it.
+ * The Host plugins a scaffold profile loads run from built packages through
+ * Node's own loader; a scaffold call that must share their module state
+ * (app-boot keeps the root Include it mounted per context) has to run that
+ * same copy, not the source a bare import gets through the tsconfig paths,
+ * and not the test runner's own inlined copy of the built file either.
+ * `require` of an ES module goes through Node's loader and shares its
+ * module map with the plugins' imports; it needs a graph without top-level
+ * await, which the built Host packages keep.
+ * @param name - the workspace package name.
+ * @returns the package's built module namespace, for the caller to type as the package's own.
+ */
+export function requireBuilt(name: string): unknown {
+  return installationRequire(name)
+}
+
 /**
  * Browser language a page must advertise to boot into the product's Chinese
  * surface: with no stored preference the client derives its initial locale

+ 2 - 2
docs/event-producer-consumer.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 docs/event-producer-consumer.md
-event-producer-consumer.md: c9ed2b3216525eefb5eee9861c3439b4475679a4
-event-producer-consumer.zh.md: 89bde0438d4d1ca68771f7d1d1c5f54aa3ecbf2a
+event-producer-consumer.md: 5759e484ba8f27965fec6ca1842758dc03724e37
+event-producer-consumer.zh.md: c9246d05332dc75cb9f27725ba726a4e7ce51564

+ 1 - 1
docs/event-producer-consumer.md

@@ -53,7 +53,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:191`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:197`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:203`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `profile/reconciled` | `emit` | [`packages/boot/app-boot/src/index.ts:255`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`plugin-manager`](../packages/boot/plugin-manager) |
+| `profile/reconciled` | `emit` | [`packages/boot/app-boot/src/index.ts:243`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`plugin-manager`](../packages/boot/plugin-manager) |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |

+ 1 - 1
docs/event-producer-consumer.zh.md

@@ -55,7 +55,7 @@
 | `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:191`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:197`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:203`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `profile/reconciled` | `emit` | [`packages/boot/app-boot/src/index.ts:255`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`plugin-manager`](../packages/boot/plugin-manager) |
+| `profile/reconciled` | `emit` | [`packages/boot/app-boot/src/index.ts:243`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`plugin-manager`](../packages/boot/plugin-manager) |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |

+ 2 - 14
packages/boot/app-boot/src/index.ts

@@ -232,18 +232,6 @@ export function loadLayeredEnv(
 
 const bootstrapIncludes = new WeakMap<Context, Entry>()
 
-/** The pinned id of the bootstrap Include: app glue, not a config row. */
-const ROOT_INCLUDE_ID = 'include'
-
-/**
- * The root Include entry of a booted tree: the one this module mounted, else
- * the entry carrying the pinned root id, which a tree mounted through another
- * copy of this module (a test harness importing sources beside built packages) has.
- */
-function rootIncludeOf(ctx: Context): Entry | undefined {
-  return bootstrapIncludes.get(ctx) ?? [...ctx.get('loader')?.entries() ?? []].find(entry => entry.id === ROOT_INCLUDE_ID)
-}
-
 declare module '@deepseek-ai/cordis' {
   interface Events {
     /**
@@ -273,7 +261,7 @@ const userPatchesSchema = entryListSchema
 export async function reconcileProfilePatches(
   ctx: Context, patches: PatchOptions[], binName: string, requiredIds: readonly string[] = [],
 ): Promise<string[]> {
-  const entry = rootIncludeOf(ctx)
+  const entry = bootstrapIncludes.get(ctx)
   if (entry === undefined) throw new Error(`${binName}: profile reload requires the root Include entry`)
   const previousFailures = (await inactiveEntries(ctx)).map(failure => ({
     ...failure, fiber: failure.entry.fiber, options: JSON.stringify(failure.entry.options),
@@ -563,7 +551,7 @@ export async function mountRootInclude(
     ...patches.length > 0 ? { patches: [...patches] } : {},
   }
   const rootInclude: EntryOptions = {
-    id: ROOT_INCLUDE_ID,
+    id: 'include',
     name: 'cordis:include',
     config: includeConfig,
   }

+ 1 - 14
packages/boot/app-boot/tests/user-patches.spec.ts

@@ -11,7 +11,7 @@ import { pathToFileURL } from 'node:url'
 import { afterAll, afterEach, describe, expect, it, onTestFinished } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import Include, { type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
-import Loader, { type EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
+import Loader from '@deepseek-ai/cordis-plugin-loader'
 import {
   boot,
   loadOptionalPatches,
@@ -340,19 +340,6 @@ describe('profile reconciliation settlement', () => {
     await expect(reconcileProfilePatches(ctx, [], NAME)).rejects.toThrow('profile reload requires the root Include entry')
   })
 
-  it('finds a root Include mounted under its pinned id by another copy of this module', async () => {
-    const dir = tmp()
-    writeFileSync(join(dir, 'cordis.yml'), '[]\n')
-    const ctx = new Context()
-    onTestFinished(() => ctx.fiber.dispose())
-    await ctx.plugin(Loader)
-    ctx.loader.builtins.include = Include
-    const root: EntryOptions = { id: 'include', name: 'cordis:include', config: { path: pathToFileURL(join(dir, 'cordis.yml')).href } }
-    await ctx.loader.create(root)
-    await ctx.loader.await()
-    await expect(reconcileProfilePatches(ctx, [], NAME)).resolves.toEqual([])
-  })
-
   it('removes a previously failed entry without reporting its old activation error', async () => {
     const dir = tmp()
     writeFileSync(join(dir, 'cordis.yml'), '[]\n')