216 lines
8.0 KiB
TypeScript
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);
|
|
});
|
|
});
|