Skip to content

Commit 0f5845b

Browse files
committed
wip
1 parent 91df319 commit 0f5845b

4 files changed

Lines changed: 242 additions & 3 deletions

File tree

TODO.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,31 @@ complete=HANG)
446446
- AQUIRE_LOCK: поллинг sleep(100) без таймаута; _navigationStack растёт на
447447
каждый переход (write amplification при persist).
448448

449-
## Найденные и исправленные баги (23 итого)
449+
## 21-й заход: createNumericIndex и контракт fork (+6 тестов, +1 баг)
450+
451+
### Новые тесты (6), сьют вырос до 234/234 — test/spec/numindex.test.mjs
452+
- регрессия бага №24: после remove новый индекс НЕ коллидирует с живыми id
453+
([1,2,3] − 1 → next=4, «three» не перезаписан);
454+
- нечисловые id (uuid) игнорируются при вычислении следующего индекса;
455+
- пустой сторэдж → индекс 1;
456+
- fork: ошибка runFn уходит в onError, результат null, сессия зачищена
457+
(контракт «fork не пробрасывает ошибки» закреплён);
458+
- fork happy path: возврат значения, clientId/agentName в runFn, cleanup;
459+
- fork на живой clientId кидает "already exists".
460+
461+
### Баг №24: createNumericIndex терял данные после удаления элементов
462+
Файл: `src/classes/Storage.ts` (найден пробой: next=3 при живом id=3, элемент
463+
«three» молча перезаписан upsert'ом)
464+
- Индекс считался как list().length + 1: после удаления элемента длина меньше
465+
максимального id, и новый индекс коллидировал с живым элементом — upsert
466+
затирал чужие данные.
467+
- Исправление: индекс = max(числовых id) + 1; нечисловые id игнорируются;
468+
пустой сторэдж → 1.
469+
- Ограничение (задокументировано): параллельные createNumericIndex без upsert
470+
между ними по-прежнему возвращают одинаковое значение — метод вычисляет
471+
«следующий свободный» от ТЕКУЩИХ данных и не резервирует его.
472+
473+
## Найденные и исправленные баги (24 итого)
450474

451475
### 1. Дедлок waitForOutput при functools-kit v4 (причина 39 упавших тестов)
452476
Файл: `src/client/ClientSwarm.ts`

src/classes/Storage.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,22 @@ export class StorageUtils implements TStorage {
341341
`agent-swarm StorageUtils ${payload.storageName} not registered in ${payload.agentName} (createNumericIndex)`
342342
);
343343
}
344-
const { length } = await swarm.storagePublicService.list(
344+
const items = await swarm.storagePublicService.list(
345345
METHOD_NAME_CREATE_NUMERIC_INDEX,
346346
payload.clientId,
347347
payload.storageName
348348
);
349-
return length + 1;
349+
// length + 1 collides with surviving ids once anything was removed
350+
// (items [1,2,3] minus 1 -> length 2 -> next "3" overwrites live item 3),
351+
// so the next index must come from the maximum numeric id instead.
352+
let maxId = 0;
353+
for (const { id } of items) {
354+
const numericId = Number(id);
355+
if (!Number.isNaN(numericId)) {
356+
maxId = Math.max(maxId, numericId);
357+
}
358+
}
359+
return maxId + 1;
350360
}
351361
);
352362

test/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import "./spec/aliastools.test.mjs";
3333
import "./spec/multiclient.test.mjs";
3434
import "./spec/doublesend.test.mjs";
3535
import "./spec/lifecycle.test.mjs";
36+
import "./spec/numindex.test.mjs";
3637

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

test/spec/numindex.test.mjs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { test } from "worker-testbed";
2+
3+
import {
4+
addAgent,
5+
addCompletion,
6+
addEmbedding,
7+
addStorage,
8+
addSwarm,
9+
fork,
10+
hasSession,
11+
session,
12+
setConfig,
13+
Storage,
14+
} from "../../build/index.mjs";
15+
import { randomString } from "functools-kit";
16+
17+
const addEcho = (name) =>
18+
addCompletion({
19+
completionName: name,
20+
getCompletion: async ({ agentName, messages }) => {
21+
const [last] = messages.slice(-1);
22+
return { agentName, content: `echo:${last.content}`, role: "assistant" };
23+
},
24+
});
25+
26+
const makeStorageSwarm = (suffix) => {
27+
const COMPLETION = addEcho(`${suffix}-completion`);
28+
addEmbedding({
29+
embeddingName: `${suffix}-embedding`,
30+
createEmbedding: async (t) => [t.length],
31+
calculateSimilarity: async (a, b) => (a[0] === b[0] ? 1 : 0),
32+
});
33+
addStorage({
34+
storageName: `${suffix}-storage`,
35+
embedding: `${suffix}-embedding`,
36+
createIndex: (i) => i.text,
37+
});
38+
const AGENT = addAgent({
39+
agentName: `${suffix}-agent`,
40+
completion: COMPLETION,
41+
prompt: "",
42+
storages: [`${suffix}-storage`],
43+
});
44+
return addSwarm({
45+
swarmName: `${suffix}-swarm`,
46+
agentList: [AGENT],
47+
defaultAgent: AGENT,
48+
});
49+
};
50+
51+
test("Will skip surviving ids in numeric index after remove", async ({ pass, fail }) => {
52+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
53+
54+
const SWARM = makeStorageSwarm("n1");
55+
const CLIENT_ID = randomString();
56+
const chatSession = session(CLIENT_ID, SWARM);
57+
const base = { clientId: CLIENT_ID, agentName: "n1-agent", storageName: "n1-storage" };
58+
59+
await Storage.upsert({ ...base, item: { id: 1, text: "one" } });
60+
await Storage.upsert({ ...base, item: { id: 2, text: "two" } });
61+
await Storage.upsert({ ...base, item: { id: 3, text: "three" } });
62+
await Storage.remove({ ...base, itemId: 1 });
63+
const nextIndex = await Storage.createNumericIndex(base);
64+
await Storage.upsert({ ...base, item: { id: nextIndex, text: "new-item" } });
65+
const items = await Storage.list(base);
66+
await chatSession.dispose();
67+
68+
const ids = items.map((i) => i.id).sort();
69+
const three = items.find((i) => i.id === 3);
70+
const ok =
71+
nextIndex === 4 && items.length === 3 && three?.text === "three" && ids.join(",") === "2,3,4";
72+
73+
if (ok) {
74+
pass();
75+
return;
76+
}
77+
fail(`nextIndex=${nextIndex} items=${JSON.stringify(items.map((i) => [i.id, i.text]))}`);
78+
});
79+
80+
test("Will ignore non-numeric ids in numeric index", async ({ pass, fail }) => {
81+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
82+
83+
const SWARM = makeStorageSwarm("n2");
84+
const CLIENT_ID = randomString();
85+
const chatSession = session(CLIENT_ID, SWARM);
86+
const base = { clientId: CLIENT_ID, agentName: "n2-agent", storageName: "n2-storage" };
87+
88+
await Storage.upsert({ ...base, item: { id: "uuid-like", text: "alpha" } });
89+
await Storage.upsert({ ...base, item: { id: 5, text: "beta" } });
90+
const nextIndex = await Storage.createNumericIndex(base);
91+
await chatSession.dispose();
92+
93+
const ok =
94+
nextIndex === 6;
95+
96+
if (ok) {
97+
pass();
98+
return;
99+
}
100+
fail(`nextIndex=${nextIndex}`);
101+
});
102+
103+
test("Will start numeric index from one on empty storage", async ({ pass, fail }) => {
104+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
105+
106+
const SWARM = makeStorageSwarm("n3");
107+
const CLIENT_ID = randomString();
108+
const chatSession = session(CLIENT_ID, SWARM);
109+
const base = { clientId: CLIENT_ID, agentName: "n3-agent", storageName: "n3-storage" };
110+
const nextIndex = await Storage.createNumericIndex(base);
111+
await chatSession.dispose();
112+
113+
const ok =
114+
nextIndex === 1;
115+
116+
if (ok) {
117+
pass();
118+
return;
119+
}
120+
fail(`nextIndex=${nextIndex}`);
121+
});
122+
123+
test("Will route fork error to onError and clean up session", async ({ pass, fail }) => {
124+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
125+
126+
const COMPLETION = addEcho("f1-completion");
127+
const AGENT = addAgent({ agentName: "f1-agent", completion: COMPLETION, prompt: "" });
128+
const SWARM = addSwarm({ swarmName: "f1-swarm", agentList: [AGENT], defaultAgent: AGENT });
129+
130+
const CLIENT_ID = randomString();
131+
let capturedError = "";
132+
const result = await fork(
133+
async () => {
134+
throw new Error("fork boom");
135+
},
136+
{
137+
clientId: CLIENT_ID,
138+
swarmName: SWARM,
139+
onError: (error) => {
140+
capturedError = error.message;
141+
},
142+
}
143+
);
144+
145+
const ok =
146+
result === null && capturedError === "fork boom" && hasSession(CLIENT_ID) === false;
147+
148+
if (ok) {
149+
pass();
150+
return;
151+
}
152+
fail(`result=${JSON.stringify(result)} error=${capturedError} hasSession=${hasSession(CLIENT_ID)}`);
153+
});
154+
155+
test("Will return fork value and clean up session", async ({ pass, fail }) => {
156+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
157+
158+
const COMPLETION = addEcho("f2-completion");
159+
const AGENT = addAgent({ agentName: "f2-agent", completion: COMPLETION, prompt: "" });
160+
const SWARM = addSwarm({ swarmName: "f2-swarm", agentList: [AGENT], defaultAgent: AGENT });
161+
162+
const CLIENT_ID = randomString();
163+
const result = await fork(
164+
async (clientId, agentName) => `ran:${clientId === CLIENT_ID}:${agentName}`,
165+
{ clientId: CLIENT_ID, swarmName: SWARM }
166+
);
167+
168+
const ok =
169+
result === `ran:true:f2-agent` && hasSession(CLIENT_ID) === false;
170+
171+
if (ok) {
172+
pass();
173+
return;
174+
}
175+
fail(`result=${JSON.stringify(result)} hasSession=${hasSession(CLIENT_ID)}`);
176+
});
177+
178+
test("Will reject fork with live duplicate clientId", async ({ pass, fail }) => {
179+
setConfig({ CC_PERSIST_ENABLED_BY_DEFAULT: false });
180+
181+
const COMPLETION = addEcho("f3-completion");
182+
const AGENT = addAgent({ agentName: "f3-agent", completion: COMPLETION, prompt: "" });
183+
const SWARM = addSwarm({ swarmName: "f3-swarm", agentList: [AGENT], defaultAgent: AGENT });
184+
185+
const CLIENT_ID = randomString();
186+
const chatSession = session(CLIENT_ID, SWARM);
187+
let duplicateError = "";
188+
try {
189+
await fork(async () => "never", { clientId: CLIENT_ID, swarmName: SWARM });
190+
} catch (error) {
191+
duplicateError = error.message;
192+
}
193+
await chatSession.dispose();
194+
195+
const ok =
196+
duplicateError.includes("already exists");
197+
198+
if (ok) {
199+
pass();
200+
return;
201+
}
202+
fail(`error=${duplicateError}`);
203+
});
204+

0 commit comments

Comments
 (0)