Files
tg-chat-bot/test/pipeline.test.mjs
2026-05-18 13:31:37 +03:00

179 lines
6.1 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
const {UserRequestPipeline} = await import("../dist/ai/user-request-pipeline/pipeline.js");
const {splitAttachmentsBySize} = await import("../dist/ai/user-request-pipeline/size-gate.js");
const {PIPELINE_ATTACHMENT_LIMIT_BYTES} = await import("../dist/ai/user-request-pipeline/types.js");
function baseState() {
return {
requestId: "test-request",
chatId: 1,
messageId: 2,
fromId: 3,
receivedAt: new Date(0).toISOString(),
text: "hello",
settings: {
provider: "OLLAMA",
responseLanguage: "default",
voiceMode: "execute",
imageOutputMode: "photo",
},
inputAttachments: [],
outputAttachments: [],
artifacts: [],
toolRankDecisions: [],
audit: [],
};
}
test("pipeline runs only requested stage slice", async () => {
const state = baseState();
const pipeline = new UserRequestPipeline({
stageNames: ["input_size_gate", "download_attachments"],
stages: [{
name: "input_size_gate",
async run() {
return {
stage: "input_size_gate",
status: "succeeded",
details: {checked: true},
};
},
}],
});
await pipeline.run(state, new AbortController().signal);
assert.equal(state.audit.length, 3);
assert.equal(state.audit[0].stage, "input_size_gate");
assert.equal(state.audit[0].status, "running");
assert.equal(state.audit[1].stage, "input_size_gate");
assert.equal(state.audit[1].status, "succeeded");
assert.deepEqual(state.audit[1].details, {checked: true});
assert.equal(state.audit[2].stage, "download_attachments");
assert.equal(state.audit[2].status, "skipped");
assert.deepEqual(state.audit[2].details, {
reason: "stage_not_registered",
fallbackAction: "continue_without_stage",
});
});
test("pipeline stops when fallback decision is fail_request", async () => {
const state = baseState();
const pipeline = new UserRequestPipeline({
stageNames: ["send_response"],
stages: [{
name: "send_response",
async run() {
throw new Error("send failed");
},
}],
fallbackPolicies: [{
stage: "send_response",
onUnavailable: "fail_request",
onFailed: "fail_request",
}],
});
await assert.rejects(() => pipeline.run(state, new AbortController().signal), /send failed/);
assert.equal(state.audit.at(-1).stage, "send_response");
assert.equal(state.audit.at(-1).status, "failed");
assert.equal(state.audit.at(-1).details.fallbackAction, "fail_request");
});
test("pipeline continues when fallback decision allows continuation", async () => {
const state = baseState();
const pipeline = new UserRequestPipeline({
stageNames: ["document_rag", "send_response"],
stages: [
{
name: "document_rag",
async run() {
throw new Error("rag failed");
},
},
{
name: "send_response",
async run() {
return {
stage: "send_response",
status: "succeeded",
};
},
},
],
});
await pipeline.run(state, new AbortController().signal);
assert.equal(state.audit.some(event => event.stage === "document_rag" && event.status === "failed"), true);
assert.equal(state.audit.at(-1).stage, "send_response");
assert.equal(state.audit.at(-1).status, "succeeded");
});
test("pipeline persists stage artifacts and direction-aware attachments", async () => {
const state = baseState();
const pipeline = new UserRequestPipeline({
stageNames: ["persist_output_artifacts"],
stages: [{
name: "persist_output_artifacts",
async run() {
return {
stage: "persist_output_artifacts",
status: "succeeded",
artifacts: [{
kind: "final_text",
stage: "persist_output_artifacts",
createdAt: new Date(0).toISOString(),
text: "answer",
}],
attachments: [
{
direction: "input",
kind: "document",
fileName: "input.txt",
sizeBytes: 10,
},
{
direction: "output",
kind: "document",
fileName: "output.txt",
sizeBytes: 20,
},
],
};
},
}],
});
await pipeline.run(state, new AbortController().signal);
assert.equal(state.artifacts.length, 1);
assert.equal(state.artifacts[0].kind, "final_text");
assert.equal(state.inputAttachments.length, 1);
assert.equal(state.inputAttachments[0].fileName, "input.txt");
assert.equal(state.outputAttachments.length, 1);
assert.equal(state.outputAttachments[0].fileName, "output.txt");
});
test("size gate splits accepted and rejected attachments", () => {
const result = splitAttachmentsBySize([
{
direction: "input",
kind: "document",
fileName: "small.txt",
sizeBytes: PIPELINE_ATTACHMENT_LIMIT_BYTES,
},
{
direction: "input",
kind: "document",
fileName: "large.txt",
sizeBytes: PIPELINE_ATTACHMENT_LIMIT_BYTES + 1,
},
]);
assert.deepEqual(result.accepted.map(attachment => attachment.fileName), ["small.txt"]);
assert.equal(result.rejected.length, 1);
assert.equal(result.rejected[0].attachment.fileName, "large.txt");
});