Skip to content

Commit 6aa767e

Browse files
committed
wip
1 parent 0f5845b commit 6aa767e

3 files changed

Lines changed: 307 additions & 0 deletions

File tree

TODO.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,21 @@ complete=HANG)
470470
между ними по-прежнему возвращают одинаковое значение — метод вычисляет
471471
«следующий свободный» от ТЕКУЩИХ данных и не резервирует его.
472472

473+
## 22-й заход: полный контракт бан-механизма (+8 тестов, багов не найдено)
474+
475+
### Новые тесты (8), сьют вырос до 242/242 — test/spec/banhammer.test.mjs
476+
- autoBan на провале validateInput: первый «bad» возвращает banMessage и баннит
477+
навсегда (hasBan=true, последующие валидные сообщения тоже блокированы);
478+
- политика без banMessage → глобальный дефолт CC_BANHAMMER_PLACEHOLDER
479+
(«I am not going to discuss it!»), контракт «всегда строка»;
480+
- autoBan на провале validateOutput: утечка в выводе баннит клиента;
481+
- makeConnection: забаненному клиенту banMessage доставляется через connector;
482+
- unbanClient после autoBan полностью восстанавливает флоу;
483+
- getBanMessage вернувший null → фолбэк на banMessage;
484+
- бан-чек идёт РАНЬШЕ validateInput (шпион: 0 вызовов у забаненного);
485+
- MergePolicy: бан живёт в одной политике (hasBan per-policy), блокирует весь
486+
вход, unban именно этой политики восстанавливает.
487+
473488
## Найденные и исправленные баги (24 итого)
474489

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

test/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import "./spec/multiclient.test.mjs";
3434
import "./spec/doublesend.test.mjs";
3535
import "./spec/lifecycle.test.mjs";
3636
import "./spec/numindex.test.mjs";
37+
import "./spec/banhammer.test.mjs";
3738

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

test/spec/banhammer.test.mjs

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import { test } from "worker-testbed";
2+
3+
import {
4+
addAgent,
5+
addCompletion,
6+
addPolicy,
7+
addSwarm,
8+
makeConnection,
9+
session,
10+
setConfig,
11+
Policy,
12+
} from "../../build/index.mjs";
13+
import { randomString, sleep } from "functools-kit";
14+
15+
const addEcho = (name) =>
16+
addCompletion({
17+
completionName: name,
18+
getCompletion: async ({ agentName, messages }) => {
19+
const [last] = messages.slice(-1);
20+
return { agentName, content: `echo:${last.content}`, role: "assistant" };
21+
},
22+
});
23+
24+
test("Will auto-ban client on input validation failure", async ({ pass, fail }) => {
25+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
26+
27+
const POLICY = addPolicy({
28+
policyName: "b1-policy",
29+
persist: false,
30+
autoBan: true,
31+
banMessage: "AUTO-BANNED",
32+
validateInput: async (incoming) => incoming !== "bad",
33+
});
34+
const C = addEcho("b1-completion");
35+
const AGENT = addAgent({ agentName: "b1-agent", completion: C, prompt: "" });
36+
const SWARM = addSwarm({ swarmName: "b1-swarm", agentList: [AGENT], defaultAgent: AGENT, policies: [POLICY] });
37+
38+
const cid = randomString();
39+
const cs = session(cid, SWARM);
40+
const beforeBan = await cs.complete("hello");
41+
const bad = await cs.complete("bad");
42+
const bannedNow = await Policy.hasBan({ clientId: cid, swarmName: SWARM, policyName: POLICY });
43+
const afterBan = await cs.complete("hello");
44+
await cs.dispose();
45+
46+
const ok =
47+
beforeBan === "echo:hello" && bad === "AUTO-BANNED" && bannedNow === true && afterBan === "AUTO-BANNED";
48+
49+
if (ok) {
50+
pass();
51+
return;
52+
}
53+
fail(`before=${beforeBan} bad=${bad} banned=${bannedNow} after=${afterBan}`);
54+
});
55+
56+
test("Will use banhammer placeholder when banMessage is missing", async ({ pass, fail }) => {
57+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
58+
59+
const POLICY = addPolicy({
60+
policyName: "b2-policy",
61+
persist: false,
62+
validateInput: async (incoming) => incoming !== "bad",
63+
});
64+
const C = addEcho("b2-completion");
65+
const AGENT = addAgent({ agentName: "b2-agent", completion: C, prompt: "" });
66+
const SWARM = addSwarm({ swarmName: "b2-swarm", agentList: [AGENT], defaultAgent: AGENT, policies: [POLICY] });
67+
68+
const cid = randomString();
69+
const cs = session(cid, SWARM);
70+
const defaultMessage = await cs.complete("bad");
71+
await cs.dispose();
72+
73+
const ok =
74+
defaultMessage === "I am not going to discuss it!";
75+
76+
if (ok) {
77+
pass();
78+
return;
79+
}
80+
fail(`message=${JSON.stringify(defaultMessage)}`);
81+
});
82+
83+
test("Will auto-ban client on output validation failure", async ({ pass, fail }) => {
84+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
85+
86+
const POLICY = addPolicy({
87+
policyName: "b3-policy",
88+
persist: false,
89+
autoBan: true,
90+
banMessage: "OUTPUT-BANNED",
91+
validateOutput: async (outgoing) => !outgoing.includes("secret"),
92+
});
93+
addCompletion({
94+
completionName: "b3-completion",
95+
getCompletion: async ({ agentName, messages }) => {
96+
const [last] = messages.slice(-1);
97+
if (last.content === "leak") {
98+
return { agentName, content: "the-secret-data", role: "assistant" };
99+
}
100+
return { agentName, content: `echo:${last.content}`, role: "assistant" };
101+
},
102+
});
103+
const AGENT = addAgent({ agentName: "b3-agent", completion: "b3-completion", prompt: "" });
104+
const SWARM = addSwarm({ swarmName: "b3-swarm", agentList: [AGENT], defaultAgent: AGENT, policies: [POLICY] });
105+
106+
const cid = randomString();
107+
const cs = session(cid, SWARM);
108+
const leak = await cs.complete("leak");
109+
const bannedNow = await Policy.hasBan({ clientId: cid, swarmName: SWARM, policyName: POLICY });
110+
const afterBan = await cs.complete("hello");
111+
await cs.dispose();
112+
113+
const ok =
114+
leak === "OUTPUT-BANNED" && bannedNow === true && afterBan === "OUTPUT-BANNED";
115+
116+
if (ok) {
117+
pass();
118+
return;
119+
}
120+
fail(`leak=${leak} banned=${bannedNow} after=${afterBan}`);
121+
});
122+
123+
test("Will deliver ban message to banned makeConnection client", async ({ pass, fail }) => {
124+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
125+
126+
const POLICY = addPolicy({
127+
policyName: "b4-policy",
128+
persist: false,
129+
banMessage: "CONN-BANNED",
130+
validateInput: async () => true,
131+
});
132+
const C = addEcho("b4-completion");
133+
const AGENT = addAgent({ agentName: "b4-agent", completion: C, prompt: "" });
134+
const SWARM = addSwarm({ swarmName: "b4-swarm", agentList: [AGENT], defaultAgent: AGENT, policies: [POLICY] });
135+
136+
const cid = randomString();
137+
const received = [];
138+
const send = makeConnection((msg) => received.push(msg.data), cid, SWARM);
139+
await send("hello");
140+
await Policy.banClient({ clientId: cid, swarmName: SWARM, policyName: POLICY });
141+
await send("after-ban");
142+
await sleep(100);
143+
144+
const ok =
145+
received[0] === "echo:hello" && received[1] === "CONN-BANNED";
146+
147+
if (ok) {
148+
pass();
149+
return;
150+
}
151+
fail(`received=${JSON.stringify(received)}`);
152+
});
153+
154+
test("Will restore flow after unban of auto-banned client", async ({ pass, fail }) => {
155+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
156+
157+
const POLICY = addPolicy({
158+
policyName: "b5-policy",
159+
persist: false,
160+
autoBan: true,
161+
banMessage: "B5-BANNED",
162+
validateInput: async (incoming) => incoming !== "bad",
163+
});
164+
const C = addEcho("b5-completion");
165+
const AGENT = addAgent({ agentName: "b5-agent", completion: C, prompt: "" });
166+
const SWARM = addSwarm({ swarmName: "b5-swarm", agentList: [AGENT], defaultAgent: AGENT, policies: [POLICY] });
167+
168+
const cid = randomString();
169+
const cs = session(cid, SWARM);
170+
await cs.complete("bad");
171+
const whileBanned = await cs.complete("hello");
172+
await Policy.unbanClient({ clientId: cid, swarmName: SWARM, policyName: POLICY });
173+
const afterUnban = await cs.complete("hello");
174+
await cs.dispose();
175+
176+
const ok =
177+
whileBanned === "B5-BANNED" && afterUnban === "echo:hello";
178+
179+
if (ok) {
180+
pass();
181+
return;
182+
}
183+
fail(`whileBanned=${whileBanned} afterUnban=${afterUnban}`);
184+
});
185+
186+
test("Will fall back to banMessage when getBanMessage returns null", async ({ pass, fail }) => {
187+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
188+
189+
const POLICY = addPolicy({
190+
policyName: "b6-policy",
191+
persist: false,
192+
banMessage: "FALLBACK-MESSAGE",
193+
getBanMessage: async () => null,
194+
validateInput: async (incoming) => incoming !== "bad",
195+
});
196+
const C = addEcho("b6-completion");
197+
const AGENT = addAgent({ agentName: "b6-agent", completion: C, prompt: "" });
198+
const SWARM = addSwarm({ swarmName: "b6-swarm", agentList: [AGENT], defaultAgent: AGENT, policies: [POLICY] });
199+
200+
const cid = randomString();
201+
const cs = session(cid, SWARM);
202+
const blocked = await cs.complete("bad");
203+
await cs.dispose();
204+
205+
const ok =
206+
blocked === "FALLBACK-MESSAGE";
207+
208+
if (ok) {
209+
pass();
210+
return;
211+
}
212+
fail(`blocked=${blocked}`);
213+
});
214+
215+
test("Will check ban before invoking validateInput", async ({ pass, fail }) => {
216+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
217+
218+
let validateCalls = 0;
219+
const POLICY = addPolicy({
220+
policyName: "b7-policy",
221+
persist: false,
222+
banMessage: "B7-BANNED",
223+
validateInput: async () => {
224+
validateCalls += 1;
225+
return true;
226+
},
227+
});
228+
const C = addEcho("b7-completion");
229+
const AGENT = addAgent({ agentName: "b7-agent", completion: C, prompt: "" });
230+
const SWARM = addSwarm({ swarmName: "b7-swarm", agentList: [AGENT], defaultAgent: AGENT, policies: [POLICY] });
231+
232+
const cid = randomString();
233+
const cs = session(cid, SWARM);
234+
await Policy.banClient({ clientId: cid, swarmName: SWARM, policyName: POLICY });
235+
const blocked = await cs.complete("hello");
236+
await cs.dispose();
237+
238+
const ok =
239+
blocked === "B7-BANNED" && validateCalls === 0;
240+
241+
if (ok) {
242+
pass();
243+
return;
244+
}
245+
fail(`blocked=${blocked} validateCalls=${validateCalls}`);
246+
});
247+
248+
test("Will block via single merged policy ban and restore on unban", async ({ pass, fail }) => {
249+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
250+
251+
const PASS_POLICY = addPolicy({
252+
policyName: "b8-pass",
253+
persist: false,
254+
banMessage: "PASS-BANNED",
255+
validateInput: async () => true,
256+
});
257+
const STRICT_POLICY = addPolicy({
258+
policyName: "b8-strict",
259+
persist: false,
260+
banMessage: "STRICT-BANNED",
261+
validateInput: async () => true,
262+
});
263+
const C = addEcho("b8-completion");
264+
const AGENT = addAgent({ agentName: "b8-agent", completion: C, prompt: "" });
265+
const SWARM = addSwarm({
266+
swarmName: "b8-swarm",
267+
agentList: [AGENT],
268+
defaultAgent: AGENT,
269+
policies: [PASS_POLICY, STRICT_POLICY],
270+
});
271+
272+
const cid = randomString();
273+
const cs = session(cid, SWARM);
274+
await Policy.banClient({ clientId: cid, swarmName: SWARM, policyName: STRICT_POLICY });
275+
const banInPass = await Policy.hasBan({ clientId: cid, swarmName: SWARM, policyName: PASS_POLICY });
276+
const banInStrict = await Policy.hasBan({ clientId: cid, swarmName: SWARM, policyName: STRICT_POLICY });
277+
const blocked = await cs.complete("hello");
278+
await Policy.unbanClient({ clientId: cid, swarmName: SWARM, policyName: STRICT_POLICY });
279+
const restored = await cs.complete("hello");
280+
await cs.dispose();
281+
282+
const ok =
283+
banInPass === false && banInStrict === true && blocked === "STRICT-BANNED" && restored === "echo:hello";
284+
285+
if (ok) {
286+
pass();
287+
return;
288+
}
289+
fail(`banInPass=${banInPass} banInStrict=${banInStrict} blocked=${blocked} restored=${restored}`);
290+
});
291+

0 commit comments

Comments
 (0)