@johpaz/hive-sdk 0.1.6 → 0.2.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.
- package/CHANGELOG.md +135 -0
- package/README.md +11 -1
- package/package.json +18 -2
- package/packages/core/src/agent/acceptance-checks.ts +9 -9
- package/packages/core/src/agent/agent-catalog.ts +3 -3
- package/packages/core/src/agent/agent-loop.ts +115 -26
- package/packages/core/src/agent/capability-search.ts +2 -2
- package/packages/core/src/agent/catalog-selector.ts +4 -4
- package/packages/core/src/agent/compaction.ts +10 -9
- package/packages/core/src/agent/context-compiler.ts +58 -34
- package/packages/core/src/agent/conversation-store.ts +31 -7
- package/packages/core/src/agent/curator.ts +4 -4
- package/packages/core/src/agent/delegation-runtime.ts +5 -5
- package/packages/core/src/agent/goal-runner.ts +9 -9
- package/packages/core/src/agent/index.ts +1 -0
- package/packages/core/src/agent/llm-client.ts +98 -37
- package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
- package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
- package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
- package/packages/core/src/agent/llm-providers/groq.ts +1 -1
- package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
- package/packages/core/src/agent/llm-providers/interface.ts +2 -2
- package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
- package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
- package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
- package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
- package/packages/core/src/agent/llm-providers/nvidia.ts +1 -1
- package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +9 -5
- package/packages/core/src/agent/llm-providers/openai.ts +1 -1
- package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
- package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
- package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
- package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
- package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
- package/packages/core/src/agent/playbook-selector.ts +4 -4
- package/packages/core/src/agent/prompt-builder.ts +5 -5
- package/packages/core/src/agent/proof-packet.ts +5 -5
- package/packages/core/src/agent/providers/index.ts +4 -4
- package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
- package/packages/core/src/agent/realtime-providers/index.ts +29 -0
- package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
- package/packages/core/src/agent/reflector.ts +6 -6
- package/packages/core/src/agent/run-store.ts +8 -8
- package/packages/core/src/agent/service.ts +10 -10
- package/packages/core/src/agent/skill-selector.ts +6 -6
- package/packages/core/src/agent/thread-id.ts +71 -0
- package/packages/core/src/agent/thread-store.ts +250 -0
- package/packages/core/src/agent/tool-selector.ts +7 -5
- package/packages/core/src/agent/tracer.ts +5 -5
- package/packages/core/src/api/createAgent.ts +1 -1
- package/packages/core/src/artifacts/store.ts +84 -3
- package/packages/core/src/canvas/emitter.ts +2 -2
- package/packages/core/src/channels/telegram.ts +1 -1
- package/packages/core/src/channels/webchat.ts +1 -1
- package/packages/core/src/config/loader.ts +14 -5
- package/packages/core/src/events/agent-bus.ts +3 -3
- package/packages/core/src/events/channel-narration.ts +3 -3
- package/packages/core/src/events/event-bus.ts +1 -1
- package/packages/core/src/events/narration.ts +3 -3
- package/packages/core/src/gateway/delegation-groups.ts +4 -4
- package/packages/core/src/gateway/durable-queue.ts +5 -5
- package/packages/core/src/gateway/job-store.ts +5 -5
- package/packages/core/src/gateway/notification-inbox.ts +2 -2
- package/packages/core/src/gateway/server.ts +2 -2
- package/packages/core/src/mcp/MCPClient.ts +3 -3
- package/packages/core/src/mcp/hot-reload.ts +5 -5
- package/packages/core/src/mcp/tool-sync.ts +5 -5
- package/packages/core/src/mcp/transports/index.ts +2 -2
- package/packages/core/src/mcp/transports/sse.ts +1 -1
- package/packages/core/src/models/index.ts +36 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +6 -6
- package/packages/core/src/plugins/loader.ts +4 -1
- package/packages/core/src/resilience/circuit-breaker.ts +16 -5
- package/packages/core/src/resilience/retry.ts +1 -1
- package/packages/core/src/scheduler/CronScheduler.ts +6 -6
- package/packages/core/src/scheduler/integration.ts +9 -9
- package/packages/core/src/sessions/index.ts +266 -0
- package/packages/core/src/storage/bootstrap.ts +34 -8
- package/packages/core/src/storage/causal-events.ts +1 -1
- package/packages/core/src/storage/collections.ts +32 -1
- package/packages/core/src/storage/crypto.ts +16 -2
- package/packages/core/src/storage/hive.ts +1 -1
- package/packages/core/src/storage/hivedb.ts +10 -1
- package/packages/core/src/storage/onboarding.ts +6 -6
- package/packages/core/src/storage/reconcile.ts +5 -5
- package/packages/core/src/storage/seed.ts +103 -13
- package/packages/core/src/storage/usage.ts +3 -3
- package/packages/core/src/swarm/AgentExecutor.ts +2 -2
- package/packages/core/src/swarm/Coordinator.ts +8 -8
- package/packages/core/src/swarm/EventBridge.ts +2 -2
- package/packages/core/src/swarm/RoleSwarm.ts +234 -0
- package/packages/core/src/swarm/TaskGraph.ts +2 -2
- package/packages/core/src/swarm/index.ts +7 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
- package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
- package/packages/core/src/tools/ToolExecutor.ts +7 -3
- package/packages/core/src/tools/core/index.ts +2 -2
- package/packages/core/src/tools/cron/index.ts +4 -4
- package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
- package/packages/core/src/tools/web/artifact-read.ts +162 -0
- package/packages/core/src/tools/web/browser-backend.ts +141 -44
- package/packages/core/src/tools/web/browser-click.ts +2 -2
- package/packages/core/src/tools/web/browser-extract.ts +2 -2
- package/packages/core/src/tools/web/browser-navigate.ts +2 -2
- package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
- package/packages/core/src/tools/web/browser-script.ts +2 -2
- package/packages/core/src/tools/web/browser-service.ts +63 -384
- package/packages/core/src/tools/web/browser-session.ts +125 -0
- package/packages/core/src/tools/web/browser-type.ts +2 -2
- package/packages/core/src/tools/web/browser-wait.ts +2 -2
- package/packages/core/src/tools/web/computer-use.ts +553 -0
- package/packages/core/src/tools/web/index.ts +8 -1
- package/packages/core/src/tools/web/webview-backend.ts +460 -21
- package/packages/core/src/utils/index.ts +1 -0
- package/packages/core/src/utils/logger.ts +12 -4
- package/packages/core/src/utils/redact-binary.ts +17 -0
- package/packages/core/src/utils/toon.ts +1 -1
- package/packages/core/src/voice/index.ts +6 -6
- package/bun.lock +0 -859
- package/bunfig.toml +0 -9
- package/docs/API-AGENTS.md +0 -367
- package/docs/API-CONTEXT-COMPILER.md +0 -249
- package/docs/API-DAG-SCHEDULER.md +0 -273
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
- package/docs/API-WORKERS-EVENTS.md +0 -299
- package/docs/HIVE-HARNESS.md +0 -113
- package/docs/INDEX.md +0 -190
- package/docs/TEMPLATE-HIVE-APP.md +0 -360
- package/packages/cli/package.json +0 -17
- package/packages/cli/src/commands/create-app.test.ts +0 -180
- package/packages/core/package.json +0 -70
- package/packages/core/src/api/createAgent.test.ts +0 -160
- package/packages/core/src/canvas/canvas.test.ts +0 -36
- package/packages/core/src/channels/channels.test.ts +0 -18
- package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
- package/packages/core/src/gateway/gateway.test.ts +0 -38
- package/packages/core/src/memory/Scratchpad.test.ts +0 -68
- package/packages/core/src/scheduler/scheduler.test.ts +0 -15
- package/packages/core/src/skills/skills.test.ts +0 -62
- package/packages/core/src/swarm/swarm.test.ts +0 -24
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
- package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
- package/packages/core/src/tools/api/api-request.test.ts +0 -164
- package/packages/core/src/tools/web/browser-service.test.ts +0 -83
- package/packages/core/src/workers/workers.test.ts +0 -41
- package/scripts/bump-version.ts +0 -248
- package/scripts/generate-skill-bundle.ts +0 -108
- package/test/acceptance-checks.test.ts +0 -403
- package/test/agent-loop-terminal-synthesis.test.ts +0 -32
- package/test/browser-backend.test.ts +0 -308
- package/test/catalog-agents-stay-enabled.test.ts +0 -117
- package/test/causal-events.test.ts +0 -117
- package/test/compaction.test.ts +0 -105
- package/test/context-compiler.test.ts +0 -269
- package/test/curator.test.ts +0 -130
- package/test/durable-queue.test.ts +0 -114
- package/test/harness-barrel.test.ts +0 -64
- package/test/hive-helpers.test.ts +0 -130
- package/test/hivedb-search.test.ts +0 -189
- package/test/internal-turns.test.ts +0 -166
- package/test/job-idempotency.test.ts +0 -68
- package/test/job-retry-backoff.test.ts +0 -184
- package/test/job-store.test.ts +0 -381
- package/test/llm-retry.test.ts +0 -97
- package/test/memory-perf.test.ts +0 -774
- package/test/minimal-loadout.test.ts +0 -78
- package/test/model-catalog.test.ts +0 -105
- package/test/preload.ts +0 -12
- package/test/reflector.test.ts +0 -320
- package/test/retention-cap.test.ts +0 -91
- package/test/retired-capabilities-pruned.test.ts +0 -192
- package/test/run-store.test.ts +0 -355
- package/test/scratchpad.test.ts +0 -74
- package/test/secrets-durability.test.ts +0 -119
- package/test/seed-model-reseed.test.ts +0 -155
- package/test/setup-agent-seed.test.ts +0 -264
- package/test/tool-inventory.test.ts +0 -65
- package/test/tool-runtime.test.ts +0 -258
- package/test/tool-selector-runtime-tools.test.ts +0 -117
- package/test/toon.test.ts +0 -429
- package/tsconfig.json +0 -42
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BrowserBackend — selección de backend y el WebViewBackend real.
|
|
3
|
-
*
|
|
4
|
-
* Los tests marcados como "vivos" abren un navegador de verdad y se saltan solos
|
|
5
|
-
* donde no hay motor (sin Chrome y sin macOS). Los de selección son puros y
|
|
6
|
-
* corren siempre: son los que evitan que un cambio de default mande el gateway
|
|
7
|
-
* headless a un backend que necesita display.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
process.env.HIVE_DB_PATH = ":memory:";
|
|
11
|
-
|
|
12
|
-
import { describe, test, expect, beforeEach, afterEach, afterAll } from "bun:test";
|
|
13
|
-
import {
|
|
14
|
-
resolveBackendKind,
|
|
15
|
-
isWebViewSupported,
|
|
16
|
-
resolveWebViewEngine,
|
|
17
|
-
findChrome,
|
|
18
|
-
} from "../packages/core/src/tools/web/browser-backend.ts";
|
|
19
|
-
import { WebViewBackend } from "../packages/core/src/tools/web/webview-backend.ts";
|
|
20
|
-
|
|
21
|
-
const ORIGINAL_ENV = process.env.HIVE_BROWSER_BACKEND;
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
if (ORIGINAL_ENV === undefined) delete process.env.HIVE_BROWSER_BACKEND;
|
|
25
|
-
else process.env.HIVE_BROWSER_BACKEND = ORIGINAL_ENV;
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Una sola vez, al final del archivo: `closeAll()` mata TODOS los subprocesos de
|
|
29
|
-
// navegador del proceso, así que dentro de un `afterAll` por bloque le arranca
|
|
30
|
-
// Chrome de abajo al bloque siguiente ("Chrome killed by signal 9").
|
|
31
|
-
afterAll(() => {
|
|
32
|
-
(globalThis as { Bun?: { WebView?: { closeAll?: () => void } } }).Bun?.WebView?.closeAll?.();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// ─── selección de backend ─────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
describe("resolveBackendKind", () => {
|
|
38
|
-
test("el default es agent-browser — es el único que corre headless sin Chrome", () => {
|
|
39
|
-
delete process.env.HIVE_BROWSER_BACKEND;
|
|
40
|
-
expect(resolveBackendKind(undefined)).toBe("agent-browser");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("respeta la preferencia de config", () => {
|
|
44
|
-
delete process.env.HIVE_BROWSER_BACKEND;
|
|
45
|
-
expect(resolveBackendKind("webview")).toBe("webview");
|
|
46
|
-
expect(resolveBackendKind("agent-browser")).toBe("agent-browser");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("HIVE_BROWSER_BACKEND pisa la config", () => {
|
|
50
|
-
process.env.HIVE_BROWSER_BACKEND = "webview";
|
|
51
|
-
expect(resolveBackendKind("agent-browser")).toBe("webview");
|
|
52
|
-
|
|
53
|
-
process.env.HIVE_BROWSER_BACKEND = "agent-browser";
|
|
54
|
-
expect(resolveBackendKind("webview")).toBe("agent-browser");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("'auto' nunca elige webview si no hay motor disponible", () => {
|
|
58
|
-
delete process.env.HIVE_BROWSER_BACKEND;
|
|
59
|
-
const kind = resolveBackendKind("auto");
|
|
60
|
-
expect(kind).toBe(isWebViewSupported() ? "webview" : "agent-browser");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("un valor desconocido cae al default en vez de romper", () => {
|
|
64
|
-
process.env.HIVE_BROWSER_BACKEND = "netscape";
|
|
65
|
-
expect(resolveBackendKind("webview")).toBe("agent-browser");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("detección de motor", () => {
|
|
70
|
-
test("resolveWebViewEngine devuelve webkit en macOS y chrome sólo si hay binario", () => {
|
|
71
|
-
const engine = resolveWebViewEngine();
|
|
72
|
-
if (process.platform === "darwin") {
|
|
73
|
-
expect(engine).toBe("webkit");
|
|
74
|
-
} else {
|
|
75
|
-
expect(engine).toBe(findChrome() ? "chrome" : null);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("BUN_CHROME_PATH gana sobre la búsqueda en rutas estándar", () => {
|
|
80
|
-
const previous = process.env.BUN_CHROME_PATH;
|
|
81
|
-
process.env.BUN_CHROME_PATH = "/ruta/propia/chrome";
|
|
82
|
-
try {
|
|
83
|
-
expect(findChrome()).toBe("/ruta/propia/chrome");
|
|
84
|
-
} finally {
|
|
85
|
-
if (previous === undefined) delete process.env.BUN_CHROME_PATH;
|
|
86
|
-
else process.env.BUN_CHROME_PATH = previous;
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// ─── WebViewBackend vivo ──────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
const LIVE = isWebViewSupported();
|
|
94
|
-
// El charset va explícito: sin él el motor asume latin-1 y "botón" llega como
|
|
95
|
-
// "botón". Una página real lo declara; una data: URL no.
|
|
96
|
-
const page = (html: string) => `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
|
97
|
-
|
|
98
|
-
describe.skipIf(!LIVE)("WebViewBackend (navegador real)", () => {
|
|
99
|
-
let backend: WebViewBackend;
|
|
100
|
-
|
|
101
|
-
beforeEach(() => {
|
|
102
|
-
backend = new WebViewBackend();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
afterEach(() => {
|
|
106
|
-
backend.close();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("navega y evalúa en la página", async () => {
|
|
110
|
-
await backend.navigate(page("<h1>hola mundo</h1>"));
|
|
111
|
-
expect(await backend.evaluate<string>("document.querySelector('h1').textContent")).toBe("hola mundo");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("serializa operaciones concurrentes — WebView rechaza las solapadas", async () => {
|
|
115
|
-
// Sin la cola interna esto falla con
|
|
116
|
-
// "ERR_INVALID_STATE: a simple operation is already pending".
|
|
117
|
-
await backend.navigate(page("<p id=x>1</p>"));
|
|
118
|
-
|
|
119
|
-
const results = await Promise.all([
|
|
120
|
-
backend.evaluate("1 + 1"),
|
|
121
|
-
backend.evaluate("2 + 2"),
|
|
122
|
-
backend.evaluate("3 + 3"),
|
|
123
|
-
backend.evaluate("document.getElementById('x').textContent"),
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
expect(results).toEqual([2, 4, 6, "1"]);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("una operación fallida no rompe la cola para las siguientes", async () => {
|
|
130
|
-
await backend.navigate(page("<h1>sigo viva</h1>"));
|
|
131
|
-
|
|
132
|
-
await expect(backend.evaluate("esto.no.existe()")).rejects.toThrow();
|
|
133
|
-
expect(await backend.evaluate<string>("document.querySelector('h1').textContent")).toBe("sigo viva");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("click y fill operan por selector CSS", async () => {
|
|
137
|
-
await backend.navigate(
|
|
138
|
-
page(`<input id="i"><button id="b" onclick="document.getElementById('i').dataset.hit='1'">Go</button>`),
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
await backend.fill("#i", "texto nuevo");
|
|
142
|
-
expect(await backend.evaluate<string>("document.getElementById('i').value")).toBe("texto nuevo");
|
|
143
|
-
|
|
144
|
-
await backend.click("#b");
|
|
145
|
-
expect(await backend.evaluate<string>("document.getElementById('i').dataset.hit")).toBe("1");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("fill reemplaza el contenido en vez de agregarlo", async () => {
|
|
149
|
-
await backend.navigate(page(`<input id="i" value="viejo">`));
|
|
150
|
-
await backend.fill("#i", "nuevo");
|
|
151
|
-
expect(await backend.evaluate<string>("document.getElementById('i').value")).toBe("nuevo");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("typeIn y fill fallan claro cuando el selector no existe", async () => {
|
|
155
|
-
await backend.navigate(page("<h1>x</h1>"));
|
|
156
|
-
await expect(backend.typeIn("#no-existe", "x")).rejects.toThrow(/no encontrado/);
|
|
157
|
-
await expect(backend.fill("#no-existe", "x")).rejects.toThrow(/no encontrado/);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("la navegación hacia atrás usa goBack — los tipos de bun-types dicen back()", async () => {
|
|
161
|
-
await backend.navigate(page("<h1>primera</h1>"));
|
|
162
|
-
await backend.navigate(page("<h1>segunda</h1>"));
|
|
163
|
-
expect(await backend.evaluate<string>("document.querySelector('h1').textContent")).toBe("segunda");
|
|
164
|
-
|
|
165
|
-
await backend.back();
|
|
166
|
-
expect(await backend.evaluate<string>("document.querySelector('h1').textContent")).toBe("primera");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("screenshot devuelve un PNG en base64", async () => {
|
|
170
|
-
await backend.navigate(page("<h1>foto</h1>"));
|
|
171
|
-
const shot = await backend.screenshot();
|
|
172
|
-
expect(shot.length).toBeGreaterThan(100);
|
|
173
|
-
// Cabecera PNG (\x89PNG) en base64.
|
|
174
|
-
expect(shot.startsWith("iVBORw0KGgo")).toBe(true);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("screenshotElement recorta al elemento pedido", async () => {
|
|
178
|
-
await backend.navigate(
|
|
179
|
-
page(`<div id="chico" style="width:80px;height:40px;background:red"></div>`),
|
|
180
|
-
);
|
|
181
|
-
const shot = await backend.screenshotElement("#chico");
|
|
182
|
-
expect(shot.startsWith("iVBORw0KGgo")).toBe(true);
|
|
183
|
-
// El recorte tiene que pesar menos que la captura del viewport entero.
|
|
184
|
-
expect(shot.length).toBeLessThan((await backend.screenshot()).length);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("screenshotElement falla claro si el elemento no existe", async () => {
|
|
188
|
-
await backend.navigate(page("<h1>x</h1>"));
|
|
189
|
-
await expect(backend.screenshotElement("#fantasma")).rejects.toThrow(/no visible o inexistente/);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// ─── snapshot sintetizado ─────────────────────────────────────────────────────
|
|
194
|
-
|
|
195
|
-
describe.skipIf(!LIVE)("WebViewBackend.snapshot", () => {
|
|
196
|
-
let backend: WebViewBackend;
|
|
197
|
-
|
|
198
|
-
beforeEach(() => {
|
|
199
|
-
backend = new WebViewBackend();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
afterEach(() => {
|
|
203
|
-
backend.close();
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test("reproduce el formato de agent-browser: rol, nombre y ref", async () => {
|
|
207
|
-
await backend.navigate(page(`<h1>Example Domain</h1><p><a href="/x">Learn more</a></p>`));
|
|
208
|
-
const snapshot = await backend.snapshot({ compact: true, depth: 3 });
|
|
209
|
-
|
|
210
|
-
expect(snapshot).toContain('- heading "Example Domain" [level=1, ref=e1]');
|
|
211
|
-
expect(snapshot).toContain('link "Learn more"');
|
|
212
|
-
expect(snapshot).toMatch(/ref=e\d+/);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("anida los hijos bajo el padre que emitió línea", async () => {
|
|
216
|
-
await backend.navigate(page(`<p>Intro <a href="/x">un link</a></p>`));
|
|
217
|
-
const snapshot = await backend.snapshot();
|
|
218
|
-
|
|
219
|
-
const linea = snapshot.split("\n").find((l) => l.includes("link"));
|
|
220
|
-
expect(linea?.startsWith(" ")).toBe(true);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test("los divs de maquetado no generan sangría — el árbol no se hunde", async () => {
|
|
224
|
-
await backend.navigate(
|
|
225
|
-
page(`<div><div><div><div><button>Enviar</button></div></div></div></div>`),
|
|
226
|
-
);
|
|
227
|
-
const snapshot = await backend.snapshot({ depth: 3 });
|
|
228
|
-
|
|
229
|
-
// Cuatro divs de por medio y el botón sigue en el nivel raíz.
|
|
230
|
-
expect(snapshot).toBe('- button "Enviar" [ref=e1]');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("ignora script, style y lo oculto", async () => {
|
|
234
|
-
await backend.navigate(
|
|
235
|
-
page(`<script>var secreto=1</script><style>.x{}</style>
|
|
236
|
-
<div hidden><a href="/h">oculto</a></div>
|
|
237
|
-
<div style="display:none"><a href="/d">tampoco</a></div>
|
|
238
|
-
<div aria-hidden="true"><a href="/a">ni este</a></div>
|
|
239
|
-
<a href="/v">visible</a>`),
|
|
240
|
-
);
|
|
241
|
-
const snapshot = await backend.snapshot();
|
|
242
|
-
|
|
243
|
-
expect(snapshot).toContain("visible");
|
|
244
|
-
expect(snapshot).not.toContain("secreto");
|
|
245
|
-
expect(snapshot).not.toContain("oculto");
|
|
246
|
-
expect(snapshot).not.toContain("tampoco");
|
|
247
|
-
expect(snapshot).not.toContain("ni este");
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test("toma el nombre accesible de aria-label, alt y placeholder", async () => {
|
|
251
|
-
await backend.navigate(
|
|
252
|
-
page(`<button aria-label="Cerrar ventana"></button>
|
|
253
|
-
<img src="x.png" alt="Un gato">
|
|
254
|
-
<input placeholder="Tu correo">`),
|
|
255
|
-
);
|
|
256
|
-
const snapshot = await backend.snapshot();
|
|
257
|
-
|
|
258
|
-
expect(snapshot).toContain("Cerrar ventana");
|
|
259
|
-
expect(snapshot).toContain("Un gato");
|
|
260
|
-
expect(snapshot).toContain("Tu correo");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("interactiveOnly deja sólo lo accionable", async () => {
|
|
264
|
-
await backend.navigate(
|
|
265
|
-
page(`<h1>Un título</h1><p>Un párrafo largo</p><a href="/x">Un link</a><button>Un botón</button>`),
|
|
266
|
-
);
|
|
267
|
-
const snapshot = await backend.snapshot({ interactiveOnly: true });
|
|
268
|
-
|
|
269
|
-
expect(snapshot).toContain("Un link");
|
|
270
|
-
expect(snapshot).toContain("Un botón");
|
|
271
|
-
expect(snapshot).not.toContain("Un título");
|
|
272
|
-
expect(snapshot).not.toContain("Un párrafo largo");
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test("depth corta la profundidad de lo que sí anida", async () => {
|
|
276
|
-
// La profundidad cuenta niveles *emitidos*, no nodos del DOM: por eso hace
|
|
277
|
-
// falta un ancestro con nombre propio (el nav) para que el link baje un nivel.
|
|
278
|
-
await backend.navigate(page(`<nav aria-label="Menú"><a href="/x">hondo</a></nav>`));
|
|
279
|
-
|
|
280
|
-
expect(await backend.snapshot({ depth: 1 })).not.toContain("hondo");
|
|
281
|
-
expect(await backend.snapshot({ depth: 5 })).toContain("hondo");
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("marca los atributos de estado del control", async () => {
|
|
285
|
-
await backend.navigate(
|
|
286
|
-
page(`<input type="checkbox" aria-label="Acepto" checked>
|
|
287
|
-
<button disabled aria-label="Enviar">x</button>`),
|
|
288
|
-
);
|
|
289
|
-
const snapshot = await backend.snapshot();
|
|
290
|
-
|
|
291
|
-
expect(snapshot).toContain("checked");
|
|
292
|
-
expect(snapshot).toContain("disabled");
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("un DOM enorme se trunca en vez de comerse el contexto", async () => {
|
|
296
|
-
const filas = Array.from({ length: 4000 }, (_, i) => `<p>fila número ${i} con texto de relleno</p>`).join("");
|
|
297
|
-
await backend.navigate(page(`<div>${filas}</div>`));
|
|
298
|
-
|
|
299
|
-
const snapshot = await backend.snapshot();
|
|
300
|
-
expect(snapshot.length).toBeLessThan(21_000);
|
|
301
|
-
expect(snapshot).toContain("snapshot truncado");
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test("una página vacía devuelve string vacío, no una excepción", async () => {
|
|
305
|
-
await backend.navigate(page("<body></body>"));
|
|
306
|
-
expect(await backend.snapshot()).toBe("");
|
|
307
|
-
});
|
|
308
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A catalog persona is a capability of the whole hive, not a disposable
|
|
3
|
-
* worker: nothing automatic may switch one off. Disabling is the user's call,
|
|
4
|
-
* from the Agents UI (PUT /api/agents/:id). Uses HIVE_DB_PATH=":memory:".
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
process.env.HIVE_DB_PATH = ":memory:";
|
|
8
|
-
|
|
9
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
10
|
-
import { closeHiveDb } from "../packages/core/src/storage/hivedb";
|
|
11
|
-
import { ensureHiveDb } from "../packages/core/src/storage/bootstrap";
|
|
12
|
-
import { col } from "../packages/core/src/storage/hive";
|
|
13
|
-
import type { AgentDoc, AgentProposalDoc } from "../packages/core/src/storage/collections";
|
|
14
|
-
import { runCurator } from "../packages/core/src/agent/curator";
|
|
15
|
-
import { agentArchiveTool } from "../packages/core/src/tools/agents";
|
|
16
|
-
import { CATALOG_AGENT_IDS } from "../packages/core/src/agent/agent-catalog";
|
|
17
|
-
|
|
18
|
-
const VICTIM = "workspace_file_operator";
|
|
19
|
-
|
|
20
|
-
beforeEach(async () => {
|
|
21
|
-
closeHiveDb();
|
|
22
|
-
await ensureHiveDb();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
closeHiveDb();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
async function agent(id: string): Promise<AgentDoc> {
|
|
30
|
-
const entry = await (await col<AgentDoc>("agents")).get(id);
|
|
31
|
-
if (!entry) throw new Error(`agent not found: ${id}`);
|
|
32
|
-
return entry.doc;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function setCounters(id: string, helpful: number, harmful: number): Promise<void> {
|
|
36
|
-
const agentsCol = await col<AgentDoc>("agents");
|
|
37
|
-
const entry = (await agentsCol.get(id))!;
|
|
38
|
-
await agentsCol.put(id, { ...entry.doc, helpful_count: helpful, harmful_count: harmful }, { expectedVersion: entry.version });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe("curator", () => {
|
|
42
|
-
test("never disables a catalog agent, however bad its ACE counters are", async () => {
|
|
43
|
-
await setCounters(VICTIM, 0, 25);
|
|
44
|
-
|
|
45
|
-
await runCurator();
|
|
46
|
-
|
|
47
|
-
expect((await agent(VICTIM)).enabled).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("raises a reviewable proposal instead of applying the change", async () => {
|
|
51
|
-
await setCounters(VICTIM, 0, 5);
|
|
52
|
-
|
|
53
|
-
await runCurator();
|
|
54
|
-
|
|
55
|
-
const proposals = (await (await col<AgentProposalDoc>("agentProposals")).findBy("agent_id", VICTIM)).map(e => e.doc);
|
|
56
|
-
expect(proposals).toHaveLength(1);
|
|
57
|
-
expect(proposals[0].type).toBe("disable_agent");
|
|
58
|
-
expect(proposals[0].status).toBe("proposed");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("does not archive catalog agents that were never used", async () => {
|
|
62
|
-
// Seeded with lastTraceAt=null, which the inactivity cutoff reads as epoch 0
|
|
63
|
-
expect((await agent(VICTIM)).lastTraceAt).toBeNull();
|
|
64
|
-
|
|
65
|
-
await runCurator();
|
|
66
|
-
|
|
67
|
-
for (const id of CATALOG_AGENT_IDS) {
|
|
68
|
-
expect((await agent(id)).status).not.toBe("archived");
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("repairs a catalog agent archived by an older release on boot", async () => {
|
|
73
|
-
const agentsCol = await col<AgentDoc>("agents");
|
|
74
|
-
const existing = (await agentsCol.get(VICTIM))!;
|
|
75
|
-
await agentsCol.put(
|
|
76
|
-
VICTIM,
|
|
77
|
-
{ ...existing.doc, status: "archived" },
|
|
78
|
-
{ expectedVersion: existing.version },
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
await ensureHiveDb();
|
|
82
|
-
|
|
83
|
-
expect((await agent(VICTIM)).status).toBe("idle");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("does not archive an inactive worker automatically", async () => {
|
|
87
|
-
const agentsCol = await col<AgentDoc>("agents");
|
|
88
|
-
const stale = { ...(await agent(VICTIM)), id: "stale-worker", source: "user" as const, lastTraceAt: 1 };
|
|
89
|
-
await agentsCol.put(stale.id, stale);
|
|
90
|
-
|
|
91
|
-
await runCurator();
|
|
92
|
-
|
|
93
|
-
expect((await agent("stale-worker")).status).not.toBe("archived");
|
|
94
|
-
expect((await agent("stale-worker")).enabled).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe("agent_archive tool", () => {
|
|
99
|
-
test("refuses to archive a catalog agent", async () => {
|
|
100
|
-
const result = await agentArchiveTool.execute({ agentId: VICTIM }) as { ok: boolean; error?: string };
|
|
101
|
-
|
|
102
|
-
expect(result.ok).toBe(false);
|
|
103
|
-
expect(result.error).toContain("catalog agent");
|
|
104
|
-
expect((await agent(VICTIM)).enabled).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("still archives a worker created by the coordinator", async () => {
|
|
108
|
-
const agentsCol = await col<AgentDoc>("agents");
|
|
109
|
-
const worker = { ...(await agent(VICTIM)), id: "spawned-worker", source: "user" as const };
|
|
110
|
-
await agentsCol.put(worker.id, worker);
|
|
111
|
-
|
|
112
|
-
const result = await agentArchiveTool.execute({ agentId: "spawned-worker" }) as { ok: boolean };
|
|
113
|
-
|
|
114
|
-
expect(result.ok).toBe(true);
|
|
115
|
-
expect((await agent("spawned-worker")).enabled).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Track B feasibility proof: HiveDB's subscribe()/events() (wrapped by
|
|
3
|
-
* watchCausalEvents()) actually works for hive's use case — forward
|
|
4
|
-
* delivery, pattern filtering, and clean shutdown — and explicitly proves
|
|
5
|
-
* the "no historical replay" constraint documented in causal-events.ts
|
|
6
|
-
* (subscribe()/events() are a pure in-process pub/sub, not a durable read
|
|
7
|
-
* of the log — see hiveBD's reactive.rs).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
process.env.HIVE_DB_PATH = ":memory:";
|
|
11
|
-
|
|
12
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
13
|
-
import { closeHiveDb, getHiveDb } from "../packages/core/src/storage/hivedb";
|
|
14
|
-
import { ensureHiveDb } from "../packages/core/src/storage/bootstrap";
|
|
15
|
-
import { resetBootId } from "../packages/core/src/storage/boot-id";
|
|
16
|
-
import { watchCausalEvents } from "../packages/core/src/storage/causal-events";
|
|
17
|
-
import type { CausalEvent } from "../packages/core/src/storage/causal-events";
|
|
18
|
-
|
|
19
|
-
beforeEach(async () => {
|
|
20
|
-
closeHiveDb();
|
|
21
|
-
resetBootId();
|
|
22
|
-
await ensureHiveDb();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
closeHiveDb();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("causal-events: watchCausalEvents()", () => {
|
|
30
|
-
test("delivers only events matching the pattern, forward, in order", async () => {
|
|
31
|
-
const stream = await watchCausalEvents({ agentId: "agent-x" });
|
|
32
|
-
const db = await getHiveDb();
|
|
33
|
-
|
|
34
|
-
await db.append({
|
|
35
|
-
agentId: "agent-x",
|
|
36
|
-
streamId: "s1",
|
|
37
|
-
kind: "IntentLogged",
|
|
38
|
-
payload: JSON.stringify({ actor: "agent-x", intent: "one" }),
|
|
39
|
-
});
|
|
40
|
-
await db.append({
|
|
41
|
-
agentId: "agent-other",
|
|
42
|
-
streamId: "s2",
|
|
43
|
-
kind: "IntentLogged",
|
|
44
|
-
payload: JSON.stringify({ actor: "agent-other", intent: "should be filtered out" }),
|
|
45
|
-
});
|
|
46
|
-
await db.append({
|
|
47
|
-
agentId: "agent-x",
|
|
48
|
-
streamId: "s1",
|
|
49
|
-
kind: "StateTransition",
|
|
50
|
-
payload: JSON.stringify({ description: "three" }),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const received: CausalEvent[] = [];
|
|
54
|
-
const iterator = stream[Symbol.asyncIterator]();
|
|
55
|
-
received.push((await iterator.next()).value!);
|
|
56
|
-
received.push((await iterator.next()).value!);
|
|
57
|
-
stream.close();
|
|
58
|
-
|
|
59
|
-
expect(received.length).toBe(2);
|
|
60
|
-
expect(received.every((e) => e.agentId === "agent-x")).toBe(true);
|
|
61
|
-
expect(received.map((e) => e.kindTag)).toEqual(["IntentLogged", "StateTransition"]);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("has no historical replay: events appended before subscribing are never delivered", async () => {
|
|
65
|
-
const db = await getHiveDb();
|
|
66
|
-
await db.append({
|
|
67
|
-
agentId: "agent-y",
|
|
68
|
-
streamId: "s1",
|
|
69
|
-
kind: "IntentLogged",
|
|
70
|
-
payload: JSON.stringify({ actor: "agent-y", intent: "pre-existing, must not be delivered" }),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Subscribe AFTER the append above.
|
|
74
|
-
const stream = await watchCausalEvents({ agentId: "agent-y" });
|
|
75
|
-
const iterator = stream[Symbol.asyncIterator]();
|
|
76
|
-
|
|
77
|
-
const raceResult = await Promise.race([
|
|
78
|
-
iterator.next().then((r) => ({ type: "event" as const, value: r.value })),
|
|
79
|
-
new Promise<{ type: "timeout" }>((resolve) => setTimeout(() => resolve({ type: "timeout" }), 200)),
|
|
80
|
-
]);
|
|
81
|
-
expect(raceResult.type).toBe("timeout");
|
|
82
|
-
|
|
83
|
-
// Prove the stream itself still works for a genuinely new event —
|
|
84
|
-
// distinguishes "no replay" from "the stream is just broken".
|
|
85
|
-
await db.append({
|
|
86
|
-
agentId: "agent-y",
|
|
87
|
-
streamId: "s1",
|
|
88
|
-
kind: "StateTransition",
|
|
89
|
-
payload: JSON.stringify({ description: "post-subscribe, must be delivered" }),
|
|
90
|
-
});
|
|
91
|
-
const { value: event } = await iterator.next();
|
|
92
|
-
expect(event?.kindTag).toBe("StateTransition");
|
|
93
|
-
|
|
94
|
-
stream.close();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("close() ends the for-await loop cleanly and doesn't block a subsequent closeHiveDb()", async () => {
|
|
98
|
-
const stream = await watchCausalEvents({ agentId: "agent-z" });
|
|
99
|
-
const db = await getHiveDb();
|
|
100
|
-
await db.append({
|
|
101
|
-
agentId: "agent-z",
|
|
102
|
-
streamId: "s1",
|
|
103
|
-
kind: "IntentLogged",
|
|
104
|
-
payload: JSON.stringify({ actor: "agent-z", intent: "one" }),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const collected: CausalEvent[] = [];
|
|
108
|
-
for await (const event of stream) {
|
|
109
|
-
collected.push(event);
|
|
110
|
-
stream.close();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
expect(collected.length).toBe(1);
|
|
114
|
-
// afterEach's closeHiveDb() must not hang/throw with this subscription
|
|
115
|
-
// closed-but-not-drained — implicitly verified by the test completing.
|
|
116
|
-
});
|
|
117
|
-
});
|
package/test/compaction.test.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure-function tests for compaction.ts's cut-point and transcript-rendering
|
|
3
|
-
* helpers — extracted so they're testable without a real LLM call
|
|
4
|
-
* (compactThread() itself calls callLLM and needs network).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, test, expect } from "bun:test";
|
|
8
|
-
import { findCompactionCutIndex, renderTranscript } from "../packages/core/src/agent/compaction";
|
|
9
|
-
import type { StoredMessage } from "../packages/core/src/agent/conversation-store";
|
|
10
|
-
|
|
11
|
-
function msg(partial: Partial<StoredMessage> & Pick<StoredMessage, "id" | "role" | "content">): StoredMessage {
|
|
12
|
-
return {
|
|
13
|
-
thread_id: "t",
|
|
14
|
-
channel: "webchat",
|
|
15
|
-
source: "message",
|
|
16
|
-
tool_calls_json: null,
|
|
17
|
-
tool_call_id: null,
|
|
18
|
-
reasoning_content: null,
|
|
19
|
-
content_multimodal: null,
|
|
20
|
-
token_count: 1,
|
|
21
|
-
created_at: Date.now(),
|
|
22
|
-
...partial,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe("findCompactionCutIndex", () => {
|
|
27
|
-
test("finds a clean user-turn boundary near the end of the history", () => {
|
|
28
|
-
const rows: StoredMessage[] = [
|
|
29
|
-
msg({ id: 1, role: "user", content: "a" }),
|
|
30
|
-
msg({ id: 2, role: "assistant", content: "b" }),
|
|
31
|
-
msg({ id: 3, role: "user", content: "c" }),
|
|
32
|
-
msg({ id: 4, role: "assistant", content: "d" }),
|
|
33
|
-
msg({ id: 5, role: "user", content: "e" }),
|
|
34
|
-
msg({ id: 6, role: "assistant", content: "f" }),
|
|
35
|
-
msg({ id: 7, role: "user", content: "g" }),
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
// keepLastN=3 -> naive cut at index 4 (rows[4].role === "user") — already clean.
|
|
39
|
-
expect(findCompactionCutIndex(rows, 3)).toBe(4);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("walks back to the nearest user turn when the naive cut lands on assistant", () => {
|
|
43
|
-
const rows: StoredMessage[] = [
|
|
44
|
-
msg({ id: 1, role: "user", content: "a" }),
|
|
45
|
-
msg({ id: 2, role: "user", content: "b" }), // internal event, still role:user
|
|
46
|
-
msg({ id: 3, role: "assistant", content: "c" }),
|
|
47
|
-
msg({ id: 4, role: "assistant", content: "d" }),
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
// keepLastN=1 -> naive cut at index 3 (assistant) -> walk back to index 1 (user).
|
|
51
|
-
expect(findCompactionCutIndex(rows, 1)).toBe(1);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("a tail of internal events (role:user) does not block the cut point", () => {
|
|
55
|
-
const rows: StoredMessage[] = [
|
|
56
|
-
msg({ id: 1, role: "user", content: "a" }),
|
|
57
|
-
msg({ id: 2, role: "assistant", content: "b" }),
|
|
58
|
-
msg({ id: 3, role: "user", content: "El agente completó la tarea X.", source: "task_complete" }),
|
|
59
|
-
msg({ id: 4, role: "user", content: "El agente completó la tarea Y.", source: "delegation_summary" }),
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
// keepLastN=2 -> naive cut at index 2, which IS a user turn (the internal
|
|
63
|
-
// event) — before source-based framing existed, this was persisted as
|
|
64
|
-
// role:"system" and would have blocked the cut entirely.
|
|
65
|
-
expect(findCompactionCutIndex(rows, 2)).toBe(2);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("returns 0 when no clean boundary exists (caller should skip compaction)", () => {
|
|
69
|
-
const rows: StoredMessage[] = [
|
|
70
|
-
msg({ id: 1, role: "assistant", content: "a" }),
|
|
71
|
-
msg({ id: 2, role: "assistant", content: "b" }),
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
expect(findCompactionCutIndex(rows, 1)).toBe(0);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("renderTranscript", () => {
|
|
79
|
-
test("labels normal rows by role", () => {
|
|
80
|
-
const rows: StoredMessage[] = [
|
|
81
|
-
msg({ id: 1, role: "user", content: "hola" }),
|
|
82
|
-
msg({ id: 2, role: "assistant", content: "¿en qué te ayudo?" }),
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
const transcript = renderTranscript(rows);
|
|
86
|
-
expect(transcript).toContain("[USER]: hola");
|
|
87
|
-
expect(transcript).toContain("[ASSISTANT]: ¿en qué te ayudo?");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("labels internal-source rows [EVENTO INTERNO], never [USER]", () => {
|
|
91
|
-
const rows: StoredMessage[] = [
|
|
92
|
-
msg({ id: 1, role: "user", content: "El agente completó la tarea X.", source: "task_complete" }),
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
const transcript = renderTranscript(rows);
|
|
96
|
-
expect(transcript).toContain("[EVENTO INTERNO]: El agente completó la tarea X.");
|
|
97
|
-
expect(transcript).not.toContain("[USER]");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("truncates each message to maxMsgChars", () => {
|
|
101
|
-
const rows: StoredMessage[] = [msg({ id: 1, role: "user", content: "x".repeat(500) })];
|
|
102
|
-
const transcript = renderTranscript(rows, 10);
|
|
103
|
-
expect(transcript).toBe(`[USER]: ${"x".repeat(10)}`);
|
|
104
|
-
});
|
|
105
|
-
});
|