Files

216 lines
8.0 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
buildSystemPrompt,
SYSTEM_CONTEXT,
BUSINESS_CONTEXT,
friendlyLanguage,
adminPurchaseLink,
isNoReply,
} from "../src/ai/prompts/system.js";
describe("buildSystemPrompt", () => {
it("includes the stable system context", () => {
const p = buildSystemPrompt();
expect(p).toContain("Digi Kedai customer support");
});
it("restricts scope to Digi Kedai products and declines off-topic requests", () => {
const p = buildSystemPrompt();
expect(p).toContain("only help with Digi Kedai's digital products");
expect(p).toContain("write a poem");
expect(p).toContain("off-topic requests");
});
it("includes business context", () => {
const p = buildSystemPrompt();
expect(p).toContain("digikedai.com");
});
it("appends a friendly language hint when provided", () => {
const p = buildSystemPrompt("zh");
expect(p).toContain("Chinese");
expect(p).not.toContain("language_code");
});
it("appends product facts when retrieval hits are provided", () => {
const p = buildSystemPrompt(undefined, "1. 得到 全平台(SKU CDD01)→ https://www.digikedai.com/products/cdd01/");
expect(p).toContain("Product facts");
expect(p).toContain("https://www.digikedai.com/products/cdd01/");
});
it("does not append product facts when none are retrieved", () => {
const p = buildSystemPrompt();
expect(p).not.toContain("Product facts");
});
it("instructs explicit human-handoff on customer request", () => {
const p = buildSystemPrompt();
expect(p).toContain("explicitly asks for a human");
expect(p).toContain("text-only");
});
it("only sanctions Telegram @MrFullStackDev as admin contact", () => {
const p = buildSystemPrompt();
expect(p).toContain("@MrFullStackDev");
expect(p).not.toContain("012-");
expect(p).not.toContain("@DigiKedai");
expect(p).not.toContain("t.me/DigiKedai"); // channel link never appears
expect(p).toContain("t.me/MrFullStackDev"); // purchase deep link sanctioned
});
it("requires URLs to stand alone on their own line", () => {
const p = buildSystemPrompt();
expect(p).toContain("own line");
});
it("flags repeated questions with the repeat rule", () => {
const p = buildSystemPrompt(undefined, undefined, true);
expect(p).toContain("repeat-question rule");
const plain = buildSystemPrompt();
expect(plain).not.toContain("repeat-question rule");
});
it("maps Telegram language codes to friendly names", () => {
expect(friendlyLanguage("zh-hans")).toBe("Chinese (中文)");
expect(friendlyLanguage("en-US")).toBe("English");
expect(friendlyLanguage("ms")).toBe("Bahasa Melayu");
expect(friendlyLanguage("zz")).toBe("zz");
expect(friendlyLanguage(undefined)).toBeUndefined();
});
it("points trial-interested customers to the free page", () => {
const p = buildSystemPrompt();
expect(p).toContain("https://www.digikedai.com/free/");
});
it("does not over-promise a free twin for every paid product", () => {
const p = buildSystemPrompt();
// The old rule claimed every paid product has a FREE twin; the real
// policy (改动3, 2026-08-31) is facts-driven: only a twin present in
// the retrieved facts may be promised.
expect(p).not.toContain("Every paid product has a free-trial twin");
expect(p).toContain("ONLY when that twin appears in the retrieved facts");
expect(p).toContain("say plainly that this product has no free-trial version");
});
it("does not leak internal infrastructure names to users", () => {
const p = buildSystemPrompt();
expect(p).not.toContain("AList");
expect(p).not.toContain("NocoDB");
expect(p).not.toContain("n8n");
});
});
describe("adminPurchaseLink", () => {
it("pre-fills the admin chat with an encoded purchase message when username is known", () => {
const url = adminPurchaseLink({ sku: "CXM04", username: "abc123" });
expect(url.startsWith("https://t.me/MrFullStackDev?text=")).toBe(true);
const q = url.split("?text=")[1];
expect(q).not.toMatch(/[\u4e00-\u9fff\s:]/); // fully percent-encoded
expect(decodeURIComponent(q)).toBe("我要买 CXM04 账号 username:abc123");
});
it("omits the username part when it is unknown", () => {
const url = adminPurchaseLink({ sku: "CDD01" });
const q = url.split("?text=")[1];
expect(q).not.toMatch(/[\u4e00-\u9fff\s:]/);
expect(decodeURIComponent(q)).toBe("我要买 CDD01");
});
it("appends free-text notes after the SKU segment when provided", () => {
const url = adminPurchaseLink({ sku: "CZH01", notes: "(想用银行转账)" });
const q = url.split("?text=")[1];
expect(q).not.toMatch(/[\u4e00-\u9fff\s:]/);
expect(decodeURIComponent(q)).toBe("我要买 CZH01 (想用银行转账)");
});
it("combines notes and username in order", () => {
const url = adminPurchaseLink({
sku: "CZH01",
notes: "(想用银行转账)",
username: "abc123",
});
expect(decodeURIComponent(url.split("?text=")[1])).toBe(
"我要买 CZH01 (想用银行转账) 账号 username:abc123",
);
});
it("never nests a URL inside the text parameter (双重嵌套 bug 回归)", () => {
const url = adminPurchaseLink({ sku: "CZH01", notes: "(想用银行转账)" });
// exactly one link prefix in the whole URL — no ?text=t.me/… re-wrap
expect(url.match(/t\.me\//g)).toEqual(["t.me/"]);
const q = url.split("?text=")[1];
expect(q).not.toContain("t.me/");
expect(q).not.toContain("MrFullStackDev");
});
it("embeds the pre-encoded static sample URLs in the system prompt", () => {
const p = buildSystemPrompt();
// The prompt carries the FINAL encoded URLs verbatim (builder output,
// no ${…} template recursion — 第1批 2026-08-31)
expect(p).toContain(
"https://t.me/MrFullStackDev?text=%E6%88%91%E8%A6%81%E4%B9%B0%20CXM04%20%E8%B4%A6%E5%8F%B7%20username%3Aabc123",
);
expect(p).toContain(
"https://t.me/MrFullStackDev?text=%E6%88%91%E8%A6%81%E4%B9%B0%20CXM04",
);
expect(p).toContain("purchase deep link");
expect(p).toContain("FREE-prefixed");
// and those samples are consistent with the builder's own output
expect(p).toContain(
adminPurchaseLink({ sku: "CXM04", username: "abc123" }),
);
expect(p).toContain(adminPurchaseLink({ sku: "CXM04" }));
// the prompt itself must never contain a raw recursive placeholder
expect(p).not.toContain("${adminPurchaseLink(");
});
});
describe("prompt constants", () => {
it("never mention internal infra", () => {
expect(SYSTEM_CONTEXT).not.toContain("NocoDB");
expect(BUSINESS_CONTEXT).not.toContain("PostgreSQL");
});
});
describe("WhatsApp channel prompt", () => {
const wa = () => buildSystemPrompt(undefined, undefined, false, "whatsapp");
it("never references Telegram or the admin handle", () => {
const p = wa();
expect(p).not.toContain("@MrFullStackDev");
expect(p).not.toContain("t.me");
expect(p).not.toContain("purchase deep link");
expect(p).not.toContain("012-");
});
it("tells the customer the admin replies in the same chat", () => {
const p = wa();
expect(p).toContain("NO_REPLY");
expect(p).toContain("admin will reply");
});
it("keeps shared scope and business facts", () => {
const p = wa();
expect(p).toContain("Digi Kedai customer support");
expect(p).toContain("only help with Digi Kedai's digital products");
expect(p).toContain("digikedai.com");
expect(p).not.toContain("AList");
expect(p).not.toContain("NocoDB");
});
});
describe("isNoReply", () => {
it("matches the sentinel regardless of case and decoration", () => {
expect(isNoReply("NO_REPLY")).toBe(true);
expect(isNoReply("no_reply")).toBe(true);
expect(isNoReply("[[NO_REPLY]]")).toBe(true);
expect(isNoReply("NO-REPLY")).toBe(true);
});
it("does not match a real reply", () => {
expect(isNoReply("Here is the price")).toBe(false);
expect(isNoReply("")).toBe(false);
});
});