252 lines
14 KiB
TypeScript
252 lines
14 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { decodeConversationItem, parseEnvelopeJSON } from "@/runtime/protocol/lineup-v1";
|
|
import { InteractionKernel } from "@/runtime/coordination/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: { action_group: { mode: "single-choice", actions: [{ id: "alpha", label: "Alpha" }] } } },
|
|
}),
|
|
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: { action_group: { mode: "button", actions: [{ id: "beta", label: "Beta" }] } } },
|
|
}),
|
|
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" })]);
|
|
});
|
|
|
|
it("accepts only a bounded, uniquely identified ActionGroup for choice calls", () => {
|
|
const kernel = new InteractionKernel();
|
|
const accepted = kernel.ingest({
|
|
raw: envelope({
|
|
id: "evt_choice_group",
|
|
type: "lineup.v1.tool.call",
|
|
payload: {
|
|
call_id: "call_choice_group",
|
|
tool: "choice",
|
|
title: "Pick deployment targets",
|
|
prompt: "You may select multiple targets.",
|
|
data: { action_group: { mode: "multi-choice", actions: [{ id: "staging", label: "Staging" }, { id: "production", label: "Production", description: "Requires approval" }] } },
|
|
},
|
|
}),
|
|
source: "sync",
|
|
});
|
|
const duplicateActionID = kernel.ingest({
|
|
raw: envelope({
|
|
id: "evt_choice_invalid_group",
|
|
type: "lineup.v1.tool.call",
|
|
payload: { call_id: "call_bad_group", tool: "choice", title: "Bad", prompt: "Bad", data: { action_group: { mode: "multi-choice", actions: [{ id: "same", label: "One" }, { id: "same", label: "Two" }] } } },
|
|
}),
|
|
source: "sync",
|
|
});
|
|
|
|
expect(accepted.items).toEqual([expect.objectContaining({
|
|
kind: "tool-call",
|
|
call: expect.objectContaining({ action_group: { mode: "multi-choice", actions: [{ id: "staging", label: "Staging" }, { id: "production", label: "Production", description: "Requires approval" }] } }),
|
|
})]);
|
|
expect(duplicateActionID.items).toEqual([expect.objectContaining({ kind: "fallback", reason: "invalid_payload" })]);
|
|
});
|
|
|
|
it("accepts trusted confirm and input schemas while rejecting unsafe form fields", () => {
|
|
const kernel = new InteractionKernel();
|
|
const confirm = kernel.ingest({
|
|
raw: envelope({ id: "evt_confirm", type: "lineup.v1.tool.call", payload: { call_id: "call_confirm", tool: "confirm", title: "Continue?", prompt: "This can be cancelled.", data: { confirm: { approve_label: "Continue", cancel_label: "Stop" } } } }),
|
|
source: "sync",
|
|
});
|
|
const form = kernel.ingest({
|
|
raw: envelope({ id: "evt_input", type: "lineup.v1.tool.call", payload: { call_id: "call_input", tool: "input", title: "Release", prompt: "Version and notes", data: { form: { fields: [{ id: "version", label: "Version", type: "text", required: true, pattern: "^v\\d+\\.\\d+\\.\\d+$" }, { id: "notes", label: "Notes", type: "textarea", max_length: 200 }], submit_label: "Create", cancel_label: "Cancel" } } } }),
|
|
source: "sync",
|
|
});
|
|
const invalid = kernel.ingest({
|
|
raw: envelope({ id: "evt_input_invalid", type: "lineup.v1.tool.call", payload: { call_id: "call_input_invalid", tool: "input", title: "Bad", prompt: "Bad", data: { form: { fields: [{ id: "bad", label: "Bad", type: "file" }] } } } }),
|
|
source: "sync",
|
|
});
|
|
|
|
expect(confirm.items).toEqual([expect.objectContaining({ kind: "tool-call", call: expect.objectContaining({ confirm: { approve_label: "Continue", cancel_label: "Stop" } }) })]);
|
|
expect(form.items).toEqual([expect.objectContaining({ kind: "tool-call", call: expect.objectContaining({ form: expect.objectContaining({ fields: expect.arrayContaining([expect.objectContaining({ id: "version", required: true })]) }) }) })]);
|
|
expect(invalid.items).toEqual([expect.objectContaining({ kind: "fallback", reason: "invalid_payload" })]);
|
|
});
|
|
|
|
it("aggregates structured task progress by operation_id and exposes a local cancellation request", () => {
|
|
const kernel = new InteractionKernel();
|
|
const first = kernel.ingest({
|
|
raw: envelope({ id: "evt_task_first", type: "lineup.v1.agent.progress", payload: { operation_id: "task_kernel_001", title: "Build report", percent: 15, status: "running", cancellable: true } }),
|
|
source: "sync",
|
|
});
|
|
const update = kernel.ingest({
|
|
raw: envelope({ id: "evt_task_update", type: "lineup.v1.agent.progress", payload: { operation_id: "task_kernel_001", title: "Build report", percent: 65, status: "running", cancellable: true } }),
|
|
source: "live",
|
|
});
|
|
|
|
expect(first.snapshot.tasks).toEqual([expect.objectContaining({ operation_id: "task_kernel_001", percent: 15 })]);
|
|
expect(update.snapshot.tasks).toEqual([expect.objectContaining({ operation_id: "task_kernel_001", percent: 65 })]);
|
|
expect(kernel.requestTaskCancel("task_kernel_001", "2026-08-03T09:05:00.000Z")).toMatchObject({ disposition: "accepted" });
|
|
expect(kernel.snapshot().tasks).toEqual([expect.objectContaining({ cancel_requested_at: "2026-08-03T09:05:00.000Z" })]);
|
|
});
|
|
|
|
it("admits only app identity and state for a Surface open, never an Agent-provided bundle", () => {
|
|
const safe = decodeConversationItem(envelope({
|
|
id: "evt_surface_safe",
|
|
type: "lineup.v1.ui.open",
|
|
payload: { instance_id: "task:001", app: { app_id: "lineup.task-dashboard", version: "0.1.0" }, state: { title: "Build" } },
|
|
}));
|
|
const injectedBundle = decodeConversationItem(envelope({
|
|
id: "evt_surface_bundle",
|
|
type: "lineup.v1.ui.open",
|
|
payload: { instance_id: "task:002", app: { app_id: "lineup.task-dashboard", version: "0.1.0" }, bundle_url: "https://example.invalid/app.js" },
|
|
}));
|
|
const extraAppField = decodeConversationItem(envelope({
|
|
id: "evt_surface_extra_app",
|
|
type: "lineup.v1.ui.open",
|
|
payload: { instance_id: "task:003", app: { app_id: "lineup.task-dashboard", version: "0.1.0", source: "injected" } },
|
|
}));
|
|
|
|
expect(safe).toEqual(expect.objectContaining({ kind: "surface", id: "evt_surface_safe" }));
|
|
expect(injectedBundle).toEqual(expect.objectContaining({ kind: "fallback", reason: "invalid_payload" }));
|
|
expect(extraAppField).toEqual(expect.objectContaining({ kind: "fallback", reason: "invalid_payload" }));
|
|
});
|
|
|
|
it("requires a bounded reason, expiry and exact payload shape for app calls", () => {
|
|
const valid = decodeConversationItem(envelope({
|
|
id: "evt_capability_valid", type: "lineup.v1.app.call",
|
|
payload: { call_id: "cap_001", capability: "app.open_url", reason: "打开文档", expires_at: "2026-08-04T00:00:00.000Z", arguments: { url: "https://example.com" } },
|
|
}));
|
|
const missingExpiry = decodeConversationItem(envelope({
|
|
id: "evt_capability_invalid", type: "lineup.v1.app.call",
|
|
payload: { call_id: "cap_002", capability: "app.open_url", reason: "打开文档", arguments: { url: "https://example.com" } },
|
|
}));
|
|
expect(valid).toEqual(expect.objectContaining({ kind: "app-call" }));
|
|
expect(missingExpiry).toEqual(expect.objectContaining({ kind: "fallback", reason: "invalid_payload" }));
|
|
});
|
|
|
|
it("admits artifact offers as data but rejects undeclared control fields", () => {
|
|
const safe = decodeConversationItem(envelope({
|
|
id: "evt_artifact_safe", type: "lineup.v1.artifact.offer",
|
|
payload: { artifact_id: "report_001", name: "report.pdf", mime_type: "application/pdf", size_bytes: 42, integrity: "verified", created_at: "2026-08-03T00:00:00.000Z" },
|
|
}));
|
|
const injected = decodeConversationItem(envelope({
|
|
id: "evt_artifact_injected", type: "lineup.v1.artifact.offer",
|
|
payload: { artifact_id: "report_002", name: "report.pdf", mime_type: "application/pdf", size_bytes: 42, integrity: "verified", created_at: "2026-08-03T00:00:00.000Z", url: "https://bad.example" },
|
|
}));
|
|
expect(safe).toEqual(expect.objectContaining({ kind: "artifact-offer" }));
|
|
const content = decodeConversationItem(envelope({
|
|
id: "evt_artifact_content", type: "lineup.v1.artifact.offer",
|
|
payload: { artifact_id: "report_003", name: "report.txt", mime_type: "text/plain", size_bytes: 5, integrity: "verified", created_at: "2026-08-03T00:00:00.000Z", local_ref: "artifact-cache-003", content_base64: "aGVsbG8=" },
|
|
}));
|
|
expect(content).toEqual(expect.objectContaining({ kind: "artifact-offer" }));
|
|
expect(injected).toEqual(expect.objectContaining({ kind: "fallback", reason: "invalid_payload" }));
|
|
});
|
|
});
|