eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// Tests for Eleven's guided tour (client-side flow). node test/tutorial.test.mjs
2import assert from "node:assert/strict";
3import { startTour } from "../web/static/tutorial.js";
4import { runCommand, commandCatalog, setDefaultBot } from "../web/static/apps.js";
5
6let failures = 0;
7async function test(name, fn) {
8 try {
9 await fn();
10 console.log(`PASS ${name}`);
11 } catch (e) {
12 failures++;
13 console.log(`FAIL ${name} β€” ${e && e.message}`);
14 }
15}
16
17const tick = () => new Promise((r) => setTimeout(r, 0));
18const lastChip = (spec) => spec.actions[spec.actions.length - 1]; // the advancing chip
19const textOf = (steps) => steps.map((s) => s.text).join("\n");
20
21// Walk the tour, tapping one chip per step (chooser picks which). Stops at the
22// first step with no chips (the ending). Awaits a tick after each tap since an
23// advance can be async (e.g. "Show me around" navigates to the Eleven DM first).
24async function walk(chooser = lastChip) {
25 const steps = [];
26 startTour((spec) => steps.push(spec));
27 await tick(); // spaceInfo().finally β†’ first step
28 for (let guard = 0; guard < 20; guard++) {
29 const cur = steps[steps.length - 1];
30 if (!cur.actions || !cur.actions.length) break; // the ending
31 const before = steps.length;
32 chooser(cur).run();
33 await tick();
34 if (steps.length === before) break; // tapped a non-advancing chip; stop
35 }
36 return steps;
37}
38
39await test("every step is attributed to Eleven and the tour ends with a πŸŽ‰", async () => {
40 setDefaultBot(() => null); // no bot β†’ speaks as "Eleven", AI section off
41 const steps = await walk();
42 assert.ok(steps.length >= 5, `expected a multi-step tour, got ${steps.length}`);
43 for (const s of steps) assert.equal(s.sender.name, "Eleven");
44 assert.match(steps[0].text, /I'm Eleven/); // intro introduces itself
45 assert.match(steps[steps.length - 1].text, /πŸŽ‰/); // celebratory ending
46});
47
48await test("skipIntro jumps past the 'fancy a tour?' greeting straight into the tour", async () => {
49 setDefaultBot(() => null); // no Eleven to DM β†’ the tour renders in place
50 const steps = [];
51 startTour((spec) => steps.push(spec), null, { skipIntro: true });
52 await tick(); // spaceInfo().finally β†’ toElevenDm(elevenTour resolves null)
53 await tick(); // β†’ the first real step
54 assert.ok(steps.length >= 1, "a step rendered");
55 assert.doesNotMatch(steps[0].text, /Fancy a quick tour/); // the greeting is skipped
56 assert.doesNotMatch(steps[0].text, /your guide to this app/); // ...and its intro line
57 assert.match(steps[0].text, /You're in /); // it opens on the space step instead
58});
59
60await test("the default path skips the security deep-dive; picking it shows it", async () => {
61 setDefaultBot(() => null);
62 const plain = textOf(await walk()); // taps "No thanks" at the privacy step
63 assert.doesNotMatch(plain, /never leaves your device/);
64
65 // Now tap the FIRST chip at the privacy step ("Tell me about security").
66 const security = textOf(
67 await walk((spec) => (/technical version/.test(spec.text) ? spec.actions[0] : lastChip(spec))),
68 );
69 assert.match(security, /never leaves your device/);
70});
71
72await test("the AI section appears only where a conversational helper is present", async () => {
73 setDefaultBot(() => null);
74 assert.doesNotMatch(textOf(await walk()), /AI helpers switched on/);
75
76 setDefaultBot(() => ({ name: "Eleven", kind: "ai" }));
77 const withAi = textOf(await walk());
78 assert.match(withAi, /AI helpers switched on/);
79 assert.match(withAi, /asking me anything/i); // invites a free-form question
80 setDefaultBot(() => null);
81});
82
83await test("command examples are wrapped in backticks so they render as code", async () => {
84 setDefaultBot(() => null);
85 const all = textOf(await walk());
86 assert.match(all, /`\/todo buy milk`/);
87 assert.match(all, /`\/help`/);
88});
89
90await test("tapping an advancing chip twice can't double-advance", async () => {
91 setDefaultBot(() => null);
92 const steps = [];
93 startTour((spec) => steps.push(spec));
94 await tick();
95 const chip = lastChip(steps[0]); // "Show me around" β€” fires once even if tapped twice
96 chip.run();
97 chip.run(); // stale tap
98 await tick();
99 assert.equal(steps.length, 2, "a second tap on the same chip appended another step");
100});
101
102await test("the Todo/Reminder steps invite trying the command and reassure you can come back", async () => {
103 setDefaultBot(() => null);
104 const all = textOf(await walk());
105 assert.match(all, /\/todo /); // shows how to try it yourself
106 assert.match(all, /come back to this conversation/i); // reassurance that you won't lose your place
107});
108
109await test("/tutorial is a client-answered command, listed everywhere in the typeahead", async () => {
110 const entry = commandCatalog().find((c) => c.name === "/tutorial");
111 assert.ok(entry, "/tutorial missing from the command catalog");
112 assert.equal(entry.bot, false); // client-answered β†’ shown in every thread
113
114 setDefaultBot(() => null);
115 const steps = [];
116 const handled = runCommand("/tutorial", "thread-1", (spec) => steps.push(spec));
117 assert.equal(handled, true); // caught, not broadcast
118 await tick();
119 assert.match(steps[0].text, /I'm Eleven/);
120});
121
122console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`);
123process.exit(failures === 0 ? 0 : 1);
124