140 lines
5.1 KiB
TypeScript
140 lines
5.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { createWhatsAppTrial } from "../src/channels/whatsapp/trial.js";
|
|
import type { Db } from "../src/db/db.js";
|
|
import type { ConversationMemory } from "../src/ai/memory/memory.js";
|
|
import type { NocoProvisioner } from "../src/integrations/nocodb/provision.js";
|
|
|
|
function makeProvisioner(
|
|
accounts: { id: number; username: string }[] = [],
|
|
) {
|
|
const probeExisting = vi.fn(async () => accounts);
|
|
const provisionTrial = vi.fn(async () => ({
|
|
ok: true,
|
|
username: "abc123",
|
|
password: "pw",
|
|
message: "ok",
|
|
}));
|
|
return {
|
|
probeExisting,
|
|
provisionTrial,
|
|
} as unknown as NocoProvisioner & {
|
|
probeExisting: ReturnType<typeof vi.fn>;
|
|
provisionTrial: ReturnType<typeof vi.fn>;
|
|
};
|
|
}
|
|
|
|
function makeTrial(provisioner?: NocoProvisioner) {
|
|
const db = {
|
|
getOrCreateUserId: vi.fn(async () => 1),
|
|
} as unknown as Db;
|
|
const memory = {
|
|
recall: vi.fn(async () => undefined),
|
|
remember: vi.fn(async () => {}),
|
|
recallAll: vi.fn(async () => ({})),
|
|
} as unknown as ConversationMemory;
|
|
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() } as never;
|
|
const trial = createWhatsAppTrial({ db, memory, provisioner, logger });
|
|
return { trial, db, memory };
|
|
}
|
|
|
|
describe("createWhatsAppTrial", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("starts the flow from a paid SKU with trial intent, asking for username (0 accounts)", async () => {
|
|
const prov = makeProvisioner();
|
|
const { trial } = makeTrial(prov);
|
|
const reply = await trial.handleMessage("I want try this CZH03", "60103181872");
|
|
expect(reply).toContain("FREECZH03");
|
|
expect(reply).toContain("username");
|
|
expect(prov.probeExisting).toHaveBeenCalledWith({ telegramUserId: "60103181872" });
|
|
});
|
|
|
|
it("starts the flow from a bare FREE-prefixed SKU", async () => {
|
|
const { trial } = makeTrial(makeProvisioner());
|
|
const reply = await trial.handleMessage("FreeCZH03", "60103181872");
|
|
expect(reply).toContain("FREECZH03");
|
|
});
|
|
|
|
it("starts the flow from a product-page URL", async () => {
|
|
const { trial } = makeTrial(makeProvisioner());
|
|
const reply = await trial.handleMessage(
|
|
"I want try https://www.digikedai.com/products/czh03",
|
|
"60103181872",
|
|
);
|
|
expect(reply).toContain("FREECZH03");
|
|
});
|
|
|
|
it("asks which SKU/URL when trial intent has no SKU", async () => {
|
|
const { trial } = makeTrial(makeProvisioner());
|
|
const reply = await trial.handleMessage("I want to try the baking course", "60103181872");
|
|
expect(reply).toContain("SKU");
|
|
expect(reply).toContain("digikedai.com/products");
|
|
});
|
|
|
|
it("confirms reuse for a single existing account, then provisions to it", async () => {
|
|
const prov = makeProvisioner([{ id: 7, username: "lover" }]);
|
|
const { trial } = makeTrial(prov);
|
|
const r1 = await trial.handleMessage("I want try CZH03", "60103181872");
|
|
expect(r1).toContain("lover");
|
|
|
|
const r2 = await trial.handleMessage("ok", "60103181872");
|
|
expect(prov.provisionTrial).toHaveBeenCalledWith(
|
|
expect.objectContaining({ customerId: 7, sku: "FREECZH03" }),
|
|
);
|
|
expect(r2).toContain("abc123");
|
|
});
|
|
|
|
it("lists multiple accounts and defaults to the first on consent", async () => {
|
|
const prov = makeProvisioner([
|
|
{ id: 7, username: "lover" },
|
|
{ id: 9, username: "other" },
|
|
]);
|
|
const { trial } = makeTrial(prov);
|
|
const r1 = await trial.handleMessage("FreeCZH03", "60103181872");
|
|
expect(r1).toContain("1. lover");
|
|
expect(r1).toContain("2. other");
|
|
|
|
const r2 = await trial.handleMessage("ok", "60103181872");
|
|
expect(prov.provisionTrial).toHaveBeenCalledWith(
|
|
expect.objectContaining({ customerId: 7 }),
|
|
);
|
|
expect(r2).toContain("abc123");
|
|
});
|
|
|
|
it("picks the numbered account when given an index", async () => {
|
|
const prov = makeProvisioner([
|
|
{ id: 7, username: "lover" },
|
|
{ id: 9, username: "other" },
|
|
]);
|
|
const { trial } = makeTrial(prov);
|
|
await trial.handleMessage("FreeCZH03", "60103181872");
|
|
await trial.handleMessage("2", "60103181872");
|
|
expect(prov.provisionTrial).toHaveBeenCalledWith(
|
|
expect.objectContaining({ customerId: 9 }),
|
|
);
|
|
});
|
|
|
|
it("provisions a new account with the supplied username", async () => {
|
|
const prov = makeProvisioner();
|
|
const { trial } = makeTrial(prov);
|
|
await trial.handleMessage("I want try CZH03", "60103181872");
|
|
const r2 = await trial.handleMessage("abc123", "60103181872");
|
|
expect(prov.provisionTrial).toHaveBeenCalledWith(
|
|
expect.objectContaining({ username: "abc123", sku: "FREECZH03" }),
|
|
);
|
|
expect(r2).toContain("abc123");
|
|
});
|
|
|
|
it("lets a non-trial SKU question fall through to the LLM", async () => {
|
|
const { trial } = makeTrial(makeProvisioner());
|
|
const reply = await trial.handleMessage("CZH03 多少钱", "60103181872");
|
|
expect(reply).toBeUndefined();
|
|
});
|
|
|
|
it("degrades to the username form when no provisioner is configured", async () => {
|
|
const { trial } = makeTrial(undefined);
|
|
const reply = await trial.handleMessage("I want try CZH03", "60103181872");
|
|
expect(reply).toContain("username");
|
|
});
|
|
});
|