Manual: full participation guide (media/accounts/decisions/safety) + Enter-to-door + provisioner env passthrough

This commit is contained in:
glass beadian 2026-10-12 15:49:25 +00:00
parent 07d2df8c53
commit 6161ac4740
5 changed files with 251 additions and 42 deletions

View file

@ -5,6 +5,7 @@ import {
MANUAL_TITLE,
MANUAL_INTRO,
MANUAL_PATHS_HEADING,
MANUAL_TOC_HEADING,
PARTICIPATION_MANUAL,
PARTICIPATION_PATHS,
DEFAULT_MANUAL_LANGUAGE,
@ -13,6 +14,7 @@ import {
onboardingHeader,
onboardingMessages,
onboardingLanguageOrder,
manualToc,
type ManualLanguage,
} from "@tactical-media-broadcast-hub/core";
@ -22,10 +24,24 @@ import {
* Matrix onboarding bot, so these assertions protect BOTH surfaces at once.
*/
test("the manual covers the four sections the design mandates", () => {
/** The manual outline, in reading order: what a collective gets → how it decides → solo → safety → help. */
const SECTION_IDS = [
"what-this-is",
"library",
"rooms",
"accounts",
"live",
"decisions",
"diffusion",
"solo",
"safety",
"help",
] as const;
test("the manual covers the sections the design mandates, in reading order", () => {
assert.deepEqual(
PARTICIPATION_MANUAL.map((section) => section.id),
["what-this-is", "rooms", "safety", "help"],
[...SECTION_IDS],
);
for (const section of PARTICIPATION_MANUAL) {
for (const lang of MANUAL_LANGUAGES) {
@ -50,23 +66,82 @@ test("the three participation paths are exactly join, start and solo", () => {
test("the safety section states the non-negotiables in both languages", () => {
const safety = PARTICIPATION_MANUAL.find((section) => section.id === "safety")!;
for (const [lang, needles] of [
["es", ["cifrado de extremo a extremo", "borradones", "invitación", "cifrado"]],
["en", ["end-to-end encryption", "redaction", "invitation", "ciphertext"]],
["es", ["cifrado de extremo a extremo", "borradones", "invitación", "cifrado", "contribuciones"]],
["en", ["end-to-end encryption", "redaction", "invitation", "ciphertext", "contributions"]],
] as const) {
for (const needle of needles) assert.ok(safety.body[lang].toLowerCase().includes(needle), `${lang}: ${needle}`);
}
// The default contribution policy is stated plainly, not buried.
assert.ok(safety.body.es.includes("borrar tu cuenta no borra tus contribuciones"));
assert.ok(safety.body.en.includes("deleting your account does not delete your contributions"));
});
test("the rooms section names all three room types and their privacy stance", () => {
const rooms = PARTICIPATION_MANUAL.find((section) => section.id === "rooms")!;
for (const [lang, needles] of [
["es", ["abierta", "cerrada", "acogida", "nunca está cifrada", "nace cifrada"]],
["en", ["open room", "closed room", "onboarding room", "never end-to-end encrypted", "born encrypted"]],
["es", ["abierta", "cerrada", "acogida", "nunca está cifrada", "nace cifrada", "telegram"]],
["en", ["open room", "closed room", "onboarding room", "never end-to-end encrypted", "born encrypted", "telegram"]],
] as const) {
for (const needle of needles) assert.ok(rooms.body[lang].toLowerCase().includes(needle), `${lang}: ${needle}`);
}
});
test("the library section explains folders, auto-archiving and reorganising without dead links", () => {
const library = PARTICIPATION_MANUAL.find((section) => section.id === "library")!;
for (const [lang, needles] of [
["es", ["carpeta", "archivados", "renombrar", "sin romper enlaces"]],
["en", ["folder", "archived", "renamed", "without breaking links"]],
] as const) {
for (const needle of needles) assert.ok(library.body[lang].toLowerCase().includes(needle), `${lang}: ${needle}`);
}
});
test("the accounts section names the networks and the approval-before-publish path", () => {
const accounts = PARTICIPATION_MANUAL.find((section) => section.id === "accounts")!;
for (const [lang, needles] of [
["es", ["Mastodon", "Bluesky", "Telegram", "PeerTube", "Facebook", "Instagram", "Twitch", "Discord", "aprobación"]],
["en", ["Mastodon", "Bluesky", "Telegram", "PeerTube", "Facebook", "Instagram", "Twitch", "Discord", "approval"]],
] as const) {
for (const needle of needles) assert.ok(accounts.body[lang].includes(needle), `${lang}: ${needle}`);
}
});
test("the live section covers switching, overlays, auto-director and distributed transcoding", () => {
const live = PARTICIPATION_MANUAL.find((section) => section.id === "live")!;
for (const [lang, needles] of [
["es", ["fuentes", "rótulos", "prioridades", "nodos de trabajo"]],
["en", ["sources", "overlays", "priorities", "render workers"]],
] as const) {
for (const needle of needles) assert.ok(live.body[lang].toLowerCase().includes(needle), `${lang}: ${needle}`);
}
});
test("the decisions section states the gate, the reset-on-edit rule and the verifiable record", () => {
const decisions = PARTICIPATION_MANUAL.find((section) => section.id === "decisions")!;
for (const [lang, needles] of [
["es", ["aprobación", "votación", "encadenado"]],
["en", ["approvals", "vote", "chained"]],
] as const) {
for (const needle of needles) assert.ok(decisions.body[lang].toLowerCase().includes(needle), `${lang}: ${needle}`);
}
});
test("the diffusion section says subcollectives push UP and the parent releases", () => {
const diffusion = PARTICIPATION_MANUAL.find((section) => section.id === "diffusion")!;
assert.ok(diffusion.body.es.toLowerCase().includes("hacia arriba"));
assert.ok(diffusion.body.en.toLowerCase().includes("upward"));
});
test("the solo section covers personal storage, video access and joining later by invite", () => {
const solo = PARTICIPATION_MANUAL.find((section) => section.id === "solo")!;
for (const [lang, needles] of [
["es", ["archivos", "vídeo", "invitación"]],
["en", ["files", "video", "invite"]],
] as const) {
for (const needle of needles) assert.ok(solo.body[lang].toLowerCase().includes(needle), `${lang}: ${needle}`);
}
});
test("Spanish is the default and first language everywhere", () => {
assert.equal(DEFAULT_MANUAL_LANGUAGE, "es");
assert.deepEqual(MANUAL_LANGUAGE_ORDER, ["es", "en"]);
@ -76,6 +151,7 @@ test("Spanish is the default and first language everywhere", () => {
assert.ok(MANUAL_TITLE.es && MANUAL_TITLE.en);
assert.ok(MANUAL_INTRO.es && MANUAL_INTRO.en);
assert.ok(MANUAL_PATHS_HEADING.es && MANUAL_PATHS_HEADING.en);
assert.ok(MANUAL_TOC_HEADING.es && MANUAL_TOC_HEADING.en);
});
test("isManualLanguage accepts exactly es and en", () => {
@ -86,11 +162,26 @@ test("isManualLanguage accepts exactly es and en", () => {
}
});
test("onboardingMessages is one header plus one message per section, in section order", () => {
test("manualToc is one numbered line per section, in section order", () => {
for (const lang of MANUAL_LANGUAGES) {
const lines = manualToc(lang).split("\n");
assert.equal(lines.length, PARTICIPATION_MANUAL.length);
for (const [index, section] of PARTICIPATION_MANUAL.entries()) {
assert.ok(lines[index]!.startsWith(`${index + 1}. `), `${lang} line ${index} numbered`);
assert.ok(lines[index]!.includes(section.heading[lang]), `${lang} line ${index} carries the heading`);
}
}
});
test("onboardingMessages is header+contents first, then one message per section, in section order", () => {
for (const lang of MANUAL_LANGUAGES) {
const messages = onboardingMessages(lang);
assert.equal(messages.length, PARTICIPATION_MANUAL.length + 1);
assert.ok(messages[0]!.startsWith(onboardingHeader(lang)));
assert.ok(messages[0]!.startsWith(onboardingHeader(lang)), `${lang} header first`);
// The contents list rides in the first message so a long manual stays navigable.
for (const section of PARTICIPATION_MANUAL) {
assert.ok(messages[0]!.includes(section.heading[lang]), `${lang} contents lists ${section.id}`);
}
for (const [index, section] of PARTICIPATION_MANUAL.entries()) {
const message = messages[index + 1];
assert.ok(message !== undefined, `${lang} section ${index} exists`);
@ -126,7 +217,7 @@ test("language type is closed: assigning an unknown value is a compile-time erro
});
test("manual copy stays plain-language: no dev jargon in user-facing sections", () => {
const jargon = ["api", "http", "rls", "endpoint", "json", "sdk", "federate=true", "megolm"];
const jargon = ["api", "http", "rls", "endpoint", "json", "sdk", "federate=true", "megolm", "ocis", "webdav", "r2"];
for (const section of PARTICIPATION_MANUAL) {
for (const lang of MANUAL_LANGUAGES) {
const body = section.body[lang].toLowerCase();