Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1 | import assert from "node:assert/strict"; |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 2 | import { readFileSync } from "node:fs"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 3 | import { test } from "node:test"; |
| 4 | ||
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 5 | import { ACCOUNT_SETTINGS } from "./account-settings.ts"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 6 | import { CONTACT } from "./legal.ts"; |
| 7 | import { | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 8 | G1T_INVITES, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 9 | HAVE_AN_INVITE, |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 10 | bringIntoChoices, |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 11 | inviteDraft, |
| 12 | inviteKind, | |
| 13 | invitePageCopy, | |
| 14 | invitesPage, | |
| 15 | peoplePages, | |
| 16 | workspaceInviteCopy, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 17 | INVITES_CONTACT, |
| 18 | cleanCode, | |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 19 | cleanProof, |
| 20 | invitePath, | |
| 21 | inviteSignUpCopy, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 22 | inviteFor, |
| 23 | inviteLink, | |
| 24 | inviteState, | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 25 | landingFor, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 26 | looksAutomated, |
| 27 | moreInvitesMailto, | |
| 28 | remainingLine, | |
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 29 | sharedDomainsHint, |
| 30 | sharedInviteLine, | |
| 31 | sharedInviteLink, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 32 | signUpCopy, |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 33 | suggestUsername, |
| 34 | welcomeCookie, | |
| 35 | clearWelcome, | |
| 36 | welcomes, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 37 | } from "./invites.ts"; |
| 38 | ||
| 39 | const CODE = "g1t-k7m2-q9xd-4hpw-abcd-0123-4567-89ef-ghjk"; | |
| 40 | ||
| 41 | test("while invite-only, nobody is offered a plain sign-up", () => { | |
| Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite | 42 | // Sign up everywhere; only the sign-up page says registration takes an invite. |
| 43 | assert.deepEqual(signUpCopy(), { primary: "Sign up", secondary: null }); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 44 | assert.equal(HAVE_AN_INVITE, "/register#invite"); |
| 45 | }); | |
| 46 | ||
| 47 | test("asking for more invites goes to support with the [g1t Invites] subject", () => { | |
| 48 | assert.equal(INVITES_CONTACT, CONTACT.support); | |
| 49 | assert.equal(moreInvitesMailto(), "mailto:hey@flagon.io?subject=%5Bg1t%20Invites%5D%20More%20invites"); | |
| 50 | assert.equal( | |
| 51 | moreInvitesMailto("acme"), | |
| 52 | "mailto:hey@flagon.io?subject=%5Bg1t%20Invites%5D%20More%20invites%20for%20acme", | |
| 53 | ); | |
| 54 | }); | |
| 55 | ||
| 56 | test("an invite link is on g1t.sh unless told otherwise", () => { | |
| 57 | assert.equal(inviteLink(CODE), `https://g1t.sh/invite/${CODE}`); | |
| 58 | assert.equal(inviteLink(CODE, "http://localhost:8787/"), `http://localhost:8787/invite/${CODE}`); | |
| 59 | }); | |
| 60 | ||
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 61 | const PROOF = "4f9c2a7e0b13d5c84f9c2a7e0b13d5c84f9c2a7e0b13d5c84f9c2a7e0b13d5c8"; |
| 62 | ||
| 63 | test("an invite email's proof is kept only when it looks like one, and goes along to the invite's page", () => { | |
| 64 | assert.equal(cleanProof(PROOF), PROOF); | |
| 65 | assert.equal(cleanProof(` ${PROOF.toUpperCase()} `), PROOF); | |
| 66 | assert.equal(cleanProof("not-a-proof"), null); | |
| 67 | assert.equal(cleanProof("abc"), null); | |
| 68 | assert.equal(cleanProof("a".repeat(500)), null); | |
| 69 | assert.equal(cleanProof(null), null); | |
| 70 | assert.equal(invitePath(CODE, PROOF), `/invite/${CODE}?proof=${PROOF}`); | |
| 71 | assert.equal(invitePath(CODE, null), `/invite/${CODE}`); | |
| 72 | assert.equal(invitePath(CODE), `/invite/${CODE}`); | |
| 73 | }); | |
| 74 | ||
| 75 | test("signing up from the invite email says the address is confirmed already; otherwise the code step applies", () => { | |
| 76 | const base = { address: "ada@example.com", emailProven: false, workspace: { name: "Flagon, Inc." }, repository: null }; | |
| 77 | const proven = inviteSignUpCopy({ ...base, emailProven: true }); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 78 | assert.equal(proven.intro, "You can join Flagon, Inc. as soon as you create it: accept the invitation then."); |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 79 | assert.match(proven.confirmed ?? "", /^ada@example\.com is confirmed: you came here from the invite we emailed to it/); |
| 80 | assert.match(proven.hint, /confirmed already/); | |
| 81 | assert.doesNotMatch(proven.hint, /code/); | |
| 82 | ||
| 83 | // No proof (a code typed in, or a link passed on): nothing new is said. | |
| 84 | const plain = inviteSignUpCopy(base); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 85 | assert.equal(plain.intro, "You can join Flagon, Inc. as soon as you confirm your email: accept the invitation then."); |
| Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm | 86 | assert.equal(plain.confirmed, null); |
| 87 | assert.equal(plain.hint, "Your invite was sent here. We email it a code to confirm it before you start."); | |
| 88 | ||
| 89 | // An invite for anyone with the code has no address to prove. | |
| 90 | const open = inviteSignUpCopy({ ...base, address: null, emailProven: true, workspace: null }); | |
| 91 | assert.equal(open.confirmed, null); | |
| 92 | assert.equal(open.intro, "It takes a minute."); | |
| 93 | assert.equal(open.hint, "We email it a code to confirm it before you start."); | |
| 94 | ||
| 95 | const repo = inviteSignUpCopy({ ...base, workspace: null, repository: { name: "flagon-io/g1t" }, emailProven: true }); | |
| 96 | assert.equal(repo.intro, "You get flagon-io/g1t as soon as you create it."); | |
| 97 | }); | |
| 98 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 99 | test("a pasted link or code is tidied to the code", () => { |
| 100 | assert.equal(cleanCode(CODE), CODE); | |
| 101 | assert.equal(cleanCode(` ${CODE} `), CODE); | |
| 102 | assert.equal(cleanCode(`https://g1t.sh/invite/${CODE}`), CODE); | |
| 103 | assert.equal(cleanCode(`https://g1t.sh/register?invite=${CODE}&next=%2F`), CODE); | |
| 104 | assert.equal(cleanCode("g1t-k7m2 q9xd"), "g1t-k7m2q9xd"); | |
| 105 | assert.equal(cleanCode(null), ""); | |
| 106 | assert.equal(cleanCode("x".repeat(500)).length, 80); | |
| 107 | }); | |
| 108 | ||
| 109 | test("each invite says where it stands and whom it is for", () => { | |
| 110 | const base = { redeemedBy: null, email: null, workspace: null }; | |
| 111 | assert.deepEqual(inviteState({ ...base, status: "pending" }), { label: "Pending", tone: "pending" }); | |
| 112 | assert.deepEqual(inviteState({ ...base, status: "redeemed", redeemedBy: "ada" }), { label: "Joined as @ada", tone: "done" }); | |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 113 | assert.deepEqual(inviteState({ ...base, status: "awaiting_confirmation", redeemedBy: "ada" }), { |
| 114 | label: "@ada is confirming their email", | |
| 115 | tone: "pending", | |
| 116 | }); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 117 | assert.deepEqual(inviteState({ ...base, status: "expired" }), { label: "Expired", tone: "dead" }); |
| 118 | assert.deepEqual(inviteState({ ...base, status: "revoked" }), { label: "Revoked", tone: "dead" }); | |
| 119 | assert.equal(inviteFor({ ...base, status: "pending" }), "Anyone with the link"); | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 120 | assert.equal(inviteFor({ ...base, status: "pending", email: "ada@example.com", workspace: "acme" }), "ada@example.com"); |
| 121 | assert.equal(inviteFor({ ...base, status: "pending", invitee: "daweazl", workspace: "flagon-io" }), "@daweazl"); | |
| 122 | // Which kind each is, beside whom it is for. | |
| 123 | assert.deepEqual(inviteKind({ workspace: null }), { kind: "g1t", label: "Invite to g1t" }); | |
| 124 | assert.deepEqual(inviteKind({ workspace: "flagon-io" }), { kind: "workspace", label: "Invite to join flagon-io" }); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 125 | assert.deepEqual(inviteState({ ...base, status: "awaiting_answer", redeemedBy: "daweazl" }), { label: "Waiting for @daweazl to accept", tone: "pending" }); |
| 126 | assert.deepEqual(inviteState({ ...base, status: "declined", invitee: "daweazl" }), { label: "@daweazl declined", tone: "dead" }); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 127 | }); |
| 128 | ||
| 129 | test("what is left reads plainly", () => { | |
| 130 | assert.equal(remainingLine({ limit: 5, used: 2, remaining: 3 }), "3 of 5 invites left"); | |
| 131 | assert.equal(remainingLine({ limit: 1, used: 0, remaining: 1 }), "1 of 1 invite left"); | |
| 132 | assert.equal(remainingLine({ limit: 5, used: 5, remaining: 0 }), "You have used all 5 of your invites"); | |
| 133 | assert.equal(remainingLine({ limit: null, used: 40, remaining: null }), "No limit on your invites"); | |
| 134 | }); | |
| 135 | ||
| 136 | test("bots that fill the hidden field or answer instantly are turned away", () => { | |
| 137 | const form = (fields: Record<string, string>) => ({ get: (name: string) => fields[name] ?? null }); | |
| 138 | const now = 1_000_000; | |
| 139 | assert.equal(looksAutomated(form({ website: "http://spam.example" }), now), true); | |
| 140 | assert.equal(looksAutomated(form({ started: String(now - 200) }), now), true); | |
| 141 | assert.equal(looksAutomated(form({ started: String(now - 10_000) }), now), false); | |
| 142 | assert.equal(looksAutomated(form({}), now), false); | |
| 143 | assert.equal(looksAutomated(form({ website: " " }), now), false); | |
| 144 | }); | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 145 | |
| 146 | test("a username is suggested from the invited address", () => { | |
| 147 | assert.equal(suggestUsername("ada.lovelace@example.com"), "ada-lovelace"); | |
| 148 | assert.equal(suggestUsername("Margaret_Hamilton+g1t@example.com"), "margaret-hamilton"); | |
| 149 | assert.equal(suggestUsername("--x--@example.com"), "x"); | |
| 150 | assert.equal(suggestUsername(`${"a".repeat(38)}.b@example.com`), "a".repeat(38)); | |
| 151 | assert.equal(suggestUsername("...@example.com"), ""); | |
| 152 | assert.equal(suggestUsername(null), ""); | |
| 153 | }); | |
| 154 | ||
| 155 | test("an invite lands in its workspace, else its repository", () => { | |
| 156 | assert.equal(landingFor({ workspace: { slug: "Flagon-IO" }, repository: null }), "flagon-io"); | |
| 157 | assert.equal(landingFor({ workspace: null, repository: { name: "flagon-io/g1t" } }), "flagon-io/g1t"); | |
| 158 | assert.equal(landingFor({ workspace: null, repository: null }), null); | |
| 159 | }); | |
| 160 | ||
| 161 | test("the welcome is for one place, and ends", () => { | |
| 162 | const set = welcomeCookie("flagon-io/g1t", true); | |
| 163 | assert.match(set, /^g1t_welcome=flagon-io%2Fg1t; Path=\/; Max-Age=300; HttpOnly; SameSite=Lax; Secure$/); | |
| 164 | const header = `a=1; ${set.split(";")[0]}; b=2`; | |
| 165 | assert.equal(welcomes(header, "flagon-io/g1t"), true); | |
| 166 | assert.equal(welcomes(header, "flagon-io"), false); | |
| 167 | assert.equal(welcomes("g1t_welcome=flagon-io", "Flagon-IO"), true); | |
| 168 | assert.equal(welcomes("g1t_welcome=%E0%A4%A", "flagon-io"), false); | |
| 169 | assert.equal(welcomes("g1t_welcome=..%2F..%2Fx", "../../x"), false); | |
| 170 | assert.equal(welcomes(null, "flagon-io"), false); | |
| 171 | assert.match(clearWelcome(false), /^g1t_welcome=; Path=\/; Max-Age=0; HttpOnly; SameSite=Lax$/); | |
| 172 | }); | |
| Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038) | 173 | |
| 174 | test("a shared invite link names its group above the sign-up form", () => { | |
| 175 | assert.equal(sharedInviteLine("Cloudflare judges"), "Invited as part of Cloudflare judges"); | |
| 176 | assert.equal(sharedInviteLine(" Hacker News readers "), "Invited as part of Hacker News readers"); | |
| 177 | // A one-person invite has no group, and says nothing of the kind. | |
| 178 | assert.equal(sharedInviteLine(null), null); | |
| 179 | assert.equal(sharedInviteLine(undefined), null); | |
| 180 | assert.equal(sharedInviteLine(" "), null); | |
| 181 | }); | |
| 182 | ||
| 183 | test("a shared invite link is sign-up with its code filled in", () => { | |
| 184 | assert.equal(sharedInviteLink(CODE), `https://g1t.sh/register?invite=${CODE}`); | |
| 185 | assert.equal(sharedInviteLink(CODE, "http://localhost:5173/"), `http://localhost:5173/register?invite=${CODE}`); | |
| 186 | // The register page reads the code back out of its own link. | |
| 187 | assert.equal(cleanCode(sharedInviteLink(CODE)), CODE); | |
| 188 | }); | |
| 189 | ||
| 190 | test("a shared link limited to domains says which, on the email field", () => { | |
| 191 | assert.equal(sharedDomainsHint([]), undefined); | |
| 192 | assert.equal(sharedDomainsHint(null), undefined); | |
| 193 | assert.equal(sharedDomainsHint(["cloudflare.com"]), "This invite is for addresses at cloudflare.com. Use yours there."); | |
| 194 | assert.equal( | |
| 195 | sharedDomainsHint(["a.com", "b.com", "c.com"]), | |
| 196 | "This invite is for addresses at a.com, b.com or c.com. Use yours there.", | |
| 197 | ); | |
| 198 | }); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 199 | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 200 | test("an invite to g1t can also invite its person to a workspace you own that can add people, never chosen for you", () => { |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 201 | const memberships = [ |
| 202 | { slug: "flagon-io", name: "Flagon, Inc.", role: "owner" as const }, | |
| 203 | { slug: "side", name: "side", role: "owner" as const }, | |
| 204 | { slug: "friends", name: "Friends", role: "member" as const }, | |
| 205 | ]; | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 206 | // Free ones and ones you only belong to are not offered; nothing is chosen, not even the current one. |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 207 | const here = bringIntoChoices(memberships, ["side"], "flagon-io"); |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 208 | assert.deepEqual(here, { options: [{ slug: "flagon-io", name: "Flagon, Inc." }], note: null }); |
| 209 | assert.equal("chosen" in here, false); | |
| 210 | // In a free workspace: not offered, and the form says why. | |
| 211 | assert.match(bringIntoChoices(memberships, ["side"], "side").note ?? "", /side is on the free plan, so it cannot add people/); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 212 | // In one you are only a member of. |
| 213 | assert.match(bringIntoChoices(memberships, [], "friends").note ?? "", /Only the owners of friends/); | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 214 | assert.deepEqual(bringIntoChoices([], [], null), { options: [], note: null }); |
| 215 | }); | |
| 216 | ||
| 217 | /** A submitted form, as `inviteDraft` reads it. */ | |
| 218 | const form = (fields: Record<string, string>) => ({ get: (name: string) => fields[name] ?? null }); | |
| 219 | ||
| 220 | test("an invite to g1t sends no workspace unless its box is ticked", () => { | |
| 221 | // Off by default: a form without the box sends no `join`, even with a workspace left in it. | |
| 222 | assert.deepEqual(inviteDraft(form({ intent: "create-invite", email: " ada@example.com ", charge: "mine" })), { | |
| 223 | email: "ada@example.com", | |
| 224 | workspace: null, | |
| 225 | }); | |
| 226 | const untickedButFilled = inviteDraft(form({ email: "", join: "flagon-io", join_role: "owner" })); | |
| 227 | assert.equal("join" in untickedButFilled, false); | |
| 228 | assert.equal("joinRole" in untickedButFilled, false); | |
| 229 | assert.equal(untickedButFilled.email, null); | |
| 230 | // Ticked: the workspace and its role go with it. | |
| 231 | assert.deepEqual(inviteDraft(form({ also_join: "on", join: "flagon-io", join_role: "owner", charge: "flagon-io" })), { | |
| 232 | email: null, | |
| 233 | workspace: "flagon-io", | |
| 234 | join: "flagon-io", | |
| 235 | joinRole: "owner", | |
| 236 | }); | |
| 237 | // Ticked with no workspace chosen: still none. Any role but owner is member. | |
| 238 | assert.equal("join" in inviteDraft(form({ also_join: "on", join: "" })), false); | |
| 239 | assert.equal(inviteDraft(form({ also_join: "on", join: "acme", join_role: "admin" })).joinRole, "member"); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 240 | }); |
| 241 | ||
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 242 | test("the invites form keeps the workspace behind an unticked box", () => { |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 243 | const section = readFileSync(new URL("../components/invites-section.tsx", import.meta.url), "utf8"); |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 244 | assert.match(section, /useState\(false\)/); |
| 245 | assert.match(section, /name="also_join"/); | |
| 246 | // The workspace and role fields are drawn only once the box is ticked, so nothing else is sent. | |
| 247 | assert.match(section, /\{alsoJoin && \(\s*<div[^]*?name="join"[^]*?name="join_role"/); | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 248 | // No workspace is chosen for them: the select starts on its placeholder, and is required. |
| 249 | assert.match(section, /name="join"\s*required\s*placeholder="Choose a workspace"/); | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 250 | assert.doesNotMatch(section, /bringInto\.chosen|Bring them into/); |
| 251 | }); | |
| 252 | ||
| 253 | test("invites to g1t are made only while sign-up takes one; after that only the list stays", () => { | |
| 254 | assert.deepEqual(invitesPage("invite", 0), { form: true, listed: true }); | |
| 255 | assert.deepEqual(invitesPage(null, 0), { form: true, listed: true }); | |
| 256 | // Open: no form; the menus list the page only with invites to look back on. | |
| 257 | assert.deepEqual(invitesPage("open", 0), { form: false, listed: false }); | |
| 258 | assert.deepEqual(invitesPage("open", 3), { form: false, listed: true }); | |
| 259 | assert.match(G1T_INVITES.open, /^Anyone can sign up for g1t now/); | |
| 260 | assert.match(G1T_INVITES.open, /workspace's People page/); | |
| 261 | }); | |
| 262 | ||
| 263 | test("the two invites say which they are", () => { | |
| 264 | // Settings → Invites: an account, and no workspace. | |
| 265 | assert.equal(G1T_INVITES.heading, "Invite people to g1t"); | |
| 266 | assert.equal(ACCOUNT_SETTINGS.invites.heading, G1T_INVITES.heading); | |
| 267 | assert.equal(ACCOUNT_SETTINGS.invites.title, G1T_INVITES.nav); | |
| 268 | assert.equal(ACCOUNT_SETTINGS.invites.about, G1T_INVITES.about); | |
| 269 | assert.match(G1T_INVITES.about, /lets one person make an account\. It does not add them to any workspace/); | |
| 270 | assert.equal(G1T_INVITES.alsoJoin, "Also invite them to a workspace"); | |
| 271 | // A workspace's People page: an invitation to accept or decline, which signs up whoever has no account. | |
| 272 | const closed = workspaceInviteCopy("Flagon, Inc.", true); | |
| 273 | assert.equal(closed.heading, "Invite to Flagon, Inc."); | |
| 274 | assert.match(closed.hint, /invitation to join Flagon, Inc\..*join only if they accept/); | |
| 275 | assert.match(closed.hint, /If they do not have a g1t account yet, the invitation also lets them sign up/); | |
| 276 | assert.equal(closed.elsewhere, "To invite someone to g1t without adding them to Flagon, Inc., use Settings → Invites."); | |
| 277 | // Once anyone can sign up, there is no invite to g1t to point to. | |
| 278 | const open = workspaceInviteCopy("Flagon, Inc.", false); | |
| 279 | assert.equal(open.elsewhere, null); | |
| 280 | assert.doesNotMatch(open.hint, /one of yours/); | |
| 281 | // Settings → Invites points to the People pages of the workspaces you own, the current one first. | |
| 282 | assert.deepEqual( | |
| 283 | peoplePages( | |
| 284 | [ | |
| 285 | { slug: "side", name: null, role: "owner" }, | |
| 286 | { slug: "Flagon-io", name: "Flagon, Inc.", role: "owner" }, | |
| 287 | { slug: "friends", name: "Friends", role: "member" }, | |
| 288 | ], | |
| 289 | "flagon-io", | |
| 290 | ), | |
| 291 | [ | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 292 | { slug: "flagon-io", name: "Flagon, Inc.", to: "/flagon-io/-/members" }, |
| 293 | { slug: "side", name: "side", to: "/side/-/members" }, | |
| Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page) | 294 | ], |
| 295 | ); | |
| 296 | }); | |
| 297 | ||
| 298 | test("an invite's page names the invite it is", () => { | |
| 299 | const base = { kind: "account" as const, invitedBy: { username: "syntaqx" }, workspace: null, repository: null, hasAccount: false }; | |
| 300 | const g1t = invitePageCopy(base, false); | |
| 301 | assert.equal(`${g1t.before}${g1t.place ?? ""}${g1t.after}`, "@syntaqx invited you to g1t"); | |
| 302 | assert.match(g1t.about, /lets you make an account\. It does not add you to anyone's workspace/); | |
| 303 | const join = invitePageCopy({ ...base, workspace: { name: "Flagon, Inc." } }, false); | |
| 304 | assert.equal(`${join.before}${join.place}${join.after}`, "@syntaqx invited you to join Flagon, Inc. on g1t"); | |
| 305 | assert.equal(join.place, "Flagon, Inc."); | |
| 306 | assert.match(join.about, /invitation to join Flagon, Inc\., which you accept or decline/); | |
| 307 | assert.match(join.about, /You do not have a g1t account yet, so it also lets you make one/); | |
| 308 | // Someone with an account, or signed in, just accepts. | |
| 309 | assert.match(invitePageCopy({ ...base, kind: "workspace", workspace: { name: "Flagon, Inc." }, hasAccount: true }, false).about, /Accepting joins you to Flagon, Inc\./); | |
| 310 | assert.equal(invitePageCopy({ ...base, invitedBy: null }, false).before, "The g1t team invited you to g1t"); | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 311 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.