Починить восстановление кириллицы в автопрогонах
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { repairAddressMojibakeText } from "../src/services/addressTextRepair";
|
||||
|
||||
describe("address text mojibake repair", () => {
|
||||
it("preserves C1 bytes when repairing old autorun Russian text", () => {
|
||||
const damagedTitle = String.fromCharCode(
|
||||
0x420, 0x2018, 0x420, 0x45b, 0x420, 0x203a, 0x420, 0xac, 0x420, 0x401, 0x420, 0x45b, 0x420,
|
||||
0x2122, 0x20, 0x420, 0x45b, 0x420, 0x2018, 0x420, 0xa9, 0x420, 0x98, 0x420, 0x2122, 0x20,
|
||||
0x420, 0xa0, 0x421, 0x453, 0x421, 0x2021, 0x420, 0x405, 0x420, 0xb0, 0x421, 0x40f, 0x20,
|
||||
0x421, 0x403, 0x420, 0xb5, 0x421, 0x403, 0x421, 0x403, 0x420, 0x451, 0x421, 0x40f, 0x20,
|
||||
0x31, 0x36, 0x2e, 0x30, 0x34, 0x2e, 0x32, 0x30, 0x32, 0x36, 0x2c, 0x20, 0x32, 0x31,
|
||||
0x3a, 0x32, 0x36, 0x3a, 0x30, 0x36
|
||||
);
|
||||
const damagedAlternative = String.fromCharCode(
|
||||
0x420, 0x452, 0x420, 0x203a, 0x420, 0xac, 0x420, 0x45e, 0x420, 0x2022, 0x420, 0xa0, 0x420,
|
||||
0x45c, 0x420, 0x452, 0x420, 0x45e, 0x420, 0x98, 0x420, 0x2019, 0x420, 0x452
|
||||
);
|
||||
|
||||
expect(repairAddressMojibakeText(damagedTitle)).toBe(
|
||||
"\u0411\u041e\u041b\u042c\u0428\u041e\u0419 \u041e\u0411\u0429\u0418\u0419 \u0420\u0443\u0447\u043d\u0430\u044f \u0441\u0435\u0441\u0441\u0438\u044f 16.04.2026, 21:26:06"
|
||||
);
|
||||
expect(repairAddressMojibakeText(damagedAlternative)).toBe(
|
||||
"\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410"
|
||||
);
|
||||
});
|
||||
|
||||
it("repairs already lossy known replacement fragments", () => {
|
||||
expect(repairAddressMojibakeText("\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\uFFFD?\u0412\u0410")).toBe(
|
||||
"\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410"
|
||||
);
|
||||
expect(repairAddressMojibakeText("\u0411\u041e\u041b\u042c\u0428\u041e\u0419 \u041e\u0411\u0429\uFFFD\u0419")).toBe(
|
||||
"\u0411\u041e\u041b\u042c\u0428\u041e\u0419 \u041e\u0411\u0429\u0418\u0419"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2276,6 +2276,101 @@ describe("assistant address follow-up carryover", () => {
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues the original inventory query after replacement-damaged organization clarification", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage = "кайф - что там на складе по остаткам?";
|
||||
const secondMessage = "АЛЬТЕРНАТ\uFFFD?ВА";
|
||||
const repairedSecondMessage = "\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410";
|
||||
const addressQueryService = {
|
||||
tryHandle: vi.fn(async (message: string, options?: any) => {
|
||||
calls.push({ message, options });
|
||||
if (message === firstMessage) {
|
||||
return buildAddressLimitedLaneResult("missing_anchor", {
|
||||
reply_text: [
|
||||
"Нужно уточнить организацию, чтобы не смешивать компании в одном ответе.",
|
||||
"Сейчас в доступном контуре вижу такие организации:",
|
||||
"- ООО Альтернатива Плюс",
|
||||
"- ООО Лайсвуд",
|
||||
"- РАЙМ"
|
||||
].join("\n"),
|
||||
debug: {
|
||||
...buildAddressLimitedLaneResult("missing_anchor").debug,
|
||||
detected_intent: "inventory_on_hand_as_of_date",
|
||||
extracted_filters: {
|
||||
as_of_date: "2026-04-15"
|
||||
},
|
||||
selected_recipe: null,
|
||||
organization_candidates: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
|
||||
reasons: ["organization_clarification_required", "multiple_known_organizations_detected"]
|
||||
}
|
||||
});
|
||||
}
|
||||
if (
|
||||
(message === secondMessage || message === repairedSecondMessage) &&
|
||||
options?.followupContext &&
|
||||
options?.activeOrganization === "ООО Альтернатива Плюс"
|
||||
) {
|
||||
return buildAddressLaneResult({
|
||||
reply_text: "На 15.04.2026 по ООО Альтернатива Плюс подтвержден складской остаток по всем складам.",
|
||||
debug: {
|
||||
...buildAddressLaneResult().debug,
|
||||
detected_intent: "inventory_on_hand_as_of_date",
|
||||
extracted_filters: {
|
||||
as_of_date: "2026-04-15",
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
},
|
||||
reasons: ["address_followup_context_applied", "organization_grounded_from_scope_candidates"]
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
} as any;
|
||||
|
||||
const normalizerService = {
|
||||
normalize: vi.fn(async () => ({
|
||||
assistant_reply: "normalizer_fallback_should_not_be_used",
|
||||
reply_type: "partial_coverage",
|
||||
debug: {}
|
||||
}))
|
||||
} as any;
|
||||
|
||||
const sessions = new AssistantSessionStore();
|
||||
const service = new AssistantService(
|
||||
normalizerService,
|
||||
sessions as any,
|
||||
{} as any,
|
||||
{ persistSession: vi.fn() } as any,
|
||||
addressQueryService
|
||||
);
|
||||
|
||||
const sessionId = `asst-address-org-clarification-damaged-${Date.now()}`;
|
||||
const first = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: firstMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
expect(first.ok).toBe(true);
|
||||
expect(first.reply_type).toBe("partial_coverage");
|
||||
|
||||
const second = await service.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: secondMessage,
|
||||
useMock: true
|
||||
} as any);
|
||||
|
||||
expect(second.ok).toBe(true);
|
||||
expect(second.reply_type).toBe("factual");
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[1].message).toBe(repairedSecondMessage);
|
||||
expect(calls[1].options?.activeOrganization).toBe("ООО Альтернатива Плюс");
|
||||
expect(calls[1].options?.knownOrganizations).toEqual(["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]);
|
||||
expect(calls[1].options?.followupContext?.previous_intent).toBe("inventory_on_hand_as_of_date");
|
||||
expect(calls[1].options?.followupContext?.previous_filters?.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(calls[1].options?.followupContext?.root_filters?.organization).toBe("ООО Альтернатива Плюс");
|
||||
expect(normalizerService.normalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps historical inventory date follow-up alive after company clarification and a capability answer", async () => {
|
||||
const calls: Array<{ message: string; options?: any }> = [];
|
||||
const firstMessage = "покажи остатки по складу";
|
||||
|
||||
@@ -42,6 +42,26 @@ describe("assistant organization matcher", () => {
|
||||
expect(score).toBeGreaterThanOrEqual(90);
|
||||
});
|
||||
|
||||
it("matches replacement-damaged organization clarification when the live candidate is unique", () => {
|
||||
const resolved = resolveOrganizationSelectionFromMessage("АЛЬТЕРНАТ\uFFFD?ВА", [
|
||||
"ООО Альтернатива Плюс",
|
||||
"ООО Лайсвуд",
|
||||
"РАЙМ"
|
||||
]);
|
||||
|
||||
expect(resolved).toBe("ООО Альтернатива Плюс");
|
||||
});
|
||||
|
||||
it("keeps replacement-damaged organization clarification ambiguous when candidates share the token", () => {
|
||||
const resolved = resolveOrganizationSelectionFromMessage("АЛЬТЕРНАТ\uFFFD?ВА", [
|
||||
"ООО Альтернатива Плюс",
|
||||
"ООО Альтернатива Минус",
|
||||
"ООО Лайсвуд"
|
||||
]);
|
||||
|
||||
expect(resolved).toBeNull();
|
||||
});
|
||||
|
||||
it("treats minor live label corruption as the same organization entity", () => {
|
||||
expect(organizationsLikelySameEntity("Альтернатива Плюс", 'ООО "Альтернати"а Плюс"')).toBe(true);
|
||||
});
|
||||
|
||||
@@ -31,4 +31,26 @@ describe("autoruns question extraction", () => {
|
||||
expect(parsed[0]).toMatch(/поставщик/i);
|
||||
expect(parsed[0]).toMatch(/коротко/i);
|
||||
});
|
||||
|
||||
it("repairs old autorun C1-control mojibake before exposing cards and questions", () => {
|
||||
const damagedTitle = String.fromCharCode(
|
||||
0x420, 0x2018, 0x420, 0x45b, 0x420, 0x203a, 0x420, 0xac, 0x420, 0x401, 0x420, 0x45b, 0x420,
|
||||
0x2122, 0x20, 0x420, 0x45b, 0x420, 0x2018, 0x420, 0xa9, 0x420, 0x98, 0x420, 0x2122, 0x20,
|
||||
0x420, 0xa0, 0x421, 0x453, 0x421, 0x2021, 0x420, 0x405, 0x420, 0xb0, 0x421, 0x40f, 0x20,
|
||||
0x421, 0x403, 0x420, 0xb5, 0x421, 0x403, 0x421, 0x403, 0x420, 0x451, 0x421, 0x40f, 0x20,
|
||||
0x31, 0x36, 0x2e, 0x30, 0x34, 0x2e, 0x32, 0x30, 0x32, 0x36, 0x2c, 0x20, 0x32, 0x31,
|
||||
0x3a, 0x32, 0x36, 0x3a, 0x30, 0x36
|
||||
);
|
||||
const damagedAlternative = String.fromCharCode(
|
||||
0x420, 0x452, 0x420, 0x203a, 0x420, 0xac, 0x420, 0x45e, 0x420, 0x2022, 0x420, 0xa0, 0x420,
|
||||
0x45c, 0x420, 0x452, 0x420, 0x45e, 0x420, 0x98, 0x420, 0x2019, 0x420, 0x452
|
||||
);
|
||||
|
||||
expect(__autoRunsQuestionTestUtils.repairAutogenMojibake(damagedTitle)).toBe(
|
||||
"\u0411\u041e\u041b\u042c\u0428\u041e\u0419 \u041e\u0411\u0429\u0418\u0419 \u0420\u0443\u0447\u043d\u0430\u044f \u0441\u0435\u0441\u0441\u0438\u044f 16.04.2026, 21:26:06"
|
||||
);
|
||||
expect(__autoRunsQuestionTestUtils.repairAutogenMojibake(damagedAlternative)).toBe(
|
||||
"\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,4 +33,37 @@ describe("eval runtime question splitting", () => {
|
||||
expect(parsed[0]).toMatch(/поставщик/i);
|
||||
expect(parsed[0]).toMatch(/коротко/i);
|
||||
});
|
||||
|
||||
it("repairs mojibake questions before runtime job materialization", () => {
|
||||
const parsed = __evalRouteTestUtils.normalizeRuntimeQuestions([
|
||||
"кайф - что там на складе по остаткам?"
|
||||
]);
|
||||
|
||||
expect(parsed).toEqual(["кайф - что там на складе по остаткам?"]);
|
||||
});
|
||||
|
||||
it("repairs damaged clarification as one scenario turn when splitting is disabled", () => {
|
||||
const parsed = __evalRouteTestUtils.normalizeRuntimeQuestions(
|
||||
["\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\uFFFD?\u0412\u0410"],
|
||||
{
|
||||
dedupe: false,
|
||||
splitCandidates: false
|
||||
}
|
||||
);
|
||||
|
||||
expect(parsed).toEqual(["\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410"]);
|
||||
});
|
||||
|
||||
it("repairs C1-control autorun clarification before runtime job materialization", () => {
|
||||
const damagedAlternative = String.fromCharCode(
|
||||
0x420, 0x452, 0x420, 0x203a, 0x420, 0xac, 0x420, 0x45e, 0x420, 0x2022, 0x420, 0xa0, 0x420,
|
||||
0x45c, 0x420, 0x452, 0x420, 0x45e, 0x420, 0x98, 0x420, 0x2019, 0x420, 0x452
|
||||
);
|
||||
const parsed = __evalRouteTestUtils.normalizeRuntimeQuestions([damagedAlternative], {
|
||||
dedupe: false,
|
||||
splitCandidates: false
|
||||
});
|
||||
|
||||
expect(parsed).toEqual(["\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user