import { describe, expect, it } from 'vitest' import { Context } from 'cordis' import SystemPrompt, { PromptAssembly, TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt' import type { ToolSchema } from '@deepseek-ai/dsh-llm' function tool(name: string, description = name): ToolSchema { return { name, description, parameters: { type: 'object', properties: {} } } } async function mount(config: { persona?: string; toolOrder?: string[] } = {}): Promise { const ctx = new Context() await ctx.plugin(SystemPrompt, config) return ctx } function names(assembly: PromptAssembly): string[] { return assembly.tools.map(t => t.name) } describe('SystemPrompt tool order', () => { // The ONE place the public constant's value is pinned; everything else // (tests and deployment configs alike) references TOOL_ORDER_REST. it('exports the rest entry as ""', () => { expect(TOOL_ORDER_REST).toBe('') }) it('assembles tools in lexicographic name order when no toolOrder is configured', async () => { const ctx = await mount() ctx.systemPrompt.tools(() => ({ schemas: [tool('charlie'), tool('alpha')] })) ctx.systemPrompt.tools(() => ({ schemas: [tool('bravo')] })) expect(names(await ctx.systemPrompt.assemble())).toEqual(['alpha', 'bravo', 'charlie']) }) it('assembles the same order regardless of provider registration order', async () => { const forward = await mount() forward.systemPrompt.tools(() => ({ schemas: [tool('alpha')] })) forward.systemPrompt.tools(() => ({ schemas: [tool('zulu')] })) const backward = await mount() backward.systemPrompt.tools(() => ({ schemas: [tool('zulu')] })) backward.systemPrompt.tools(() => ({ schemas: [tool('alpha')] })) expect(names(await forward.systemPrompt.assemble())).toEqual(['alpha', 'zulu']) expect(names(await backward.systemPrompt.assemble())).toEqual(['alpha', 'zulu']) }) it('applies a configured toolOrder: listed positions, rest at the rest entry lexicographically', async () => { const ctx = await mount({ toolOrder: ['todo_write', TOOL_ORDER_REST, 'bash'] }) ctx.systemPrompt.tools(() => ({ schemas: [tool('bash'), tool('echo_b'), tool('todo_write'), tool('echo_a')] })) expect(names(await ctx.systemPrompt.assemble())).toEqual(['todo_write', 'echo_a', 'echo_b', 'bash']) }) it('rejects the assembly when toolOrder names a tool that is not registered (misconfiguration blocks work)', async () => { const ctx = await mount({ toolOrder: ['todo_write', 'ghost', TOOL_ORDER_REST, 'wraith'] }) ctx.systemPrompt.tools(() => ({ schemas: [tool('bash'), tool('todo_write')] })) await expect(ctx.systemPrompt.assemble()).rejects.toThrow( 'toolOrder lists unregistered tools "ghost", "wraith"; known tools: bash, todo_write') }) it('names the single unregistered tool when no tools are registered at all', async () => { const ctx = await mount({ toolOrder: ['ghost', TOOL_ORDER_REST] }) await expect(ctx.systemPrompt.assemble()).rejects.toThrow( 'toolOrder lists unregistered tool "ghost"; known tools: (none)') }) it.each([ ['without an explicit toolOrder', undefined], ['with only the rest entry configured', [TOOL_ORDER_REST]], ])('rejects a provider tool named like the reserved rest entry %s', async (_case, toolOrder) => { const ctx = await mount(toolOrder === undefined ? {} : { toolOrder }) ctx.systemPrompt.tools(() => ({ schemas: [tool(TOOL_ORDER_REST)] })) await expect(ctx.systemPrompt.assemble()).rejects.toThrow( `tool provider returned reserved tool name "${TOOL_ORDER_REST}"`) }) it('keeps collection order between tools that share a name (stable sort)', async () => { const ctx = await mount() ctx.systemPrompt.tools(() => ({ schemas: [tool('dup', 'first'), tool('anchor'), tool('dup', 'second')] })) const assembly = await ctx.systemPrompt.assemble() expect(assembly.tools.map(t => t.description)).toEqual(['anchor', 'first', 'second']) }) it('canonicalizes BEFORE the assemble waterfall: listeners see the ordered list and own their own edits', async () => { const ctx = await mount() ctx.systemPrompt.tools(() => ({ schemas: [tool('zulu'), tool('alpha')] })) let seen: string[] | undefined ctx.on('system-prompt/assemble', function (assembly, _context, next) { seen = assembly.tools.map(t => t.name) // A listener-appended tool is NOT re-sorted — same contract as sections: // canonicalization applies to what the registry contributed, and a // listener owns the determinism of what it emits. assembly.tools.push(tool('aardvark')) return next() }) const assembly = await ctx.systemPrompt.assemble() expect(seen).toEqual(['alpha', 'zulu']) expect(names(assembly)).toEqual(['alpha', 'zulu', 'aardvark']) }) it.each([ ['an empty list', []], ['a list without the rest entry', ['bash', 'todo_write']], ])('rejects %s at load (the rest entry is required)', async (_case, toolOrder) => { await expect(new Context().plugin(SystemPrompt, { toolOrder })).rejects.toThrow(`must contain the "${TOOL_ORDER_REST}" rest entry`) }) it.each([ ['a duplicate tool name', ['bash', 'bash', TOOL_ORDER_REST]], ['a duplicate rest entry', [TOOL_ORDER_REST, 'bash', TOOL_ORDER_REST]], ])('rejects %s at load', async (_case, toolOrder) => { await expect(new Context().plugin(SystemPrompt, { toolOrder })).rejects.toThrow('more than once') }) it('throws from direct construction too', () => { expect(() => new SystemPrompt(new Context(), { toolOrder: ['bash'] })).toThrow('rest entry') }) })