@moxxy/plugin-subagents 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { AgentDef } from '@moxxy/sdk';
3
+ import { type AgentSpecInput, resolveSpec } from './dispatch-agent.js';
4
+
5
+ // The existing dispatch-agent.test.ts only exercises end-to-end spawning with
6
+ // `getAgent: () => undefined`, so the AgentDef-merge branch of resolveSpec is
7
+ // never reached. These pin the precedence directly (caller > kind > built-in
8
+ // DEFAULT) — the merge that a regression letting def win over the caller, or
9
+ // dropping the maxIterations passthrough, would silently break.
10
+
11
+ const fullDef: AgentDef = {
12
+ name: 'researcher',
13
+ description: 'deep research',
14
+ systemPrompt: 'def-prompt',
15
+ model: 'def-model',
16
+ mode: 'research',
17
+ allowedTools: ['Read', 'Grep'],
18
+ maxIterations: 12,
19
+ };
20
+
21
+ const depsWith = (def: AgentDef | undefined) => ({
22
+ getAgent: (name: string) => (name === 'researcher' ? def : undefined),
23
+ });
24
+
25
+ const input = (over: Partial<AgentSpecInput> = {}): AgentSpecInput => ({
26
+ prompt: 'do the thing',
27
+ ...over,
28
+ });
29
+
30
+ describe('resolveSpec', () => {
31
+ it('uses the kind def fields when the caller omits them', () => {
32
+ const spec = resolveSpec(input({ agentType: 'researcher' }), depsWith(fullDef));
33
+ expect(spec.systemPrompt).toBe('def-prompt');
34
+ expect(spec.model).toBe('def-model');
35
+ expect(spec.mode).toBe('research');
36
+ expect(spec.allowedTools).toEqual(['Read', 'Grep']);
37
+ expect(spec.agentType).toBe('researcher');
38
+ expect(spec.label).toBe('researcher'); // label defaults to def.name
39
+ });
40
+
41
+ it('lets caller fields override the kind def for each field', () => {
42
+ const spec = resolveSpec(
43
+ input({
44
+ agentType: 'researcher',
45
+ systemPrompt: 'caller-prompt',
46
+ model: 'caller-model',
47
+ mode: 'goal',
48
+ allowedTools: ['Bash'],
49
+ label: 'my-label',
50
+ }),
51
+ depsWith(fullDef),
52
+ );
53
+ expect(spec.systemPrompt).toBe('caller-prompt');
54
+ expect(spec.model).toBe('caller-model');
55
+ expect(spec.mode).toBe('goal');
56
+ expect(spec.allowedTools).toEqual(['Bash']);
57
+ expect(spec.label).toBe('my-label');
58
+ expect(spec.prompt).toBe('do the thing');
59
+ });
60
+
61
+ it('flows def.maxIterations through even though the input schema omits it', () => {
62
+ const spec = resolveSpec(input({ agentType: 'researcher' }), depsWith(fullDef));
63
+ expect(spec.maxIterations).toBe(12);
64
+ });
65
+
66
+ it('omits maxIterations when the def does not set one', () => {
67
+ const def: AgentDef = { name: 'researcher', description: 'x' };
68
+ const spec = resolveSpec(input({ agentType: 'researcher' }), depsWith(def));
69
+ expect('maxIterations' in spec).toBe(false);
70
+ });
71
+
72
+ it('falls back to DEFAULT_AGENT on an unknown agentType (label "default")', () => {
73
+ const spec = resolveSpec(input({ agentType: 'nonexistent' }), depsWith(fullDef));
74
+ expect(spec.label).toBe('default'); // DEFAULT_AGENT.name
75
+ expect(spec.systemPrompt).toBeUndefined();
76
+ expect(spec.model).toBeUndefined();
77
+ expect(spec.allowedTools).toBeUndefined();
78
+ // The requested kind string is echoed verbatim, even on fallback.
79
+ expect(spec.agentType).toBe('nonexistent');
80
+ });
81
+
82
+ it('defaults agentType to "default" when the caller omits it', () => {
83
+ const spec = resolveSpec(input(), depsWith(fullDef));
84
+ expect(spec.agentType).toBe('default');
85
+ expect(spec.label).toBe('default');
86
+ });
87
+ });
88
+
89
+ describe('resolveSpec — recursion fan-out guard', () => {
90
+ // Untrusted model output can fan out 8 children; if each child inherits the
91
+ // full registry (including dispatch_agent) the depth is unbounded (8^N).
92
+ const parentTools = ['Read', 'Bash', 'dispatch_agent'];
93
+ const depsGuarded = {
94
+ getAgent: () => undefined,
95
+ getToolNames: () => parentTools,
96
+ };
97
+
98
+ it('defaults an unrestricted child to the parent tools MINUS dispatch_agent', () => {
99
+ const spec = resolveSpec(input(), depsGuarded);
100
+ expect(spec.allowedTools).toEqual(['Read', 'Bash']);
101
+ expect(spec.allowedTools).not.toContain('dispatch_agent');
102
+ });
103
+
104
+ it('preserves full inheritance (undefined allowlist) when getToolNames is not wired', () => {
105
+ const spec = resolveSpec(input(), { getAgent: () => undefined });
106
+ expect(spec.allowedTools).toBeUndefined();
107
+ });
108
+
109
+ it('lets the caller explicitly re-grant dispatch_agent', () => {
110
+ const spec = resolveSpec(
111
+ input({ allowedTools: ['Read', 'dispatch_agent'] }),
112
+ depsGuarded,
113
+ );
114
+ expect(spec.allowedTools).toEqual(['Read', 'dispatch_agent']);
115
+ });
116
+
117
+ it('lets a kind re-grant dispatch_agent via its allowedTools', () => {
118
+ const def: AgentDef = {
119
+ name: 'recurser',
120
+ description: 'x',
121
+ allowedTools: ['dispatch_agent', 'Read'],
122
+ };
123
+ const spec = resolveSpec(input({ agentType: 'recurser' }), {
124
+ getAgent: (n: string) => (n === 'recurser' ? def : undefined),
125
+ getToolNames: () => parentTools,
126
+ });
127
+ expect(spec.allowedTools).toEqual(['dispatch_agent', 'Read']);
128
+ });
129
+ });
130
+
131
+ describe('resolveSpec — label de-duplication', () => {
132
+ const deps = depsWith(undefined); // unknown kind → DEFAULT_AGENT (name "default")
133
+
134
+ it('suffixes a 1-based index for same-kind siblings in a multi-agent batch', () => {
135
+ const a = resolveSpec(input(), deps, { index: 0, total: 3 });
136
+ const b = resolveSpec(input(), deps, { index: 1, total: 3 });
137
+ const c = resolveSpec(input(), deps, { index: 2, total: 3 });
138
+ expect([a.label, b.label, c.label]).toEqual(['default-1', 'default-2', 'default-3']);
139
+ });
140
+
141
+ it('keeps the bare kind name for a single-agent batch', () => {
142
+ const spec = resolveSpec(input(), deps, { index: 0, total: 1 });
143
+ expect(spec.label).toBe('default');
144
+ });
145
+
146
+ it('never overrides an explicit caller label', () => {
147
+ const spec = resolveSpec(input({ label: 'mine' }), deps, { index: 1, total: 4 });
148
+ expect(spec.label).toBe('mine');
149
+ });
150
+ });