141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
||
import { NormalizerService } from "../src/services/normalizerService";
|
||
import type { OpenAIResponsesClient } from "../src/services/openaiResponsesClient";
|
||
import type { NormalizedQueryV2_0_2 } from "../src/types/normalizer";
|
||
|
||
function makeService(outputText: string) {
|
||
const normalizeMock = vi.fn().mockResolvedValue({
|
||
raw: { mode: "stubbed" },
|
||
outputText,
|
||
usage: {
|
||
input_tokens: 11,
|
||
output_tokens: 22,
|
||
total_tokens: 33
|
||
}
|
||
});
|
||
const openaiClient = {
|
||
normalize: normalizeMock
|
||
} as unknown as OpenAIResponsesClient;
|
||
return {
|
||
service: new NormalizerService(openaiClient),
|
||
normalizeMock
|
||
};
|
||
}
|
||
|
||
describe("NormalizerService coercion layer", () => {
|
||
it("coerces malformed v2.0.2 local payload into a usable routed fragment", async () => {
|
||
const malformed = {
|
||
schema_version: "normalized_query_v2_0_2",
|
||
global_notes: {
|
||
needs_clarification: true,
|
||
clarification_reason: "insufficient_specificity"
|
||
},
|
||
fragments: [
|
||
{
|
||
fragment_id: 1,
|
||
raw_fragment_text: "свк доки за 20год покеж",
|
||
normalized_fragment_text: "показать документы за 20 год",
|
||
domain_relevance: true,
|
||
business_scope: "document_review",
|
||
entity_hints: [],
|
||
account_hints: [],
|
||
document_hints: ["документы"],
|
||
register_hints: [],
|
||
time_scope: {
|
||
period_type: "year",
|
||
year: 2020,
|
||
month: null
|
||
},
|
||
flags: {},
|
||
candidate_labels: ["show_documents"],
|
||
confidence: 0.85,
|
||
execution_readiness: "needs_clarification",
|
||
clarification_reason: "insufficient_specificity",
|
||
soft_assumption_used: [],
|
||
route_status: "no_route",
|
||
no_route_reason: "insufficient_specificity"
|
||
}
|
||
]
|
||
};
|
||
|
||
const { service, normalizeMock } = makeService(JSON.stringify(malformed));
|
||
|
||
const response = await service.normalize({
|
||
llmProvider: "local",
|
||
model: "qwen2.5-14b-instruct-1m",
|
||
baseUrl: "http://127.0.0.1:1234/v1",
|
||
promptVersion: "normalizer_v2_0_2",
|
||
userQuestion: "свк доки за 20год покеж",
|
||
retryPolicy: "single-pass-strict"
|
||
});
|
||
|
||
expect(normalizeMock).toHaveBeenCalledTimes(1);
|
||
expect(response.ok).toBe(true);
|
||
expect(response.normalized?.schema_version).toBe("normalized_query_v2_0_2");
|
||
|
||
const normalized = response.normalized as NormalizedQueryV2_0_2;
|
||
expect(normalized.fragments).toHaveLength(1);
|
||
expect(normalized.fragments[0]).toMatchObject({
|
||
fragment_id: "F1",
|
||
domain_relevance: "in_scope",
|
||
business_scope: "company_specific_accounting",
|
||
confidence: "high",
|
||
route_status: "routed",
|
||
no_route_reason: null
|
||
});
|
||
expect(normalized.fragments[0].candidate_labels).toContain("simple_factual");
|
||
expect(normalized.fragments[0].time_scope).toMatchObject({
|
||
type: "explicit",
|
||
value: "2020"
|
||
});
|
||
});
|
||
|
||
it("coerces period_type month payload into explicit YYYY-MM time scope", async () => {
|
||
const malformed = {
|
||
schema_version: "normalized_query_v2_0_2",
|
||
fragments: [
|
||
{
|
||
fragment_id: "frag_main",
|
||
raw_fragment_text: "Какой остаток по счету 60 на 2020 май",
|
||
normalized_fragment_text: "остаток по счету 60 на май 2020",
|
||
domain_relevance: "in_scope",
|
||
business_scope: "company_specific_accounting",
|
||
entity_hints: ["счет"],
|
||
account_hints: ["60"],
|
||
document_hints: [],
|
||
register_hints: ["остаток"],
|
||
time_scope: {
|
||
period_type: "month",
|
||
year: 2020,
|
||
month: 5
|
||
},
|
||
flags: {
|
||
asks_for_period_summary: false
|
||
},
|
||
candidate_labels: ["simple_factual"],
|
||
confidence: 0.67
|
||
}
|
||
]
|
||
};
|
||
|
||
const { service } = makeService(JSON.stringify(malformed));
|
||
const response = await service.normalize({
|
||
llmProvider: "local",
|
||
model: "qwen2.5-14b-instruct-1m",
|
||
baseUrl: "http://127.0.0.1:1234/v1",
|
||
promptVersion: "normalizer_v2_0_2",
|
||
userQuestion: "Какой остаток по счету 60 на 2020 май",
|
||
retryPolicy: "single-pass-strict"
|
||
});
|
||
|
||
expect(response.ok).toBe(true);
|
||
const normalized = response.normalized as NormalizedQueryV2_0_2;
|
||
expect(normalized.fragments).toHaveLength(1);
|
||
expect(normalized.fragments[0].time_scope).toMatchObject({
|
||
type: "explicit",
|
||
value: "2020-05"
|
||
});
|
||
expect(normalized.fragments[0].route_status).toBe("routed");
|
||
});
|
||
});
|