Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/brunch-agent/test/baseline-harness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { runNodeScript } from "./run-node-script";

import type { HarnessRunRecord } from "../../../libs/@hashintel/brunch-agent/evaluations/protocols/process-model-elicitation/baseline/harness-run.ts";

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;

const testDirectory = import.meta.dirname;
const contextRoot = join(
testDirectory,
Expand Down Expand Up @@ -94,6 +97,48 @@ test("condition 5 drives the shipped elicitor through the binding and reads the
captures: 1,
complete: false,
});
const appliedSweepPart = run.history.messages
.flatMap((message) => message.parts)
.find(
(part) =>
part.type === "dynamic-tool" &&
part.toolName === "brunch_sweep" &&
part.state === "output-available",
);
if (
appliedSweepPart?.type !== "dynamic-tool" ||
appliedSweepPart.state !== "output-available" ||
!isRecord(appliedSweepPart.output)
) {
throw new Error("The applied sweep did not expose its result.");
}
expect(appliedSweepPart.output.status).toBe("applied");
const sweepCaptures = appliedSweepPart.output.captures;
if (!Array.isArray(sweepCaptures) || !isRecord(sweepCaptures[0])) {
throw new Error("The sweep did not expose its current captures.");
}
expect(sweepCaptures).toHaveLength(1);
expect(sweepCaptures[0].status).toBe("active");
const { evidence } = sweepCaptures[0];
expect(Array.isArray(evidence)).toBe(true);
if (!Array.isArray(evidence) || !isRecord(evidence[0])) {
throw new Error("The capture did not expose its evidence.");
}
expect(evidence[0].excerpt).toBe(EXPERT_OBJECTIVE_QUOTE);
const { completion } = appliedSweepPart.output;
if (!isRecord(completion) || !Array.isArray(completion.failures)) {
throw new Error("The sweep did not expose its completion report.");
}
expect(completion.complete).toBe(false);
expect(completion.failures).toHaveLength(5);
expect(
completion.failures.every(
(failure) =>
isRecord(failure) &&
typeof failure.diagnostic === "string" &&
typeof failure.message === "string",
),
).toBe(true);

// Then the interviewer closes without asking; the runner counts three such
// turns before the wrap and declares the interview stalled — no classifier.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { expect, test } from "vitest";

import { createBrunchPanelTransport } from "./brunch-panel-transport";

import type { PetrinautAiChatTransport } from "@hashintel/petrinaut/ui";
import type { UIMessageChunk } from "ai";

const sweepChunks: UIMessageChunk[] = [
{
type: "tool-input-available",
toolCallId: "no-range",
toolName: "brunch_sweep",
input: {},
},
{
type: "tool-output-available",
toolCallId: "no-range",
output: { status: "no-settled-range" },
},
{
type: "tool-input-available",
toolCallId: "refused",
toolName: "brunch_sweep",
input: {},
},
{
type: "tool-output-available",
toolCallId: "refused",
output: {
status: "refused",
refusal: {
code: "evidence-quote-not-found",
message: "Use an exact quote.",
},
},
},
{
type: "tool-input-available",
toolCallId: "applied",
toolName: "brunch_sweep",
input: {},
},
{
type: "tool-output-available",
toolCallId: "applied",
output: {
status: "applied",
appliedCaptureIds: ["capture-1"],
captures: [
{
id: "capture-1",
status: "active",
epistemicStatus: "explicit",
confidence: "high",
content: { value: { type: "slot-asserted" } },
evidence: [{ excerpt: "Line A runs next." }],
},
{
id: "capture-old",
status: "superseded",
epistemicStatus: "inferred",
confidence: "medium",
content: { absence: "unknown" },
basis: {
type: "default-rule",
description: "The earlier default.",
},
alternativeGroup: "line-a-owner",
},
{
id: "capture-withdrawn",
status: "retracted",
epistemicStatus: "explicit",
confidence: "high",
content: { value: "Sam" },
evidence: [{ excerpt: "Not Sam after all." }],
supersedes: "capture-old",
},
],
completion: {
complete: false,
pluginVersion: "1.0.0",
revision: "revision-1",
failures: [
{
diagnostic: "unaddressed",
nodeId: "activity:line-a",
kind: "activity",
slot: "owner",
requirement: "named",
actual: "not mentioned",
message: "The owner is missing.",
captureIds: ["capture-1"],
},
],
sliceNodeIds: ["activity:line-a"],
outsideSlice: [
{
nodeId: "activity:Line-B",
kind: "activity",
open: [
{
diagnostic: "unaddressed",
nodeId: "activity:Line-B",
kind: "activity",
slot: "Owner",
requirement: "named",
actual: "not mentioned",
message: "The owner is missing.",
captureIds: ["Capture-2"],
},
],
},
],
},
},
},
];

const sourceTransport: PetrinautAiChatTransport = {
reconnectToStream: async () => null,
sendMessages: async () =>
new ReadableStream({
start(controller) {
for (const chunk of sweepChunks) controller.enqueue(chunk);
controller.close();
},
}),
};

const readChunks = async (
stream: ReadableStream<UIMessageChunk>,
): Promise<UIMessageChunk[]> => {
const chunks: UIMessageChunk[] = [];
for await (const chunk of stream) chunks.push(chunk);
return chunks;
};

test("makes every Brunch sweep outcome readable in the panel", async () => {
const transport = createBrunchPanelTransport(sourceTransport);
const stream = await transport.sendMessages({} as never);
const chunks = await readChunks(stream);
const outputs = chunks.filter(
(chunk) => chunk.type === "tool-output-available",
);

expect(outputs.map((outputChunk) => outputChunk.output)).toEqual([
{
status: "no-settled-range",
title: "No settled range to sweep",
detail: "The conversation has no settled user entries.",
},
{
status: "refused",
refusal: {
code: "evidence-quote-not-found",
message: "Use an exact quote.",
},
title: "Sweep refused",
detail: "Use an exact quote.",
items: ["Refusal: evidence-quote-not-found"],
},
{
status: "applied",
appliedCaptureIds: ["capture-1"],
captures: [
{
id: "capture-1",
status: "active",
epistemicStatus: "explicit",
confidence: "high",
content: { value: { type: "slot-asserted" } },
evidence: [{ excerpt: "Line A runs next." }],
},
{
id: "capture-old",
status: "superseded",
epistemicStatus: "inferred",
confidence: "medium",
content: { absence: "unknown" },
basis: {
type: "default-rule",
description: "The earlier default.",
},
alternativeGroup: "line-a-owner",
},
{
id: "capture-withdrawn",
status: "retracted",
epistemicStatus: "explicit",
confidence: "high",
content: { value: "Sam" },
evidence: [{ excerpt: "Not Sam after all." }],
supersedes: "capture-old",
},
],
completion: {
complete: false,
pluginVersion: "1.0.0",
revision: "revision-1",
failures: [
{
diagnostic: "unaddressed",
nodeId: "activity:line-a",
kind: "activity",
slot: "owner",
requirement: "named",
actual: "not mentioned",
message: "The owner is missing.",
captureIds: ["capture-1"],
},
],
sliceNodeIds: ["activity:line-a"],
outsideSlice: [
{
nodeId: "activity:Line-B",
kind: "activity",
open: [
{
diagnostic: "unaddressed",
nodeId: "activity:Line-B",
kind: "activity",
slot: "Owner",
requirement: "named",
actual: "not mentioned",
message: "The owner is missing.",
captureIds: ["Capture-2"],
},
],
},
],
},
title: "Sweep applied",
detail: "1 new capture · 3 total · incomplete",
items: [
'Capture capture-1 (active; explicit; confidence high): {"type":"slot-asserted"} — “Line A runs next.”',
"Capture capture-old (superseded; inferred; confidence medium): absence: unknown — default-rule: The earlier default.; alternative group line-a-owner",
'Capture capture-withdrawn (retracted; explicit; confidence high): "Sam" — “Not Sam after all.”; supersedes capture-old',
"Completion: incomplete · plugin 1.0.0 · revision revision-1",
"Completion slice: activity:line-a",
"Completion gap [unaddressed] at activity:line-a.owner: needs named; actual not mentioned. The owner is missing. Captures: capture-1",
"Outside completion slice: activity:Line-B (activity); 1 open requirement",
"Outside-slice Completion gap [unaddressed] at activity:Line-B.Owner: needs named; actual not mentioned. The owner is missing. Captures: Capture-2",
],
},
]);
});
Loading
Loading