tests/presenceClient.test.ts
Rama main · réplica de lectura del repositorio original.
// Discord presence, the client half (docs/presence.md): what one tab REPORTS
// from the store it reads, and what the Settings → Discord status row SAYS
// for each state the server can be in.
//
// The reporter itself (client/presence/report.ts) is a lazy chunk with a
// store, a window and timers; what it SAYS is `deriveReport`, which is pure,
// and that is what this holds — every surface of the plan's table, the idle
// rule for a hidden tab, and the one thing a report must never carry (a
// title made on the client, which the server's policy would not have seen).
import "../client/i18n/both.ts";
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { deriveReport, mintClientId, sameSubject, type PresenceInputs } from "../client/presence/derive.ts";
import { getOrbitsLeft, onOrbitsLeft, setOrbitsLeft } from "../client/presence/orbitsLeft.ts";
import { presenceStatusLines } from "../client/components/settings/presenceStatus.ts";
import type { Pane, Workspace } from "../client/workspace.ts";
import type { PresenceStatus } from "../shared/presence/types.ts";
import { setLang, t } from "../client/i18n.ts";
function pane(id: string, paths: string[], mode: Pane["mode"] = "edit", follow: Pane["follow"] = null): Pane {
return { id, tabs: paths.map((path) => ({ path, pinned: false, ephemeral: false })), active: paths.length - 1, mode, follow, bookTarget: null };
}
function ws(panes: Pane[], focus = panes[0].id, noteFocus = panes[0].id): Workspace {
return {
panes: Object.fromEntries(panes.map((p) => [p.id, p])),
layout: { columns: [panes.map((p) => p.id)], colWeights: [1], rowWeights: Object.fromEntries(panes.map((p) => [p.id, 1 / panes.length])) },
focus,
noteFocus,
layoutName: null,
};
}
const NOW = 1_000_000;
function inputs(workspace: Workspace, extra: Partial<PresenceInputs> = {}): PresenceInputs {
return { workspace, readingMode: false, client: "AbCdEfGh12345678", cardsLeft: null, hiddenAt: null, ...extra };
}
describe("a tab's report, surface by surface", () => {
it("a note in the editor is writing, with its path; reading mode makes it reading", () => {
const w = ws([pane("p1", ["Drafts/Chapter 3.md"])]);
assert.deepEqual(deriveReport(inputs(w), NOW - 4_000, NOW), { client: "AbCdEfGh12345678", kind: "writing", path: "Drafts/Chapter 3.md", idleMs: 4_000 });
assert.equal(deriveReport(inputs(w, { readingMode: true }), NOW, NOW).kind, "reading");
assert.equal(deriveReport(inputs(ws([pane("p1", ["a.md"], "reading")])), NOW, NOW).kind, "reading");
});
it("a book, a drawing, the library and the media shelf", () => {
assert.deepEqual(deriveReport(inputs(ws([pane("p1", ["Books/Muqaddimah.pdf"])])), NOW, NOW), { client: "AbCdEfGh12345678", kind: "book", path: "Books/Muqaddimah.pdf", idleMs: 0 });
assert.equal(deriveReport(inputs(ws([pane("p1", ["Books/x.epub"])])), NOW, NOW).kind, "book");
const drawing = deriveReport(inputs(ws([pane("p1", ["Sketches/plan.excalidraw.md"])])), NOW, NOW);
assert.equal(drawing.kind, "drawing");
assert.equal(drawing.path, "Sketches/plan.excalidraw.md");
const library = deriveReport(inputs(ws([pane("p1", [], "library")])), NOW, NOW);
assert.equal(library.kind, "library");
assert.equal(library.path, undefined);
assert.equal(deriveReport(inputs(ws([pane("p1", ["~media"])])), NOW, NOW).kind, "library");
});
it("an Orbits session is the deck with its cards left; the shelf is browsing", () => {
const session = deriveReport(inputs(ws([pane("p1", ["~orbits/Decks/Arabic verbs.md"])]), { cardsLeft: 12 }), NOW, NOW);
assert.deepEqual(session, { client: "AbCdEfGh12345678", kind: "orbits", path: "Decks/Arabic verbs.md", cardsLeft: 12, idleMs: 0 });
const section = deriveReport(inputs(ws([pane("p1", ["~orbits/Decks/Arabic verbs.md#Lesson 3"])]), { cardsLeft: 3 }), NOW, NOW);
assert.equal(section.path, "Decks/Arabic verbs.md");
// The implicit deck has no note, so no path; the count still goes.
const implicit = deriveReport(inputs(ws([pane("p1", ["~orbits/*"])]), { cardsLeft: 5 }), NOW, NOW);
assert.equal(implicit.kind, "orbits");
assert.equal(implicit.path, undefined);
assert.equal(implicit.cardsLeft, 5);
const shelf = deriveReport(inputs(ws([pane("p1", ["~orbits"])]), { cardsLeft: 5 }), NOW, NOW);
assert.equal(shelf.kind, "browsing");
assert.equal(shelf.cardsLeft, undefined, "a count belongs to a session only");
});
it("every other surface is browsing, with no subject", () => {
for (const path of ["~graph", "~sigils", "~calendar", "~today", "~timeline", "~feeds", "~review-week"]) {
const r = deriveReport(inputs(ws([pane("p1", [path])])), NOW, NOW);
assert.equal(r.kind, "browsing", path);
assert.equal(r.path, undefined, path);
}
assert.equal(deriveReport(inputs(ws([pane("p1", [])])), NOW, NOW).kind, "browsing", "an empty pane");
assert.equal(deriveReport(inputs(ws([pane("p1", ["a.md"], "graph")])), NOW, NOW).kind, "browsing", "the local graph");
});
it("reads the FOCUSED pane: a book beside a note, with the keyboard in the book, is the book", () => {
const w = ws([pane("p1", ["Notes/On time.md"]), pane("p2", ["Books/Confessions.pdf"])], "p2", "p1");
assert.equal(deriveReport(inputs(w), NOW, NOW).kind, "book");
// A follower pane renders the note-focus pane's note, and so reports it.
const f = ws([pane("p1", ["Notes/On time.md"]), pane("p2", [], "reading", "note")], "p2", "p1");
assert.deepEqual([deriveReport(inputs(f), NOW, NOW).kind, deriveReport(inputs(f), NOW, NOW).path], ["reading", "Notes/On time.md"]);
});
it("never carries a title: the server reads the note and judges it", () => {
const r = deriveReport(inputs(ws([pane("p1", ["Private/Diary.md"])])), NOW, NOW);
assert.equal("title" in r, false);
});
});
describe("idle, from the tab's side", () => {
it("is the time since the last input, never negative", () => {
const w = ws([pane("p1", ["a.md"])]);
assert.equal(deriveReport(inputs(w), NOW - 90_000, NOW).idleMs, 90_000);
assert.equal(deriveReport(inputs(w), NOW + 500, NOW).idleMs, 0);
});
it("a hidden tab counts from the moment it was hidden, whatever came after", () => {
const w = ws([pane("p1", ["a.md"])]);
assert.equal(deriveReport(inputs(w, { hiddenAt: NOW - 30_000 }), NOW - 1_000, NOW).idleMs, 30_000);
// Input before it was hidden is older still, and wins.
assert.equal(deriveReport(inputs(w, { hiddenAt: NOW - 30_000 }), NOW - 60_000, NOW).idleMs, 60_000);
});
it("a change is the kind, the path or the count — not the heartbeat's idle time", () => {
const a = deriveReport(inputs(ws([pane("p1", ["a.md"])])), NOW, NOW);
assert.equal(sameSubject(a, { ...a, idleMs: 40_000 }), true);
assert.equal(sameSubject(a, { ...a, path: "b.md" }), false);
assert.equal(sameSubject(a, { ...a, kind: "reading" }), false);
assert.equal(sameSubject(a, { ...a, cardsLeft: 3 }), false);
assert.equal(sameSubject(null, a), false);
});
});
describe("the tab's id and the session's count", () => {
it("is sixteen characters of [A-Za-z0-9]", () => {
const id = mintClientId(new Uint8Array(Array.from({ length: 16 }, (_, i) => i * 37)));
assert.match(id, /^[A-Za-z0-9]{16}$/);
// The server takes 8–64 of [A-Za-z0-9_-] (shared/presence/types.ts).
assert.match(id, /^[A-Za-z0-9_-]{8,64}$/);
});
it("the session view's count is told once per change, and null when it leaves", () => {
const seen: (number | null)[] = [];
const off = onOrbitsLeft(() => seen.push(getOrbitsLeft()));
setOrbitsLeft(12);
setOrbitsLeft(12);
setOrbitsLeft(11);
setOrbitsLeft(null);
setOrbitsLeft(-1);
off();
setOrbitsLeft(4);
assert.deepEqual(seen, [12, 11, null]);
setOrbitsLeft(null);
});
});
describe("Settings → Discord → Status, in words", () => {
const base: PresenceStatus = { enabled: true, mode: "title", applicationId: "set", host: "server", connection: "connected", clients: 1, activity: null };
const said = (s: PresenceStatus): string[] => presenceStatusLines(s).map((l) => l.key);
it("names the first thing to fix, in the order a reader fixes them", () => {
assert.deepEqual(said({ ...base, mode: "off", enabled: false }), ["presenceStatOff"]);
assert.deepEqual(said({ ...base, applicationId: "placeholder", enabled: false }), ["presenceStatOff"]);
assert.deepEqual(said({ ...base, host: "none" }), ["presenceStatNoHost"]);
assert.deepEqual(said({ ...base, connection: "invalid-id" }), ["presenceStatInvalidId"]);
assert.deepEqual(said({ ...base, connection: "searching" }), ["presenceStatSearchingServer"]);
assert.deepEqual(said({ ...base, host: "desktop", connection: "searching" }), ["presenceStatSearchingDesktop"]);
});
it("says who is connected and previews what Discord shows", () => {
const lines = presenceStatusLines({ ...base, activity: { type: 0, details: "Writing", state: "Chapter 3" } });
assert.deepEqual(lines, [{ key: "presenceStatHostServer" }, { key: "presenceStatShowing", preview: "Writing · Chapter 3" }]);
assert.deepEqual(said({ ...base, host: "desktop" }), ["presenceStatHostDesktop", "presenceStatShowingNothing"]);
});
it("speaks both languages, the Arabic lines opening in Arabic so they read right to left", () => {
for (const lang of ["en", "ar"] as const) {
setLang(lang);
for (const key of ["presenceStatOff", "presenceStatNoHost", "presenceStatInvalidId", "presenceStatSearchingServer", "presenceStatSearchingDesktop"] as const) {
const text = t(key);
assert.ok(text.length > 0, key);
if (lang === "ar") assert.match(text, /^[-ۿ]/, `${key} opens with a Latin word: "${text}"`);
assert.doesNotMatch(text, /[٠-٩]/, `${key} uses Eastern digits`);
}
}
setLang("en");
});
});