fix(rectification): restore message actions and retry receipts
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
createRectificationActivityReceiptState,
|
||||
reduceRectificationActivityReceipt,
|
||||
receiptFromRectificationActivityState,
|
||||
} from "../src/lib/rectification-activity-receipt";
|
||||
|
||||
test("a successful retry clears the same tool's earlier failure", () => {
|
||||
let state = createRectificationActivityReceiptState();
|
||||
for (const event of [
|
||||
{ tool: "rectification-read-case", status: "completed" },
|
||||
{ tool: "rectification-propose-evidence", status: "failed" },
|
||||
{ tool: "rectification-propose-evidence", status: "started" },
|
||||
{ tool: "rectification-propose-evidence", status: "completed" },
|
||||
{ tool: "rectification-confirm-evidence", status: "completed" },
|
||||
] as const) {
|
||||
state = reduceRectificationActivityReceipt(state, event);
|
||||
}
|
||||
|
||||
assert.deepEqual(receiptFromRectificationActivityState(state), {
|
||||
steps: [
|
||||
"rectification-read-case",
|
||||
"rectification-propose-evidence",
|
||||
"rectification-confirm-evidence",
|
||||
],
|
||||
methods: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("an unrecovered latest tool failure remains visible", () => {
|
||||
let state = createRectificationActivityReceiptState();
|
||||
state = reduceRectificationActivityReceipt(state, {
|
||||
tool: "rectification-read-case",
|
||||
status: "completed",
|
||||
});
|
||||
state = reduceRectificationActivityReceipt(state, {
|
||||
tool: "rectification-propose-evidence",
|
||||
status: "failed",
|
||||
});
|
||||
|
||||
assert.deepEqual(receiptFromRectificationActivityState(state), {
|
||||
steps: ["rectification-read-case"],
|
||||
methods: [],
|
||||
failedTool: "rectification-propose-evidence",
|
||||
});
|
||||
});
|
||||
|
||||
test("a later failure overrides an earlier completion for the same tool", () => {
|
||||
let state = createRectificationActivityReceiptState();
|
||||
state = reduceRectificationActivityReceipt(state, {
|
||||
tool: "rectification-read-case",
|
||||
status: "completed",
|
||||
});
|
||||
state = reduceRectificationActivityReceipt(state, {
|
||||
tool: "rectification-read-case",
|
||||
status: "failed",
|
||||
});
|
||||
|
||||
assert.deepEqual(receiptFromRectificationActivityState(state), {
|
||||
steps: ["rectification-read-case"],
|
||||
methods: [],
|
||||
failedTool: "rectification-read-case",
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,13 @@ const completedActivityReceipt = readFileSync(
|
||||
new URL("../src/components/completed-activity-receipt.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const regenerateRoute = readFileSync(
|
||||
new URL(
|
||||
"../src/app/api/rectification/cases/[caseId]/turns/[turnId]/regenerate/route.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("birth-time rectification entry mounts the V9 case-ref chat", () => {
|
||||
assert.match(component, /return <RectificationAgenticChat \{\.\.\.props\} \/>/);
|
||||
@@ -193,18 +200,21 @@ test("rectification activity separates live work from the receipt above the Agen
|
||||
assert.match(activityHelper, /filter\(isPublicRectificationMethod\)/);
|
||||
assert.match(chat, /activeActivity\?: PublicActivity/);
|
||||
assert.match(chat, /completedReceipt\?: CompletedActivityReceiptView/);
|
||||
assert.match(chat, /showActivity=\{message\.state === "thinking" \|\| Boolean\(message\.activeActivity\)\}/);
|
||||
assert.match(chat, /showActivity=\{displayedMessage\.state === "thinking" \|\| Boolean\(displayedMessage\.activeActivity\)\}/);
|
||||
assert.match(chat, /"rectification-read-case": "正在读取校正记录…"/);
|
||||
assert.match(chat, /event\.type === "tool\.activity"/);
|
||||
assert.match(chat, /event\.status === "started"/);
|
||||
assert.match(chat, /event\.status === "completed"/);
|
||||
assert.match(chat, /event\.status === "failed"/);
|
||||
assert.match(chat, /event\.status !== "completed" && event\.status !== "failed"/);
|
||||
|
||||
const messageRender = chat.slice(
|
||||
chat.indexOf('{messages.map((message) => ('),
|
||||
chat.indexOf('{candidateResult?.selectionAllowed'),
|
||||
chat.indexOf("{messages.map((message) => {"),
|
||||
chat.indexOf("{candidateResult?.selectionAllowed"),
|
||||
);
|
||||
assert.ok(messageRender.indexOf("<CompletedActivityReceipt") < messageRender.indexOf("<ChatMessageRow"));
|
||||
const receiptIndex = messageRender.indexOf("<CompletedActivityReceipt");
|
||||
const replyIndex = messageRender.indexOf("<ChatMessageRow");
|
||||
assert.ok(receiptIndex >= 0 && replyIndex >= 0);
|
||||
assert.ok(receiptIndex < replyIndex);
|
||||
assert.match(completedActivityReceipt, /<details className=/);
|
||||
assert.doesNotMatch(completedActivityReceipt, /<details[^>]*\sopen/);
|
||||
assert.match(completedActivityReceipt, /本轮完成 · \$\{stepLabels\.length\} 个步骤 · \$\{receipt\.methods\.length\} 项计算依据/);
|
||||
@@ -223,6 +233,30 @@ test("rectification activity separates live work from the receipt above the Agen
|
||||
assert.doesNotMatch(receiptRule, /border:/);
|
||||
});
|
||||
|
||||
test("completed Agent replies restore feedback, copy and safe in-place regeneration actions", () => {
|
||||
for (const label of ["赞", "踩", "复制回答", "重新生成回答"]) {
|
||||
assert.match(chat, new RegExp(`aria-label="${label}"`));
|
||||
}
|
||||
assert.match(chat, /toggleRectificationFeedback/);
|
||||
assert.match(chat, /navigator\.clipboard\.writeText\(message\.text\)/);
|
||||
assert.match(chat, /\/turns\/\$\{encodeURIComponent\(message\.turnId\)\}\/regenerate/);
|
||||
assert.match(chat, /requestId: globalThis\.crypto\.randomUUID\(\)/);
|
||||
|
||||
const messageRender = chat.slice(
|
||||
chat.indexOf("{messages.map((message) => {"),
|
||||
chat.indexOf("{candidateResult?.selectionAllowed"),
|
||||
);
|
||||
const receiptIndex = messageRender.indexOf("<CompletedActivityReceipt");
|
||||
const replyIndex = messageRender.indexOf("<ChatMessageRow");
|
||||
const actionsIndex = messageRender.indexOf('className="rectification-message-actions"');
|
||||
assert.ok(receiptIndex >= 0 && replyIndex >= 0 && actionsIndex >= 0);
|
||||
assert.ok(receiptIndex < replyIndex && replyIndex < actionsIndex);
|
||||
|
||||
assert.doesNotMatch(chat, /send\("message",\s*(?:oldText|previousText)/);
|
||||
assert.doesNotMatch(regenerateRoute, /consultation-billing|reserveConsultation|billing\./);
|
||||
assert.match(regenerateRoute, /regenerateV9AssistantTurn/);
|
||||
});
|
||||
|
||||
test("candidate state renders from the snapshot API and never from sentinels", () => {
|
||||
assert.match(chat, /当前可能的出生时间/);
|
||||
assert.match(chat, /可以先采用一个作为当前排盘时间,也可以继续补充事件/);
|
||||
|
||||
@@ -10,9 +10,13 @@ import {
|
||||
RECTIFICATION_V9_SKILL_PATH,
|
||||
RECTIFICATION_V9_SKILL_NAME,
|
||||
getRectificationV9Agent,
|
||||
getRectificationV9RegenerationAgent,
|
||||
resolveRectificationStepBudget,
|
||||
} from "../src/mastra/agentic-rectification.ts";
|
||||
import { createRectificationV9Tools } from "../src/mastra/rectification-v9-tools.ts";
|
||||
import {
|
||||
createRectificationV9ReadOnlyTools,
|
||||
createRectificationV9Tools,
|
||||
} from "../src/mastra/rectification-v9-tools.ts";
|
||||
import { runV9AgentTurn, type V9AgentRunOptions } from "../src/lib/rectification-agentic/v9/agent-run.ts";
|
||||
import { persistV9Candidate } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import { RECTIFICATION_SKILL_NAME, RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
||||
@@ -464,3 +468,33 @@ test("agent construction wires the pinned skill and the ten v9 tools", () => {
|
||||
assert.equal(agent.id, "rectification-v9-test-model");
|
||||
assert.ok(agent);
|
||||
});
|
||||
|
||||
test("reply regeneration is a separate Jyotisha agent with only read-case access", () => {
|
||||
const model = {
|
||||
id: "test-model",
|
||||
label: "Test",
|
||||
description: "",
|
||||
creditCost: 1,
|
||||
isDefault: true,
|
||||
mode: "compatible" as const,
|
||||
model: { provider: "openai", name: "gpt-4o-mini", modelId: "gpt-4o-mini" } as never,
|
||||
};
|
||||
const accounting = fakeAccounting({});
|
||||
const tools = createRectificationV9ReadOnlyTools({
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
turnId: TURN_ID,
|
||||
accounting: accounting.client as never,
|
||||
});
|
||||
assert.deepEqual(Object.keys(tools), ["rectification-read-case"]);
|
||||
|
||||
const agent = getRectificationV9RegenerationAgent(model, {
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
turnId: TURN_ID,
|
||||
accounting: accounting.client as never,
|
||||
});
|
||||
assert.equal(agent.id, "rectification-v9-regeneration-test-model");
|
||||
assert.match(agentSource, /这不是新一轮校正/);
|
||||
assert.match(agentSource, /只能使用 rectification-read-case/);
|
||||
});
|
||||
|
||||
@@ -600,3 +600,63 @@ test("candidate reselection migration stays out of the identity migration tree",
|
||||
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 20260813060000_rectification_turn_regeneration.sql
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const turnRegenerationMigration = readFileSync(
|
||||
new URL(
|
||||
"../supabase/migrations/20260813060000_rectification_turn_regeneration.sql",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const turnRegenerationMigrationCopy = fileURLToPath(
|
||||
new URL(
|
||||
"../db/migrations/20260813060000_rectification_turn_regeneration.sql",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
test("turn regeneration migration is forward-only, transactional and business-tree only", () => {
|
||||
assert.ok(
|
||||
"20260813060000_rectification_turn_regeneration.sql"
|
||||
> "20260813050000_allow_rectification_candidate_reselection.sql",
|
||||
);
|
||||
assert.match(turnRegenerationMigration, /^begin;[\s\S]*^commit;$/m);
|
||||
assert.equal(
|
||||
existsSync(turnRegenerationMigrationCopy),
|
||||
false,
|
||||
"business migration must not be copied into frontend/db/migrations (BUG-127/BUG-144)",
|
||||
);
|
||||
});
|
||||
|
||||
test("turn regeneration is ownership-bound, latest-only and request-id idempotent", () => {
|
||||
assert.match(turnRegenerationMigration, /where id = p_case_id and user_id = p_user_id/);
|
||||
assert.match(turnRegenerationMigration, /if v_case\.session_id <> p_session_id/);
|
||||
assert.match(turnRegenerationMigration, /where request_id = p_request_id/);
|
||||
assert.match(turnRegenerationMigration, /agentic_rectification_regeneration_request_conflict/);
|
||||
assert.match(turnRegenerationMigration, /pg_advisory_xact_lock/);
|
||||
assert.match(turnRegenerationMigration, /v_case\.status in \('confirmed', 'closed', 'abandoned', 'superseded'\)/);
|
||||
assert.match(turnRegenerationMigration, /where id = p_turn_id and case_id = p_case_id/);
|
||||
assert.match(turnRegenerationMigration, /v_turn\.status <> 'completed'/);
|
||||
assert.match(turnRegenerationMigration, /order by created_at desc, id desc[\s\S]*limit 1/);
|
||||
assert.match(turnRegenerationMigration, /v_latest_turn_id is distinct from p_turn_id/);
|
||||
});
|
||||
|
||||
test("turn regeneration updates only the existing Assistant text and timestamp", () => {
|
||||
const update = turnRegenerationMigration.slice(
|
||||
turnRegenerationMigration.indexOf("update public.agentic_rectification_turns"),
|
||||
turnRegenerationMigration.indexOf("insert into public.agentic_rectification_turn_regenerations"),
|
||||
);
|
||||
const setClause = update.slice(update.indexOf("set "), update.indexOf("where "));
|
||||
assert.match(update, /set assistant_message = v_message,[\s\S]*updated_at = pg_catalog\.now\(\)/);
|
||||
assert.doesNotMatch(setClause, /user_message|status\s*=|case_id\s*=|session_id\s*=/);
|
||||
assert.doesNotMatch(
|
||||
turnRegenerationMigration,
|
||||
/update public\.(?:agentic_rectification_evidence|agentic_rectification_results|agentic_rectification_cases|profiles)/,
|
||||
);
|
||||
assert.match(turnRegenerationMigration, /create or replace function public\.get_agentic_rectification_turn_regeneration/);
|
||||
assert.match(turnRegenerationMigration, /create or replace function public\.regenerate_agentic_rectification_turn/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
regenerateV9AssistantTurn,
|
||||
type RectificationRegenerationAgent,
|
||||
} from "../src/lib/rectification-agentic/v9/regenerate-turn.ts";
|
||||
import { RectificationToolServiceError } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import {
|
||||
CASE_ID,
|
||||
SESSION_ID,
|
||||
SOURCE_TURN_ID,
|
||||
TURN_ID,
|
||||
USER_ID,
|
||||
dossierFixture,
|
||||
fakeAccounting,
|
||||
} from "./rectification-v9-test-support.ts";
|
||||
|
||||
const REQUEST_ID = "88888888-8888-4888-8888-888888888888";
|
||||
const OLD_REPLY = "旧的 Agent 正文";
|
||||
const NEW_REPLY = "新的自然回复";
|
||||
|
||||
function completedTurns() {
|
||||
return [
|
||||
{
|
||||
id: TURN_ID,
|
||||
role: "user",
|
||||
text: "2020 年 4 月开始实习",
|
||||
status: "completed",
|
||||
created_at: "2026-08-13T01:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: TURN_ID,
|
||||
role: "assistant",
|
||||
text: OLD_REPLY,
|
||||
status: "completed",
|
||||
created_at: "2026-08-13T01:00:00.000Z",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function fakeAgent(overrides: Partial<RectificationRegenerationAgent> = {}) {
|
||||
const calls = { getSkill: 0, generate: 0 };
|
||||
const agent: RectificationRegenerationAgent = {
|
||||
async getSkill() {
|
||||
calls.getSkill += 1;
|
||||
return { name: "jyotish-birth-time-rectification" };
|
||||
},
|
||||
async generate() {
|
||||
calls.generate += 1;
|
||||
return { text: NEW_REPLY };
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
return { agent, calls };
|
||||
}
|
||||
|
||||
function validAccounting() {
|
||||
return fakeAccounting({
|
||||
get_agentic_rectification_turn_regeneration: () => null,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({ turns: completedTurns() }),
|
||||
regenerate_agentic_rectification_turn: () => ({
|
||||
ok: true,
|
||||
turn_id: TURN_ID,
|
||||
assistant_message: NEW_REPLY,
|
||||
idempotent: false,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function options(
|
||||
accounting: ReturnType<typeof fakeAccounting>["client"],
|
||||
agent: RectificationRegenerationAgent,
|
||||
overrides: Partial<Parameters<typeof regenerateV9AssistantTurn>[0]> = {},
|
||||
) {
|
||||
return {
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
sessionId: SESSION_ID,
|
||||
turnId: TURN_ID,
|
||||
requestId: REQUEST_ID,
|
||||
accounting,
|
||||
agent,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function rejectsWithCode(promise: Promise<unknown>, code: string) {
|
||||
await assert.rejects(promise, (error: unknown) => (
|
||||
error instanceof RectificationToolServiceError && error.code === code
|
||||
));
|
||||
}
|
||||
|
||||
test("regeneration replaces the latest completed Assistant text in place", async () => {
|
||||
const accounting = validAccounting();
|
||||
const { agent, calls } = fakeAgent();
|
||||
|
||||
const result = await regenerateV9AssistantTurn(options(accounting.client, agent));
|
||||
|
||||
assert.deepEqual(result, {
|
||||
ok: true,
|
||||
turnId: TURN_ID,
|
||||
assistantMessage: NEW_REPLY,
|
||||
idempotent: false,
|
||||
});
|
||||
assert.equal(calls.getSkill, 1);
|
||||
assert.equal(calls.generate, 1);
|
||||
assert.deepEqual(accounting.calls.map((call) => call.fn), [
|
||||
"get_agentic_rectification_turn_regeneration",
|
||||
"get_agentic_rectification_case_dossier",
|
||||
"regenerate_agentic_rectification_turn",
|
||||
]);
|
||||
const update = accounting.calls.at(-1);
|
||||
assert.equal(update?.args.p_assistant_message, NEW_REPLY);
|
||||
assert.equal(accounting.calls.some((call) => call.fn === "append_agentic_rectification_turn"), false);
|
||||
});
|
||||
|
||||
test("same request id returns the stored replacement without invoking the Agent again", async () => {
|
||||
const accounting = fakeAccounting({
|
||||
get_agentic_rectification_turn_regeneration: () => ({
|
||||
ok: true,
|
||||
turn_id: TURN_ID,
|
||||
assistant_message: NEW_REPLY,
|
||||
idempotent: true,
|
||||
}),
|
||||
});
|
||||
const { agent, calls } = fakeAgent();
|
||||
|
||||
const result = await regenerateV9AssistantTurn(options(accounting.client, agent));
|
||||
|
||||
assert.equal(result.idempotent, true);
|
||||
assert.equal(calls.getSkill, 0);
|
||||
assert.equal(calls.generate, 0);
|
||||
assert.deepEqual(accounting.calls.map((call) => call.fn), [
|
||||
"get_agentic_rectification_turn_regeneration",
|
||||
]);
|
||||
});
|
||||
|
||||
test("terminal cases cannot regenerate replies", async () => {
|
||||
const accounting = fakeAccounting({
|
||||
get_agentic_rectification_turn_regeneration: () => null,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
status: "closed",
|
||||
turns: completedTurns(),
|
||||
}),
|
||||
});
|
||||
const { agent, calls } = fakeAgent();
|
||||
|
||||
await rejectsWithCode(
|
||||
regenerateV9AssistantTurn(options(accounting.client, agent)),
|
||||
"agentic_rectification_case_terminal",
|
||||
);
|
||||
assert.equal(calls.generate, 0);
|
||||
});
|
||||
|
||||
test("case and session must remain exactly bound", async () => {
|
||||
const accounting = validAccounting();
|
||||
const { agent, calls } = fakeAgent();
|
||||
|
||||
await rejectsWithCode(
|
||||
regenerateV9AssistantTurn(options(accounting.client, agent, {
|
||||
sessionId: "99999999-9999-4999-8999-999999999999",
|
||||
})),
|
||||
"agentic_rectification_case_session_mismatch",
|
||||
);
|
||||
assert.equal(calls.generate, 0);
|
||||
});
|
||||
|
||||
test("only the latest completed Assistant turn can be regenerated", async () => {
|
||||
const turns = [
|
||||
{
|
||||
id: SOURCE_TURN_ID,
|
||||
role: "assistant",
|
||||
text: "更早的回答",
|
||||
status: "completed",
|
||||
created_at: "2026-08-13T00:00:00.000Z",
|
||||
},
|
||||
...completedTurns(),
|
||||
];
|
||||
const accounting = fakeAccounting({
|
||||
get_agentic_rectification_turn_regeneration: () => null,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({ turns }),
|
||||
});
|
||||
const { agent, calls } = fakeAgent();
|
||||
|
||||
await rejectsWithCode(
|
||||
regenerateV9AssistantTurn(options(accounting.client, agent, { turnId: SOURCE_TURN_ID })),
|
||||
"agentic_rectification_turn_not_latest",
|
||||
);
|
||||
assert.equal(calls.generate, 0);
|
||||
assert.equal(accounting.calls.some((call) => call.fn === "regenerate_agentic_rectification_turn"), false);
|
||||
});
|
||||
|
||||
test("missing or empty completed Assistant text is rejected", async () => {
|
||||
const accounting = fakeAccounting({
|
||||
get_agentic_rectification_turn_regeneration: () => null,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
turns: [{
|
||||
id: TURN_ID,
|
||||
role: "assistant",
|
||||
text: " ",
|
||||
status: "completed",
|
||||
created_at: "2026-08-13T01:00:00.000Z",
|
||||
}],
|
||||
}),
|
||||
});
|
||||
const { agent, calls } = fakeAgent();
|
||||
|
||||
await rejectsWithCode(
|
||||
regenerateV9AssistantTurn(options(accounting.client, agent)),
|
||||
"agentic_rectification_turn_not_found",
|
||||
);
|
||||
assert.equal(calls.generate, 0);
|
||||
});
|
||||
|
||||
test("an empty regenerated body never reaches the update RPC", async () => {
|
||||
const accounting = validAccounting();
|
||||
const { agent } = fakeAgent({
|
||||
async generate() {
|
||||
return { text: " " };
|
||||
},
|
||||
});
|
||||
|
||||
await rejectsWithCode(
|
||||
regenerateV9AssistantTurn(options(accounting.client, agent)),
|
||||
"agentic_rectification_regeneration_empty",
|
||||
);
|
||||
assert.equal(accounting.calls.some((call) => call.fn === "regenerate_agentic_rectification_turn"), false);
|
||||
});
|
||||
@@ -148,6 +148,18 @@ test("streamToolNames exposes only allowlisted rectification tools", () => {
|
||||
test("safePublicEvent drops anything outside the allowlist", () => {
|
||||
assert.deepEqual(safePublicEvent({ type: "answer.delta", text: "你好" }), { type: "answer.delta", text: "你好" });
|
||||
assert.deepEqual(safePublicEvent({ type: "skill.loaded" }), { type: "skill.loaded" });
|
||||
assert.deepEqual(
|
||||
safePublicEvent({ type: "run.completed", turnId: TURN_ID }),
|
||||
{ type: "run.completed", turnId: TURN_ID },
|
||||
);
|
||||
assert.deepEqual(
|
||||
safePublicEvent({ type: "answer.delta", text: "你好", turnId: TURN_ID }),
|
||||
{ type: "answer.delta", text: "你好" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
safePublicEvent({ type: "run.completed", turnId: "not-a-uuid" }),
|
||||
{ type: "run.completed" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
safePublicEvent({
|
||||
type: "case.loaded",
|
||||
@@ -234,7 +246,7 @@ function fakeAgentStream(chunks: Array<{ type: string; payload?: Record<string,
|
||||
}
|
||||
|
||||
function runOptions(overrides: Partial<V9AgentRunOptions> = {}) {
|
||||
const emitted: Array<{ type: string; text?: string }> = [];
|
||||
const emitted: Array<{ type: string; text?: string; turnId?: string }> = [];
|
||||
const billing = { reserved: 0, completed: 0, released: 0 };
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
@@ -285,7 +297,10 @@ test("answer deltas stream in order and reasoning is never forwarded", async ()
|
||||
{ type: "answer.delta", text: "好的," },
|
||||
{ type: "answer.delta", text: "先确认一下:" },
|
||||
]);
|
||||
assert.equal(emitted.some((event) => event.type === "run.completed"), true);
|
||||
assert.deepEqual(
|
||||
emitted.find((event) => event.type === "run.completed"),
|
||||
{ type: "run.completed", turnId: TURN_ID },
|
||||
);
|
||||
assert.equal(emitted.some((event) => String(event.type).includes("reasoning")), false);
|
||||
assert.equal(emitted.some((event) => String(event.type).includes("raw")), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user