From 4d92a61e00db8932227276f0caea182bc18ad399 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:15:30 +0800 Subject: [PATCH 1/3] test(e2e): reserve pro for adapter coverage --- .../subagent-claude-code/tests/real-deepseek.e2e.ts | 11 ++++++----- snapshots/acp/escalation-approved/cordis.yml | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts b/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts index 9a8b3c2716..5611e4fce2 100644 --- a/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts +++ b/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts @@ -21,6 +21,7 @@ import * as claudeCode from '../src/index.ts' const execFileAsync = promisify(execFile) const OFFICIAL_DEEPSEEK_BASE_URL = 'https://api.deepseek.com' +const DEEPSEEK_MODEL = 'deepseek-v4-flash' const sdkRoot = dirname(fileURLToPath( import.meta.resolve('@anthropic-ai/claude-agent-sdk'), )) @@ -90,11 +91,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)( const env = { ANTHROPIC_AUTH_TOKEN: apiKey, ANTHROPIC_BASE_URL: `${deepSeekBaseUrl()}/anthropic`, - ANTHROPIC_MODEL: 'deepseek-v4-pro[1m]', - ANTHROPIC_DEFAULT_OPUS_MODEL: 'deepseek-v4-pro[1m]', - ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-v4-pro[1m]', - ANTHROPIC_DEFAULT_HAIKU_MODEL: 'deepseek-v4-flash', - CLAUDE_CODE_SUBAGENT_MODEL: 'deepseek-v4-flash', + ANTHROPIC_MODEL: DEEPSEEK_MODEL, + ANTHROPIC_DEFAULT_OPUS_MODEL: DEEPSEEK_MODEL, + ANTHROPIC_DEFAULT_SONNET_MODEL: DEEPSEEK_MODEL, + ANTHROPIC_DEFAULT_HAIKU_MODEL: DEEPSEEK_MODEL, + CLAUDE_CODE_SUBAGENT_MODEL: DEEPSEEK_MODEL, CLAUDE_CODE_EFFORT_LEVEL: 'max', CLAUDE_CONFIG_DIR: claudeConfig, HOME: root, diff --git a/snapshots/acp/escalation-approved/cordis.yml b/snapshots/acp/escalation-approved/cordis.yml index e08f3005c3..cc8f9609f9 100644 --- a/snapshots/acp/escalation-approved/cordis.yml +++ b/snapshots/acp/escalation-approved/cordis.yml @@ -34,7 +34,7 @@ name: '@deepseek-ai/dsh-acp' config: provider: deepseek-official - model: deepseek-v4-pro + model: deepseek-v4-flash - id: system-prompt name: '@deepseek-ai/dsh-system-prompt' From 08bfeda7e8ec33cd25900d769f99b5e45b803c50 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:42:14 +0800 Subject: [PATCH 2/3] test(e2e): use Flash for live adapter coverage --- .../llm/llm-deepseek/tests/adapter.e2e.ts | 44 ++++++++----------- .../llm/llm-deepseek/tests/adapter.spec.ts | 4 +- packages/llm/llm-pi-ai/tests/adapter.e2e.ts | 38 +++++++++------- packages/llm/llm-pi-ai/tests/adapter.spec.ts | 4 +- 4 files changed, 44 insertions(+), 46 deletions(-) diff --git a/packages/llm/llm-deepseek/tests/adapter.e2e.ts b/packages/llm/llm-deepseek/tests/adapter.e2e.ts index cf3d511063..f39aebcc27 100644 --- a/packages/llm/llm-deepseek/tests/adapter.e2e.ts +++ b/packages/llm/llm-deepseek/tests/adapter.e2e.ts @@ -28,14 +28,13 @@ import type { Config } from '@deepseek-ai/dsh-llm-deepseek' import { assemble, type AssembledResult } from './assemble.ts' /** - * Real-API e2e for the direct-fetch adapter: V4 Flash + V4 Pro across - * thinking modes and all official effort levels. The suite skips entirely - * without $DEEPSEEK_API_KEY; the pre-release vision smoke additionally + * Real-API e2e for the direct-fetch adapter: V4 Flash across thinking modes + * and a max-effort tool round trip with reasoning passback. The suite skips + * entirely without $DEEPSEEK_API_KEY; the pre-release vision smoke additionally * requires $DEEPSEEK_VISION_E2E=1 (see vitest.e2e.config.ts). */ const FLASH = 'deepseek-v4-flash' -const PRO = 'deepseek-v4-pro' const VISION = 'deepseek-v4-flash-vision-exp' const VISION_E2E_ENABLED = process.env.DEEPSEEK_VISION_E2E === '1' const TEST_PNG = Uint8Array.from(readFileSync( @@ -266,20 +265,23 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () expect(withThinking.usage?.reasoningTokens).toBeGreaterThan(0) }) - it.each(['high', 'max'] as const)( - 'pro + thinking enabled (effort %s): tool-call round trip with reasoning passback', - async (effort) => { - const ctx = await harness(PRO, { thinking: 'enabled' }) + it( + 'flash + thinking enabled (effort max): tool-call round trip with reasoning passback', + async () => { + const ctx = await harness(FLASH, { thinking: 'enabled' }) // Turn 1: the model must call the tool (and think before it). const first = await assemble(ctx,{ - model: PRO, - reasoningEffort: ReasoningEffortId(effort), + model: FLASH, + reasoningEffort: ReasoningEffortId('max'), messages: ask('What is the weather in Paris right now? Use the get_weather tool.'), tools: [weatherTool], maxTokens: 2000, }) - expect(first.finish.kind).toBe('tool-calls') + expect( + first.finish.kind, + `DeepSeek Flash tool-call turn finished as ${JSON.stringify(first.finish)}`, + ).toBe('tool-calls') const call = first.message.content.find(block => block.type === 'tool-call') expect(call).toBeDefined() expect(call!.name).toBe('get_weather') @@ -288,8 +290,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () // Turn 2: send the tool result back WITH the assistant's reasoning // block in history (the official thinking+tools passback rule). const second = await assemble(ctx,{ - model: PRO, - reasoningEffort: ReasoningEffortId(effort), + model: FLASH, + reasoningEffort: ReasoningEffortId('max'), messages: [ ...ask('What is the weather in Paris right now? Use the get_weather tool.'), createMessage({ @@ -308,22 +310,14 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () tools: [weatherTool], maxTokens: 2000, }) - expect(second.finish.kind).toBe('stop') + expect( + second.finish.kind, + `DeepSeek Flash tool-result turn finished as ${JSON.stringify(second.finish)}`, + ).toBe('stop') expect(textOf(second).toLowerCase()).toMatch(/sunny|22/) }, ) - it('pro + thinking disabled: plain generation without reasoning blocks', async () => { - const ctx = await harness(PRO, { thinking: 'disabled' }) - const result = await assemble(ctx,{ - model: PRO, - messages: ask('Reply with exactly the word: pong'), - maxTokens: 50, - }) - expect(result.finish.kind).toBe('stop') - expect(result.message.content.some(block => block.type === 'reasoning')).toBe(false) - }) - it('streams raw chunks in protocol order', async () => { const ctx = await harness(FLASH, { thinking: 'disabled' }) const kinds: string[] = [] diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index 46d51efc43..39dd9e25b5 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -338,7 +338,7 @@ describe('DeepSeekAdapter against a mock server', () => { const ctx = await harness(server.url) const result = await assemble(ctx, { - model: 'deepseek-v4-flash', + model: 'deepseek-v4-pro', messages: [createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'plugin', plugin: 'test' }, @@ -350,7 +350,7 @@ describe('DeepSeekAdapter against a mock server', () => { // The wire request carried the auth header contents we configured. expect(server.requests[0]).toMatchObject({ - model: 'deepseek-v4-flash', + model: 'deepseek-v4-pro', max_tokens: 256_000, reasoning_effort: 'high', stream: true, diff --git a/packages/llm/llm-pi-ai/tests/adapter.e2e.ts b/packages/llm/llm-pi-ai/tests/adapter.e2e.ts index 99b2be8721..93f684eef1 100644 --- a/packages/llm/llm-pi-ai/tests/adapter.e2e.ts +++ b/packages/llm/llm-pi-ai/tests/adapter.e2e.ts @@ -8,14 +8,12 @@ import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import { assemble, type AssembledResult } from './assemble.ts' /** - * Real-API e2e for the pi-ai-backed adapter: V4 Flash + V4 Pro with provider - * defaults and representative off/high/max reasoning. Mirrors the native - * adapter's StreamChunk contract and exercises a replayed tool follow-up. - * Key-gated. + * Real-API e2e for the pi-ai-backed adapter: V4 Flash defaults and + * off/high/max reasoning. Mirrors the native adapter's StreamChunk contract + * and exercises a replayed tool follow-up. Key-gated. */ const FLASH = 'deepseek-v4-flash' -const PRO = 'deepseek-v4-pro' const contexts: Context[] = [] async function harness(_model: string, config: Partial = {}) { @@ -67,10 +65,10 @@ const weatherTool: ToolSchema = { } describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () => { - it.each([FLASH, PRO])('%s + provider-default reasoning: plain text generation', async (model) => { - const ctx = await harness(model) + it(`${FLASH} + provider-default reasoning: plain text generation`, async () => { + const ctx = await harness(FLASH) const result = await assemble(ctx,{ - model, + model: FLASH, messages: ask('Reply with exactly the word: pong'), maxTokens: 50, }) @@ -91,10 +89,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () => expect(textOf(result).toLowerCase()).toContain('pong') }) - it.each([FLASH, PRO])('%s + reasoning high: reasoning blocks present', async (model) => { - const ctx = await harness(model) + it(`${FLASH} + reasoning high: reasoning blocks present`, async () => { + const ctx = await harness(FLASH) const result = await assemble(ctx,{ - model, + model: FLASH, reasoningEffort: ReasoningEffortId('high'), messages: ask('Which is larger, 9.11 or 9.8? Answer with just the number.'), maxTokens: 2000, @@ -104,24 +102,27 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () => expect(textOf(result)).toContain('9.8') }) - it('pro + reasoning max: tool-call round trip', async () => { - const ctx = await harness(PRO) + it('flash + reasoning max: tool-call round trip', async () => { + const ctx = await harness(FLASH) const first = await assemble(ctx,{ - model: PRO, + model: FLASH, reasoningEffort: ReasoningEffortId('max'), messages: ask('What is the weather in Paris right now? Use the get_weather tool.'), tools: [weatherTool], maxTokens: 2000, }) - expect(first.finish.kind).toBe('tool-calls') + expect( + first.finish.kind, + `pi-ai Flash tool-call turn finished as ${JSON.stringify(first.finish)}`, + ).toBe('tool-calls') const call = first.message.content.find(block => block.type === 'tool-call') expect(call).toBeDefined() expect(call!.name).toBe('get_weather') expect(JSON.parse(call!.arguments)).toMatchObject({ city: expect.stringMatching(/paris/i) as string }) const second = await assemble(ctx,{ - model: PRO, + model: FLASH, reasoningEffort: ReasoningEffortId('max'), messages: [ ...ask('What is the weather in Paris right now? Use the get_weather tool.'), @@ -138,7 +139,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () => tools: [weatherTool], maxTokens: 2000, }) - expect(second.finish.kind).toBe('stop') + expect( + second.finish.kind, + `pi-ai Flash tool-result turn finished as ${JSON.stringify(second.finish)}`, + ).toBe('stop') expect(textOf(second).toLowerCase()).toMatch(/sunny|22/) }) diff --git a/packages/llm/llm-pi-ai/tests/adapter.spec.ts b/packages/llm/llm-pi-ai/tests/adapter.spec.ts index 9b35c6f285..cb2e71fad1 100644 --- a/packages/llm/llm-pi-ai/tests/adapter.spec.ts +++ b/packages/llm/llm-pi-ai/tests/adapter.spec.ts @@ -135,14 +135,14 @@ describe('PiAiAdapter provider routing', () => { thinkingBudgets: { high: 2048 }, }) await assemble(ctx, { - model: 'deepseek-v4-flash', + model: 'deepseek-v4-pro', messages: [], temperature: 0.2, maxTokens: 77, sessionId: 'session-for-pi' as never, }) expect(server.requests[0]).toMatchObject({ - model: 'deepseek-v4-flash', + model: 'deepseek-v4-pro', temperature: 0.2, max_tokens: 77, thinking: { type: 'enabled' }, From 675efe73f2d83202eccf145f8d9da14905c526d3 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:09:45 +0800 Subject: [PATCH 3/3] fix: node 24.9 internal issue --- .github/workflows/ci.yml | 13 ++++++++ .../tests/loader-shape.compat.spec.ts | 32 +++++++++++++++++++ vendor/README.md | 1 + vendor/loader/src/internal.ts | 26 +++++++++++---- 4 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 packages/boot/app-boot/tests/loader-shape.compat.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b0c625770..05a6ff2360 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -252,6 +252,13 @@ jobs: name: node 22.19 runner: ubuntu-latest gate_concurrency: '1' + # Pinned inside 24.0-24.11.1: those releases carry the v1 internal + # loader while reporting major 24, and every other job tracks the + # latest 24, which is v2. A bare `24` here would retest that same v2. + - node: '24.9' + name: node 24.9 + runner: ubuntu-latest + gate_concurrency: '1' - node: 26 name: node 26 runner: ubuntu-latest @@ -276,6 +283,12 @@ jobs: DSH_BUILD_CLIENT_PROFILE: official run: pnpm run check:node-compat + # Kept out of the gate aggregate: the shape a Node release carries only + # changes with the Node version, so this belongs to the version matrix + # rather than to every commit's checks. + - name: Check Loader internal shape detection + run: pnpm exec vitest run packages/boot/app-boot/tests/loader-shape.compat.spec.ts + python-sdk: if: github.event_name == 'pull_request' runs-on: ubuntu-latest diff --git a/packages/boot/app-boot/tests/loader-shape.compat.spec.ts b/packages/boot/app-boot/tests/loader-shape.compat.spec.ts new file mode 100644 index 0000000000..7364f3c73e --- /dev/null +++ b/packages/boot/app-boot/tests/loader-shape.compat.spec.ts @@ -0,0 +1,32 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { Context } from '@deepseek-ai/cordis' +import Loader from '@deepseek-ai/cordis-plugin-loader' +import { describe, expect, it } from 'vitest' + +describe('Loader internal shape detection', () => { + it('tags the running Node loader with the resolver signature that runtime accepts', async () => { + const dir = mkdtempSync(join(tmpdir(), 'dsh-loader-shape-')) + const baseUrl = pathToFileURL(dir).href + '/' + const ctx = new Context() + ctx.baseUrl = baseUrl + await ctx.plugin(Loader) + try { + const internal = ctx.loader.internal + expect(internal, 'Node module internals are unreachable; HMR reload and client-module resolution both need them').toBeDefined() + // Resolving through the tag is exactly what Hmr._resolve() and the + // client-modules registry do. A tag taken from the Node major instead of + // the loader's own API rejects every call on 24.0-24.11.1, which report + // major 24 while carrying the v1 loader: v2 arrived only in 24.12.0. + const resolved = internal!.version === 'v2' + ? internal!.resolveSync(baseUrl, { specifier: 'node:path', attributes: {} }) + : internal!.resolveSync('node:path', baseUrl, {}) + expect(resolved.url).toBe('node:path') + } finally { + await ctx.fiber.dispose() + rmSync(dir, { recursive: true, force: true }) + } + }) +}) diff --git a/vendor/README.md b/vendor/README.md index d587daf486..4cfbe892de 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -48,6 +48,7 @@ Keep this log exhaustive — every divergence from upstream must be listed. 16. **`cordis/package.json` publishes `src`**: added `src` to the `files` list, joining the other eight vendored packages. Cordis declares `"./src/*": "./src/*"` in its exports, so a tarball without `src` publishes an export map pointing at absent files; the release change judgement also reads `files` to decide whether a diff reaches the payload, and a package whose only published paths are build output has no tracked path to match. 17. **`@deepseek-ai` rescope**: every vendored manifest `name`, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table's `npm name` column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — `Symbol.for('schemastery')` and Schemastery's `vendor:` metadata field keep their upstream values. Re-apply with `pnpm run rescope-vendor --apply` after a sync; the table's two name columns are the mapping, restated for consumers in [docs/rescope.md](../docs/rescope.md). 18. **Entry `disabled` interpolation in `loader/src/config/entry.ts`**: a `disabled: !!js` expression evaluates against the loader context at every mount decision; the raw node stays in the options, so write-back keeps the `!!js` form. `disabled` is the only interpolated metadata field. Covered by `packages/boot/app-boot/tests/user-patches.spec.ts` and `apps/cli/tests/windows-shell.spec.ts`. +19. **`loader/src/internal.ts` runtime shape detection**: `ModuleLoader.fromInternal()` classifies the internal loader by which module-job API it owns — `getOrCreateModuleJob` for v2, `getModuleJobForImport` for v1 — instead of by Node major version. Upstream tags every major `>= 24` as v2, but the v2 interface arrived in Node 24.12.0, so 24.0–24.11.1 report major 24 while still carrying the v1 loader; consumers then called `resolveSync` with reversed parameters and every call threw. `dsh web` served an empty client graph (`__DSH_BOOT__.entries: []`) and HMR partial reload resolved no entry URL, both behind swallowed or warn-level errors. Arity cannot discriminate the two shapes, because each reports `resolveSync.length === 2`. A loader owning neither API is left unclassified rather than guessed, so consumers take their documented no-internals path. Covered on the `node-compat` Node version matrix, which pins 24.9 for the mistagged range. ## Sync procedure diff --git a/vendor/loader/src/internal.ts b/vendor/loader/src/internal.ts index ccf08debc6..322fbceb6a 100644 --- a/vendor/loader/src/internal.ts +++ b/vendor/loader/src/internal.ts @@ -117,16 +117,28 @@ export namespace ModuleLoader { } catch {} } + /** + * Locate and classify the running Node internal module loader. + * + * The shape is decided by which module-job API the loader owns, never by the + * Node version: v2 landed in 24.12.0, so a major-version test mistags every + * 24.0–24.11.1 loader as v2 and makes consumers call `resolveSync` with + * reversed parameters. Arity is not usable either — `resolveSync` reports 2 + * under both shapes. A loader owning neither API is left unclassified rather + * than guessed, so consumers take their documented no-internals path. + * @returns the classified loader, or `undefined` when none is reachable or its shape is unknown. + */ export function fromInternal(): ModuleLoader | undefined { if (_cachedLoader) return _cachedLoader const [major] = process.versions.node.split('.').map(Number) + if (major < 22) return - if (major >= 24) { - const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader() - if (raw) return _cachedLoader = Object.assign(raw, { version: 'v2' }) - } else if (major >= 22) { - const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader() - if (raw) return _cachedLoader = Object.assign(raw, { version: 'v1' }) - } + const raw = requireInternal('internal/modules/esm/loader')?.getOrInitializeCascadedLoader() + if (!raw) return + const version = typeof raw.getOrCreateModuleJob === 'function' + ? 'v2' + : typeof raw.getModuleJobForImport === 'function' ? 'v1' : undefined + if (!version) return + return _cachedLoader = Object.assign(raw, { version }) } }