130 lines
5.8 KiB
TypeScript
130 lines
5.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { parseEnvelopeJSON } from "../protocol";
|
|
import { InteractionKernel } from "./interaction-kernel";
|
|
|
|
function envelope(overrides: Record<string, unknown> = {}): string {
|
|
return JSON.stringify({
|
|
v: 1,
|
|
id: "evt_kernel_001",
|
|
type: "lineup.v1.text",
|
|
conversation_id: "conversation_kernel_test",
|
|
sender: { kind: "agent", id: "agent_test" },
|
|
payload: { markdown: "# Hello\n\n**trusted**" },
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
describe("InteractionKernel", () => {
|
|
it("accepts a valid typed envelope and projects its trusted markdown item", () => {
|
|
const kernel = new InteractionKernel();
|
|
const result = kernel.ingest({ raw: envelope(), source: "sync", transport_id: "41" });
|
|
|
|
expect(result.disposition).toBe("accepted");
|
|
expect(result.items).toEqual([expect.objectContaining({
|
|
kind: "markdown",
|
|
id: "evt_kernel_001",
|
|
markdown: "# Hello\n\n**trusted**",
|
|
})]);
|
|
expect(result.snapshot.seen_envelope_ids).toBe(1);
|
|
});
|
|
|
|
it("rejects invalid protocol JSON while preserving legacy plain text as safe markdown", () => {
|
|
const kernel = new InteractionKernel();
|
|
const invalidProtocol = parseEnvelopeJSON("{");
|
|
const legacyText = kernel.ingest({ raw: "{", source: "sync" });
|
|
const unknown = kernel.ingest({
|
|
raw: envelope({ id: "evt_kernel_unknown", type: "lineup.v1.unknown" }),
|
|
source: "live",
|
|
});
|
|
|
|
expect(invalidProtocol).toMatchObject({ ok: false, fallback: { code: "invalid_json" } });
|
|
expect(legacyText.items).toEqual([expect.objectContaining({ kind: "markdown", markdown: "{" })]);
|
|
expect(unknown.items).toEqual([expect.objectContaining({ kind: "fallback", reason: "unsupported_type", id: "evt_kernel_unknown" })]);
|
|
});
|
|
|
|
it("deduplicates one envelope id across sync and live transport sources", () => {
|
|
const kernel = new InteractionKernel();
|
|
const first = kernel.ingest({ raw: envelope(), source: "sync" });
|
|
const duplicate = kernel.ingest({ raw: envelope(), source: "live" });
|
|
|
|
expect(first.disposition).toBe("accepted");
|
|
expect(duplicate).toMatchObject({ disposition: "duplicate", items: [] });
|
|
expect(duplicate.snapshot.seen_envelope_ids).toBe(1);
|
|
});
|
|
|
|
it("projects the latest agent status by actor and retains a bounded dedupe set", () => {
|
|
const kernel = new InteractionKernel(2);
|
|
const thinking = kernel.ingest({
|
|
raw: envelope({ id: "evt_status_thinking", type: "lineup.v1.agent.status", payload: { status: "thinking", detail: "planning" } }),
|
|
source: "sync",
|
|
received_at: "2026-08-03T00:00:00.000Z",
|
|
});
|
|
kernel.ingest({ raw: envelope({ id: "evt_other", payload: { markdown: "one" } }), source: "sync" });
|
|
kernel.ingest({ raw: envelope({ id: "evt_newer", payload: { markdown: "two" } }), source: "sync" });
|
|
|
|
expect(thinking.snapshot.agent_presence).toEqual([expect.objectContaining({
|
|
status: "thinking",
|
|
detail: "planning",
|
|
envelope_id: "evt_status_thinking",
|
|
updated_at: "2026-08-03T00:00:00.000Z",
|
|
})]);
|
|
// The first id has expired from the bounded set and is accepted again.
|
|
expect(kernel.ingest({ raw: envelope({ id: "evt_status_thinking" }), source: "live" }).disposition).toBe("accepted");
|
|
});
|
|
|
|
it("projects tool calls by call_id and applies a result only after submission", () => {
|
|
const kernel = new InteractionKernel();
|
|
const call = kernel.ingest({
|
|
raw: envelope({
|
|
id: "evt_tool_call",
|
|
type: "lineup.v1.tool.call",
|
|
payload: { call_id: "call_kernel_001", tool: "choice", title: "Choose", prompt: "One", data: {} },
|
|
}),
|
|
source: "sync",
|
|
received_at: "2026-08-03T08:00:00.000Z",
|
|
});
|
|
const duplicateCallID = kernel.ingest({
|
|
raw: envelope({
|
|
id: "evt_tool_call_duplicate_id",
|
|
type: "lineup.v1.tool.call",
|
|
payload: { call_id: "call_kernel_001", tool: "choice", title: "Ignored", prompt: "Ignored", data: {} },
|
|
}),
|
|
source: "live",
|
|
});
|
|
const earlyResult = kernel.ingest({
|
|
raw: envelope({ id: "evt_tool_result_early", type: "lineup.v1.tool.result", payload: { call_id: "call_kernel_001", status: "completed", result: {} } }),
|
|
source: "live",
|
|
});
|
|
|
|
expect(call.snapshot.tool_calls).toEqual([expect.objectContaining({ call_id: "call_kernel_001", status: "pending" })]);
|
|
expect(duplicateCallID.snapshot.tool_calls).toEqual([
|
|
expect.objectContaining({ request: expect.objectContaining({ title: "Choose" }) }),
|
|
]);
|
|
expect(earlyResult.snapshot.tool_calls).toEqual([expect.objectContaining({ status: "pending" })]);
|
|
expect(kernel.submitToolCall("call_kernel_001", "2026-08-03T08:01:00.000Z")).toMatchObject({ disposition: "accepted" });
|
|
|
|
const completed = kernel.ingest({
|
|
raw: envelope({ id: "evt_tool_result_completed", type: "lineup.v1.tool.result", payload: { call_id: "call_kernel_001", status: "completed", result: { selected: "alpha" } } }),
|
|
source: "live",
|
|
});
|
|
expect(completed.snapshot.tool_calls).toEqual([
|
|
expect.objectContaining({ call_id: "call_kernel_001", status: "completed", result: { selected: "alpha" } }),
|
|
]);
|
|
});
|
|
|
|
it("keeps malformed tool payloads on the safe fallback path", () => {
|
|
const kernel = new InteractionKernel();
|
|
const invalidCall = kernel.ingest({
|
|
raw: envelope({ id: "evt_invalid_tool_call", type: "lineup.v1.tool.call", payload: { call_id: "call_bad", tool: "shell", data: {} } }),
|
|
source: "sync",
|
|
});
|
|
const invalidResult = kernel.ingest({
|
|
raw: envelope({ id: "evt_invalid_tool_result", type: "lineup.v1.tool.result", payload: { call_id: "call_bad", status: "submitted", result: {} } }),
|
|
source: "sync",
|
|
});
|
|
|
|
expect(invalidCall.items).toEqual([expect.objectContaining({ kind: "fallback", reason: "invalid_payload" })]);
|
|
expect(invalidResult.items).toEqual([expect.objectContaining({ kind: "fallback", reason: "invalid_payload" })]);
|
|
});
|
|
});
|