Skip to content

Commit 1dbc718

Browse files
committed
tests
1 parent db8515d commit 1dbc718

3 files changed

Lines changed: 296 additions & 0 deletions

File tree

TODO.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,20 @@ tech[MCP tool] / human[operator]):
725725
- полный мультихоп в ОДНОЙ сессии: chat→sales→triage→tech→triage→operator;
726726
- два клиента проходят разные ветки конкурентно и изолированно.
727727

728+
## 31-й заход: вариативное ветвление навигаций (+6 тестов, багов не найдено)
729+
730+
Mesh-топология (каждый агент несёт nav-тулы ко всем остальным), а не звезда.
731+
Багов нет — фикс №34 (nav-тулы не дедуплицируются) уже сделал mesh рабочим.
732+
733+
### Новые тесты (6), сьют вырос до 293/293 — test/spec/navbranch.test.mjs
734+
- data-driven ветвление: один hub по разному вводу уходит к alpha/beta/gamma;
735+
- цепочка хопов между сообщениями: hub→alpha→beta→gamma→hub;
736+
- повторный вход: дойти до листа, вернуться в hub, взять ДРУГУЮ ветку (×3);
737+
- каскад вложенных навигаций в ОДНОМ ходу: hub→a→b→c (route накапливается за
738+
ход; getNavigationRoute чистится beginMonit на каждый complete — by design);
739+
- вариативный роут из решения ТУЛА: priority=high→gamma, low→alpha;
740+
- конкурентный fan-out: 3 клиента расходятся по разным листам изолированно.
741+
728742
## Найденные и исправленные баги (35 итого)
729743

730744
### 1. Дедлок waitForOutput при functools-kit v4 (причина 39 упавших тестов)

test/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import "./spec/finalguard.test.mjs";
4343
import "./spec/navprev.test.mjs";
4444
import "./spec/edgecases.test.mjs";
4545
import "./spec/integration.test.mjs";
46+
import "./spec/navbranch.test.mjs";
4647

4748
run(import.meta.url, () => {
4849
console.log("All tests are finished");

test/spec/navbranch.test.mjs

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { test } from "worker-testbed";
2+
3+
import {
4+
addAgent,
5+
addAgentNavigation,
6+
addCompletion,
7+
addSwarm,
8+
addTool,
9+
changeToAgent,
10+
commitToolOutput,
11+
executeForce,
12+
getAgentName,
13+
getNavigationRoute,
14+
session,
15+
setConfig,
16+
} from "../../build/index.mjs";
17+
import { randomString, sleep } from "functools-kit";
18+
19+
const HANG = Symbol("hang");
20+
const raceHang = (p, ms = 8000) => Promise.race([p, sleep(ms).then(() => HANG)]);
21+
22+
// Builds a fresh mesh of four agents, each carrying navigation tools to every
23+
// other. Routing is driven by an if-branching mock keyed on (agentName, text).
24+
// `p` prefixes all names so each worker test stays independent.
25+
const buildMesh = (p) => {
26+
const names = [`${p}-hub`, `${p}-alpha`, `${p}-beta`, `${p}-gamma`];
27+
const routeTable = {
28+
[`${p}-hub`]: {
29+
"to-alpha": `${p}-nav-alpha`,
30+
"to-beta": `${p}-nav-beta`,
31+
"to-gamma": `${p}-nav-gamma`,
32+
},
33+
[`${p}-alpha`]: { "hop-beta": `${p}-nav-beta` },
34+
[`${p}-beta`]: { "hop-gamma": `${p}-nav-gamma` },
35+
[`${p}-gamma`]: { "back-hub": `${p}-nav-hub` },
36+
};
37+
38+
const completion = addCompletion({
39+
completionName: `${p}-completion`,
40+
getCompletion: async ({ agentName, messages }) => {
41+
const text = (messages[messages.length - 1] ?? { content: "" }).content;
42+
const table = routeTable[agentName] ?? {};
43+
if (table[text]) {
44+
return { agentName, content: "", role: "assistant", tool_calls: [{ function: { name: table[text], arguments: {} } }] };
45+
}
46+
return { agentName, content: `${agentName}:${text}`, role: "assistant" };
47+
},
48+
});
49+
50+
const navTools = {
51+
[`${p}-nav-hub`]: `${p}-hub`,
52+
[`${p}-nav-alpha`]: `${p}-alpha`,
53+
[`${p}-nav-beta`]: `${p}-beta`,
54+
[`${p}-nav-gamma`]: `${p}-gamma`,
55+
};
56+
for (const [toolName, target] of Object.entries(navTools)) {
57+
addAgentNavigation({ toolName, description: `go ${target}`, navigateTo: target });
58+
}
59+
const allNavTools = Object.keys(navTools);
60+
for (const name of names) {
61+
addAgent({
62+
agentName: name,
63+
completion,
64+
prompt: `You are ${name}`,
65+
tools: allNavTools,
66+
dependsOn: names.filter((n) => n !== name),
67+
});
68+
}
69+
const swarm = addSwarm({ swarmName: `${p}-swarm`, agentList: names, defaultAgent: `${p}-hub` });
70+
return { swarm, names };
71+
};
72+
73+
test("Will branch to different agents from one hub by input", async ({ pass, fail }) => {
74+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
75+
const { swarm } = buildMesh("b1");
76+
77+
const cases = [
78+
["to-alpha", "b1-alpha"],
79+
["to-beta", "b1-beta"],
80+
["to-gamma", "b1-gamma"],
81+
];
82+
const results = [];
83+
for (const [input, expected] of cases) {
84+
const CLIENT_ID = randomString();
85+
const cs = session(CLIENT_ID, swarm);
86+
const answer = await raceHang(cs.complete(input));
87+
const active = await getAgentName(CLIENT_ID);
88+
await cs.dispose();
89+
results.push(active === expected && answer === `${expected}:${input}`);
90+
}
91+
92+
if (results.every(Boolean)) {
93+
pass();
94+
return;
95+
}
96+
fail(`results=${JSON.stringify(results)}`);
97+
});
98+
99+
test("Will follow chained hops across messages around the mesh", async ({ pass, fail }) => {
100+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
101+
const { swarm } = buildMesh("b2");
102+
103+
const CLIENT_ID = randomString();
104+
const cs = session(CLIENT_ID, swarm);
105+
await raceHang(cs.complete("to-alpha"));
106+
const a1 = await getAgentName(CLIENT_ID);
107+
await raceHang(cs.complete("hop-beta"));
108+
const a2 = await getAgentName(CLIENT_ID);
109+
await raceHang(cs.complete("hop-gamma"));
110+
const a3 = await getAgentName(CLIENT_ID);
111+
await raceHang(cs.complete("back-hub"));
112+
const a4 = await getAgentName(CLIENT_ID);
113+
await cs.dispose();
114+
115+
if (a1 === "b2-alpha" && a2 === "b2-beta" && a3 === "b2-gamma" && a4 === "b2-hub") {
116+
pass();
117+
return;
118+
}
119+
fail(`a1=${a1} a2=${a2} a3=${a3} a4=${a4}`);
120+
});
121+
122+
test("Will take different branches on re-entry to the hub", async ({ pass, fail }) => {
123+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
124+
const { swarm } = buildMesh("b3");
125+
126+
const CLIENT_ID = randomString();
127+
const cs = session(CLIENT_ID, swarm);
128+
await raceHang(cs.complete("to-alpha"));
129+
const first = await getAgentName(CLIENT_ID);
130+
await changeToAgent("b3-hub", CLIENT_ID);
131+
await raceHang(cs.complete("to-gamma"));
132+
const second = await getAgentName(CLIENT_ID);
133+
await changeToAgent("b3-hub", CLIENT_ID);
134+
await raceHang(cs.complete("to-beta"));
135+
const third = await getAgentName(CLIENT_ID);
136+
await cs.dispose();
137+
138+
if (first === "b3-alpha" && second === "b3-gamma" && third === "b3-beta") {
139+
pass();
140+
return;
141+
}
142+
fail(`first=${first} second=${second} third=${third}`);
143+
});
144+
145+
test("Will cascade nested navigations within one turn", async ({ pass, fail }) => {
146+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
147+
148+
addCompletion({
149+
completionName: "b4-completion",
150+
getCompletion: async ({ agentName, messages }) => {
151+
const t = (messages[messages.length - 1] ?? { content: "" }).content;
152+
if (agentName === "b4-hub" && t === "cascade") {
153+
return { agentName, content: "", role: "assistant", tool_calls: [{ function: { name: "b4-nav-a", arguments: {} } }] };
154+
}
155+
if (agentName === "b4-a" && t === "cascade") {
156+
return { agentName, content: "", role: "assistant", tool_calls: [{ function: { name: "b4-nav-b", arguments: {} } }] };
157+
}
158+
if (agentName === "b4-b" && t === "cascade") {
159+
return { agentName, content: "", role: "assistant", tool_calls: [{ function: { name: "b4-nav-c", arguments: {} } }] };
160+
}
161+
return { agentName, content: `${agentName}:${t}`, role: "assistant" };
162+
},
163+
});
164+
addAgentNavigation({ toolName: "b4-nav-a", description: "", navigateTo: "b4-a" });
165+
addAgentNavigation({ toolName: "b4-nav-b", description: "", navigateTo: "b4-b" });
166+
addAgentNavigation({ toolName: "b4-nav-c", description: "", navigateTo: "b4-c" });
167+
const names = ["b4-hub", "b4-a", "b4-b", "b4-c"];
168+
for (const name of names) {
169+
addAgent({
170+
agentName: name,
171+
completion: "b4-completion",
172+
prompt: "",
173+
tools: ["b4-nav-a", "b4-nav-b", "b4-nav-c"],
174+
dependsOn: names.filter((n) => n !== name),
175+
});
176+
}
177+
const swarm = addSwarm({ swarmName: "b4-swarm", agentList: names, defaultAgent: "b4-hub" });
178+
179+
const CLIENT_ID = randomString();
180+
const cs = session(CLIENT_ID, swarm);
181+
const answer = await raceHang(cs.complete("cascade"));
182+
const active = await getAgentName(CLIENT_ID);
183+
const route = [...getNavigationRoute(CLIENT_ID, swarm)];
184+
await cs.dispose();
185+
186+
const ok =
187+
active === "b4-c" &&
188+
answer === "b4-c:cascade" &&
189+
route.includes("b4-a") &&
190+
route.includes("b4-b") &&
191+
route.includes("b4-c");
192+
193+
if (ok) {
194+
pass();
195+
return;
196+
}
197+
fail(`active=${active} answer=${String(answer)} route=${JSON.stringify(route)}`);
198+
});
199+
200+
test("Will route variadically from a tool decision", async ({ pass, fail }) => {
201+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
202+
203+
addTool({
204+
toolName: "b5-smart-route",
205+
validate: () => true,
206+
type: "function",
207+
function: {
208+
name: "b5-smart-route",
209+
description: "Route based on priority",
210+
parameters: { type: "object", properties: { priority: { type: "string" } }, required: [] },
211+
},
212+
call: async ({ toolId, clientId, agentName, params }) => {
213+
const target = params.priority === "high" ? "b5-gamma" : "b5-alpha";
214+
await commitToolOutput(toolId, `routing to ${target}`, clientId, agentName);
215+
await changeToAgent(target, clientId);
216+
await executeForce("arrived", clientId);
217+
},
218+
});
219+
addCompletion({
220+
completionName: "b5-completion",
221+
getCompletion: async ({ agentName, messages }) => {
222+
const last = messages[messages.length - 1] ?? { content: "" };
223+
if (agentName === "b5-router" && last.content.startsWith("route:")) {
224+
const priority = last.content.slice("route:".length);
225+
return { agentName, content: "", role: "assistant", tool_calls: [{ function: { name: "b5-smart-route", arguments: { priority } } }] };
226+
}
227+
return { agentName, content: `${agentName}:${last.content}`, role: "assistant" };
228+
},
229+
});
230+
const ROUTER = addAgent({
231+
agentName: "b5-router",
232+
completion: "b5-completion",
233+
prompt: "",
234+
tools: ["b5-smart-route"],
235+
dependsOn: ["b5-alpha", "b5-gamma"],
236+
});
237+
const A = addAgent({ agentName: "b5-alpha", completion: "b5-completion", prompt: "" });
238+
const G = addAgent({ agentName: "b5-gamma", completion: "b5-completion", prompt: "" });
239+
const swarm = addSwarm({ swarmName: "b5-swarm", agentList: [ROUTER, A, G], defaultAgent: ROUTER });
240+
241+
const highClient = randomString();
242+
const csHigh = session(highClient, swarm);
243+
await raceHang(csHigh.complete("route:high"));
244+
const highTarget = await getAgentName(highClient);
245+
await csHigh.dispose();
246+
247+
const lowClient = randomString();
248+
const csLow = session(lowClient, swarm);
249+
await raceHang(csLow.complete("route:low"));
250+
const lowTarget = await getAgentName(lowClient);
251+
await csLow.dispose();
252+
253+
if (highTarget === "b5-gamma" && lowTarget === "b5-alpha") {
254+
pass();
255+
return;
256+
}
257+
fail(`highTarget=${highTarget} lowTarget=${lowTarget}`);
258+
});
259+
260+
test("Will fan out concurrent clients to distinct leaves", async ({ pass, fail }) => {
261+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
262+
const { swarm } = buildMesh("b6");
263+
264+
const inputs = ["to-alpha", "to-beta", "to-gamma"];
265+
const expected = ["b6-alpha", "b6-beta", "b6-gamma"];
266+
const clients = inputs.map(() => randomString());
267+
const sessions = clients.map((c) => session(c, swarm));
268+
const answers = await Promise.all(sessions.map((s, i) => raceHang(s.complete(inputs[i]))));
269+
const actives = await Promise.all(clients.map((c) => getAgentName(c)));
270+
await Promise.all(sessions.map((s) => s.dispose()));
271+
272+
const ok =
273+
actives.every((a, i) => a === expected[i]) &&
274+
answers.every((ans, i) => ans === `${expected[i]}:${inputs[i]}`);
275+
276+
if (ok) {
277+
pass();
278+
return;
279+
}
280+
fail(`actives=${JSON.stringify(actives)} answers=${JSON.stringify(answers.map(String))}`);
281+
});

0 commit comments

Comments
 (0)