Files
deepseek-harness/packages/core/system-prompt/tests/scoped.spec.ts
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

210 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { createScope, scopeOf } from '@deepseek-ai/dsh-scope'
import type { Scope, ScopeKey } from '@deepseek-ai/dsh-scope'
import SystemPrompt, { TOOL_ORDER_REST, renderContextSnapshot, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import type { Config, PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
async function mount(config: Config = {}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt, config)
return ctx
}
async function mintScope(ctx: Context, name: string): Promise<Scope> {
let scope!: Scope
// The scoped context resolves services through the MINTING plugin's
// dependency chain — the minter must inject what scope holders will reach.
await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, { name }) },
{ inject: ['systemPrompt'] }))
return scope
}
const schema = (name: string) => ({ name, description: `tool ${name}`, parameters: {} })
/** The key a test scope was minted with (scopeOf over the scope's own ctx). */
function scopeKeyOf(scope: Scope): ScopeKey {
// scopeOf never answers undefined for a context the scope itself minted.
return scopeOf(scope.ctx)!
}
describe('scoped sections', () => {
it('a scoped persona shadows deployment:persona for that scope only (either order)', async () => {
const ctx = await mount({ persona: 'You are the deployment.' })
const scope = await mintScope(ctx, 'child')
scope.ctx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: 'You run tests.' })
const scoped = renderPrompt(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) }))
const global = renderPrompt(await ctx.systemPrompt.assemble())
expect(scoped).toContain('You run tests.')
expect(scoped).not.toContain('You are the deployment.')
expect(global).toContain('You are the deployment.')
expect(global).not.toContain('You run tests.')
})
it('scoped-only sections join that scope alone; disposal removes them', async () => {
const ctx = await mount()
const scope = await mintScope(ctx, 'child')
scope.ctx.systemPrompt.section({ name: 'child:extra', order: 50, text: 'Extra guidance.' })
expect(renderPrompt(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) }))).toContain('Extra guidance.')
expect(renderPrompt(await ctx.systemPrompt.assemble())).not.toContain('Extra guidance.')
await scope.dispose()
expect(renderPrompt(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) }))).not.toContain('Extra guidance.')
})
it('duplicate names throw per layer, naming agent.ctx for the global case', async () => {
const ctx = await mount()
const scope = await mintScope(ctx, 'child')
ctx.systemPrompt.section({ name: 'x', order: 1, text: 'a' })
expect(() => ctx.systemPrompt.section({ name: 'x', order: 1, text: 'b' })).toThrow(/agent\.ctx/)
scope.ctx.systemPrompt.section({ name: 'y', order: 1, text: 'a' })
expect(() => scope.ctx.systemPrompt.section({ name: 'y', order: 1, text: 'b' })).toThrow(/already registered in this scope/)
})
it('shadows a global section before evaluating either text provider', async () => {
const ctx = await mount()
const scope = await mintScope(ctx, 'child')
const globalText = vi.fn(() => 'global text')
const scopedText = vi.fn(() => 'scoped text')
ctx.systemPrompt.section({ name: 'shared', order: 1, text: globalText })
scope.ctx.systemPrompt.section({ name: 'shared', order: 1, text: scopedText })
const assembly = await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })
expect(assembly.sections.find(section => section.name === 'shared')?.text).toBe('scoped text')
expect(globalText).not.toHaveBeenCalled()
expect(scopedText).toHaveBeenCalledOnce()
})
})
describe('scoped variables', () => {
it('a scoped variable shadows its global name-twin for that scope', async () => {
const ctx = await mount({ persona: 'Mode: {{mode}}.' })
const scope = await mintScope(ctx, 'child')
ctx.systemPrompt.variable('mode', () => 'normal')
scope.ctx.systemPrompt.variable('mode', () => 'strict')
expect(renderPrompt(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) }))).toContain('Mode: strict.')
expect(renderPrompt(await ctx.systemPrompt.assemble())).toContain('Mode: normal.')
})
it('same-layer duplicates throw; scoped layer cleans up on dispose', async () => {
const ctx = await mount()
const scope = await mintScope(ctx, 'child')
scope.ctx.systemPrompt.variable('v', () => '1')
expect(() => scope.ctx.systemPrompt.variable('v', () => '2')).toThrow(/already registered in this scope/)
await scope.dispose()
// Re-minting a scope with the SAME key starts clean.
const again = await mintScope(ctx, 'child2')
again.ctx.systemPrompt.variable('v', () => '3')
})
it('defers a scoped variable that replaces the last provider in its generation', async () => {
const ctx = await mount({ persona: 'Mode: {{mode}}.' })
const scope = await mintScope(ctx, 'child')
const key = scopeKeyOf(scope)
const calls: string[] = []
scope.ctx.systemPrompt.section({ name: 'scope:sibling', order: 1, text: 'Scoped.' })
const dispose = scope.ctx.systemPrompt.variable('mode', () => {
calls.push('first')
dispose()
scope.ctx.systemPrompt.variable('mode', () => {
calls.push('replacement')
return 'replacement'
})
return 'first'
})
expect(renderPrompt(await ctx.systemPrompt.assemble({ scope: key }))).toContain('Mode: first.')
expect(calls).toEqual(['first'])
expect(renderPrompt(await ctx.systemPrompt.assemble({ scope: key }))).toContain('Mode: replacement.')
expect(calls).toEqual(['first', 'replacement'])
})
})
describe('scoped cache-safe context', () => {
it('shadows a global context for one scope and cleans up with that scope', async () => {
const ctx = await mount()
const scope = await mintScope(ctx, 'child-context')
ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' })
scope.ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'scoped policy' })
expect(() => scope.ctx.systemPrompt.context({ name: 'policy', order: 2, text: 'duplicate' }))
.toThrow('prompt context "policy" is already registered in this scope')
expect(renderContextSnapshot(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })))
.toContain('scoped policy')
expect(renderContextSnapshot(await ctx.systemPrompt.assemble())).toContain('global policy')
await scope.dispose()
expect(renderContextSnapshot(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })))
.toContain('global policy')
})
})
describe('scoped tool providers and toolOrder × restriction', () => {
it('scoped providers are consulted only for their scope', async () => {
const ctx = await mount()
const scope = await mintScope(ctx, 'child')
ctx.systemPrompt.tools(() => ({ schemas: [schema('global_tool')] }))
scope.ctx.systemPrompt.tools(() => ({ schemas: [schema('scoped_tool')] }))
const scoped = await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })
const global = await ctx.systemPrompt.assemble()
expect(scoped.tools.map(t => t.name)).toEqual(['global_tool', 'scoped_tool'])
expect(global.tools.map(t => t.name)).toEqual(['global_tool'])
})
it('disposing a scoped tool provider empties its layer without residue', async () => {
const ctx = await mount()
const scope = await mintScope(ctx, 'child')
const dispose = scope.ctx.systemPrompt.tools(() => ({ schemas: [schema('scoped_tool')] }))
dispose()
const after = await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })
expect(after.tools.map(t => t.name)).toEqual([])
// Re-registering through the same scope starts a fresh layer.
scope.ctx.systemPrompt.tools(() => ({ schemas: [schema('again')] }))
const again = await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })
expect(again.tools.map(t => t.name)).toEqual(['again'])
})
it('a toolOrder entry restricted away for a scope is a normal absence, while a typo still throws', async () => {
const ctx = await mount({ toolOrder: ['bash', TOOL_ORDER_REST] })
// A provider mimicking the registry's restriction split: bash exists
// (knownNames) but is masked for this assembly (schemas).
ctx.systemPrompt.tools(() => ({
schemas: [schema('read')],
knownNames: ['read', 'bash'],
}))
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.tools.map(t => t.name)).toEqual(['read'])
const bad = await mount({ toolOrder: ['basj', TOOL_ORDER_REST] })
bad.systemPrompt.tools(() => ({ schemas: [schema('read')], knownNames: ['read', 'bash'] }))
await expect(bad.systemPrompt.assemble()).rejects.toThrow('toolOrder lists unregistered tool "basj"; known tools: bash, read')
})
})
describe('scoped assemble dispatch', () => {
it('an agent.ctx assemble listener shapes only its own scope\'s assemblies', async () => {
const ctx = await mount()
const scope = await mintScope(ctx, 'child')
const shaped: (ScopeKey | undefined)[] = []
scope.ctx.on('system-prompt/assemble', async (_assembly: PromptAssembly, context, next: () => Promise<PromptAssembly>) => {
shaped.push(context.scope)
const result = await next()
result.sections.push({ name: 'listener:extra', text: 'listener text' })
return result
})
const scoped = await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })
const global = await ctx.systemPrompt.assemble()
expect(scoped.sections.some(s => s.name === 'listener:extra')).toBe(true)
expect(global.sections.some(s => s.name === 'listener:extra')).toBe(false)
expect(shaped).toHaveLength(1)
})
})