From bafe70657ef5f2b9614ecabdd9d7d8c374cd3648 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 1 Aug 2026 04:01:19 +0000 Subject: [PATCH 1/2] pit: read names as well as endings from the paste field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The paste field took `eggs` and `.eggs` and refused everything else, so `blue.eggs` came back as "not a valid TLD — letters, digits and dashes only, no dots". That reads as a complaint about the paste, but the paste was fine: a name under an ending is a thing this registry sells, and the field simply had no way to say one. All four spellings now parse — `foo`, `bar.foo`, `.whatever` and `.foo.whatever` — and each entry records which it was rather than being flattened to an ending. A pasted name claims the ending it needs first, in the same batch, because the alternative is refusing the line and asking the operator to paste the halves in the right order to reach the same place. An ending claimed on a name's behalf is reported like any other claim. Quietly acquiring something nobody read themselves asking for is the one outcome here worth being loud about. Names that cannot land now say why. A name under someone else's ending reports "you do not own .agent" — the fact that sends you somewhere useful — instead of the dot complaint, which sent you off to fix a paste that never had anything wrong with it. The vendored parser is ahead of @moshcoder/moshpit-name for the moment. The drift test holds both copies to the rules they share and records the gap explicitly; it goes back to a straight comparison once the package ships name parsing. Co-Authored-By: Claude Opus 5 (1M context) --- apps/pwa/src/lib/moshpit-name.mjs | 47 ++++++++++-- apps/pwa/src/moshpit.mjs | 82 +++++++++++++++++++- apps/pwa/src/routes/moshpit.mjs | 11 ++- apps/pwa/test/moshpit-bulk-claim.test.mjs | 91 +++++++++++++++++++++-- apps/pwa/test/moshpit-name-drift.test.mjs | 25 ++++++- 5 files changed, 237 insertions(+), 19 deletions(-) diff --git a/apps/pwa/src/lib/moshpit-name.mjs b/apps/pwa/src/lib/moshpit-name.mjs index d044e50..8395773 100644 --- a/apps/pwa/src/lib/moshpit-name.mjs +++ b/apps/pwa/src/lib/moshpit-name.mjs @@ -224,6 +224,14 @@ export const DEFAULT_TLD_PRICE_USD = MAX_CHILD_PRICE_USD; * Deduplicated on the normalised form, so `.Eggs`, `eggs` and `EGGS` in one * paste are one claim rather than one claim and two "already taken" errors * against yourself. + * + * Both halves of the namespace come through the same field. `eggs` and `.eggs` + * are the ending; `blue.eggs` and `.blue.eggs` are a name under it. The leading + * dot is decoration in either case — people type the shape they saw written + * down, and refusing one spelling of the thing they already own would be a + * distinction without a difference. Which one a line meant is recorded on the + * entry rather than resolved here, because parsing is not the place that knows + * who holds `.eggs`. */ export function parseTldList(input, limit = MAX_BULK_TLDS) { // Records split on newlines, commas and semicolons; fields inside a record @@ -243,9 +251,13 @@ export function parseTldList(input, limit = MAX_BULK_TLDS) { for (const record of records) { const fields = record.split(/\s+/).filter(Boolean); - const tld = normalizeToken(fields[0]); - if (!tld || seen.has(tld)) continue; - seen.add(tld); + const target = parseListToken(fields[0]); + if (!target) continue; + // Keyed on the whole thing, so `.eggs` and `blue.eggs` in one paste are two + // entries rather than one swallowing the other. + const key = target.label ? `${target.label}.${target.tld}` : target.tld; + if (seen.has(key)) continue; + seen.add(key); // Counted rather than silently dropped: "I pasted 300 and got 200" needs to // be visible, or the missing hundred look like they failed for some other @@ -262,12 +274,35 @@ export function parseTldList(input, limit = MAX_BULK_TLDS) { if (price !== null) priceUsd = price; else aliasOf = normalizeToken(field); } - entries.push({ tld, aliasOf, priceUsd }); + entries.push({ tld: target.tld, label: target.label, aliasOf, priceUsd }); } // `tlds` alongside `entries` because most callers only want the names, and - // making every one of them map over the records would be noise. - return { entries, tlds: entries.map((e) => e.tld), skipped }; + // making every one of them map over the records would be noise. `names` is + // the same courtesy for the other half, and the two are disjoint: an entry is + // an ending or a name under one, never both. + return { + entries, + tlds: entries.filter((e) => !e.label).map((e) => e.tld), + names: entries.filter((e) => e.label).map((e) => ({ tld: e.tld, label: e.label })), + skipped, + }; +} + +/** + * One pasted token, as the ending or the name it was written as. + * + * A token that reads as neither comes back as an ending carrying its own bad + * text — `a.b.c` is not silently dropped, it is handed on so the caller can + * reject it by name and say why. Losing it here would turn "you pasted + * something wrong" into a line that never appears in the report at all. + */ +function parseListToken(value) { + const raw = normalizeToken(value); + if (!raw) return null; + if (!raw.includes(".")) return { tld: raw, label: null }; + const name = parseMoshpitName(raw); + return name ? { tld: name.tld, label: name.label } : { tld: raw, label: null }; } function normalizeToken(value) { diff --git a/apps/pwa/src/moshpit.mjs b/apps/pwa/src/moshpit.mjs index 5a728f0..3da438a 100644 --- a/apps/pwa/src/moshpit.mjs +++ b/apps/pwa/src/moshpit.mjs @@ -703,19 +703,51 @@ export async function registerTlds({ const taken = []; const rejected = []; const settingsFailed = []; + const names = []; + const namesMine = []; + const namesTaken = []; + const namesRejected = []; // Validation first, in memory. A reserved or malformed ending never needs a // round trip to be refused, and filtering here keeps the batches below to // things that can actually land. const candidates = []; + const nameCandidates = []; for (const entry of entries) { const tld = normalizeTld(entry.tld); if (!tld) { rejected.push({ tld: entry.tld, error: "not a valid TLD — letters, digits and dashes only, no dots" }); continue; } const why = tldRejection(tld); if (why) { rejected.push({ tld, error: why }); continue; } + + if (entry.label) { + const label = normalizeLabel(entry.label); + if (!label) { + namesRejected.push({ tld: `${entry.label}.${tld}`, error: "not a valid name — letters, digits and dashes only" }); + continue; + } + nameCandidates.push({ tld, label }); + continue; + } + candidates.push({ ...entry, tld }); } + // A name has nowhere to live until its ending exists, so the endings the + // pasted names imply are claimed alongside the ones pasted outright. Pasting + // `blue.eggs` when you hold nothing means you wanted `.eggs` too — the + // alternative is refusing the line and making the operator paste the halves + // in two passes, in the right order, to get the same result. + // + // Implied or explicit, a claim is a claim: these land in `claimed` and are + // reported like any other, because quietly acquiring an ending someone did + // not read themselves asking for is the one outcome worth being loud about. + const wanted = new Set(candidates.map((c) => c.tld)); + for (const { tld } of nameCandidates) { + if (wanted.has(tld)) continue; + wanted.add(tld); + candidates.push({ tld, aliasOf: null, priceUsd: null }); + } + const at = Date.now(); for (const chunk of chunksOf(candidates, chunkSize)) { @@ -775,7 +807,28 @@ export async function registerTlds({ if (updates.length || logs.length) await db.batch([...updates, ...logs], "write"); } - return { claimed, mine, taken, rejected, settingsFailed, skipped, remaining: [], attempted: entries.length }; + // Names last, once every ending above has settled. `registerName` re-checks + // ownership rather than trusting the loop: an ending in this batch may have + // gone to someone else a moment ago, and "you do not own .agent" is the + // honest report for a name under it — not the "no dots allowed" that the + // parser used to give, which described the paste rather than the problem. + for (const { tld, label } of nameCandidates) { + const full = `${label}.${tld}`; + const result = await registerName({ tld, label, userId }); + if (result.ok) { names.push(full); continue; } + if (result.taken) { + const existing = await getName(tld, label); + (existing && existing.user_id === userId ? namesMine : namesTaken).push(full); + continue; + } + namesRejected.push({ tld: full, error: result.error }); + } + + return { + claimed, mine, taken, rejected, settingsFailed, + names, namesMine, namesTaken, namesRejected, + skipped, remaining: [], attempted: entries.length, + }; } function* chunksOf(list, size) { @@ -798,14 +851,35 @@ export function summarizeBulkClaim(result, limit = MAX_BULK_TLDS) { // The single-ending case says the plain thing. "claimed 1 — .eggs." is what a // batch report looks like, and the commonest path through this page is one // ending typed into one box. - const onlyClaimed = result.claimed.length === 1 && !result.mine.length && !result.taken.length - && !result.rejected.length && !result.settingsFailed?.length && !result.skipped - && !result.remaining?.length; + // A name is shown as written — `blue.eggs`, not `.blue.eggs` — because the + // leading dot belongs to an ending and putting one on a name would spell it + // as something you cannot claim. + const showNames = (list, n = 6) => + list.slice(0, n).join(", ") + (list.length > n ? ` +${list.length - n} more` : ""); + + const nameCount = result.names?.length || 0; + const quiet = !result.mine.length && !result.taken.length && !result.rejected.length + && !result.settingsFailed?.length && !result.skipped && !result.remaining?.length + && !result.namesMine?.length && !result.namesTaken?.length && !result.namesRejected?.length; + + const onlyClaimed = quiet && result.claimed.length === 1 && !nameCount; if (onlyClaimed) return `.${result.claimed[0]} is yours.`; + // The same plain sentence for the other half. Claiming `.eggs` on the way to + // `blue.eggs` is one intention, so it reads as one result rather than as a + // claim report with a name bolted to it. + if (quiet && nameCount === 1 && result.claimed.length <= 1) return `${result.names[0]} is yours.`; + const parts = []; if (result.claimed.length) parts.push(`claimed ${result.claimed.length} — ${show(result.claimed)}`); + if (nameCount) parts.push(`registered ${nameCount} — ${showNames(result.names)}`); if (result.mine.length) parts.push(`${result.mine.length} already yours`); + if (result.namesMine?.length) parts.push(`${result.namesMine.length} name${result.namesMine.length > 1 ? "s" : ""} already yours`); + if (result.namesTaken?.length) parts.push(`${result.namesTaken.length} name${result.namesTaken.length > 1 ? "s" : ""} taken by someone else (${showNames(result.namesTaken)})`); + if (result.namesRejected?.length) { + const reasons = result.namesRejected.slice(0, 3).map((r) => `${r.tld} — ${r.error}`).join("; "); + parts.push(`${result.namesRejected.length} name${result.namesRejected.length > 1 ? "s" : ""} rejected (${reasons}${result.namesRejected.length > 3 ? "; …" : ""})`); + } if (result.taken.length) parts.push(`${result.taken.length} taken by someone else (${show(result.taken)})`); if (result.rejected.length) { // The reason matters more than the count here: "reserved" and "too short" diff --git a/apps/pwa/src/routes/moshpit.mjs b/apps/pwa/src/routes/moshpit.mjs index 5baf93f..8b2a420 100644 --- a/apps/pwa/src/routes/moshpit.mjs +++ b/apps/pwa/src/routes/moshpit.mjs @@ -1440,6 +1440,10 @@ const back = (res, params, tab = "yours") => * Reported as `ok` when anything landed at all, even alongside collisions — * a list where 38 of 40 were claimed succeeded, and colouring it as an error * because two were taken would misread the normal case as a failure. + * + * A name counts as landing. Pasting `blue.eggs` under an ending you already + * hold claims no ending at all, and judging that paste by the ending count + * alone would flash red over a registration that worked. */ moshpitRouter.post("/pit/claim-bulk", requireAuth, async (req, res) => { const result = await registerTlds({ @@ -1449,7 +1453,8 @@ moshpitRouter.post("/pit/claim-bulk", requireAuth, async (req, res) => { // The flash rides back in the query string, so it has to stay short enough // to survive a URL. const summary = summarizeBulkClaim(result).slice(0, 500); - return back(res, result.claimed.length ? { ok: summary } : { err: summary }); + const landed = result.claimed.length || result.names.length; + return back(res, landed ? { ok: summary } : { err: summary }); }); moshpitRouter.post("/pit/claim", requireAuth, async (req, res) => { @@ -1459,7 +1464,9 @@ moshpitRouter.post("/pit/claim", requireAuth, async (req, res) => { input: req.body?.tld, userId: req.user.id, ownerEmail: req.user.email ?? null, priceUsd: req.body?.price_usd, aliasOf: req.body?.alias_of, }); - if (!result.claimed.length) return back(res, { err: summarizeBulkClaim(result).slice(0, 500) }); + if (!result.claimed.length && !result.names.length) { + return back(res, { err: summarizeBulkClaim(result).slice(0, 500) }); + } back(res, { ok: summarizeBulkClaim(result).slice(0, 500) }); }); diff --git a/apps/pwa/test/moshpit-bulk-claim.test.mjs b/apps/pwa/test/moshpit-bulk-claim.test.mjs index 5acf799..86c4c85 100644 --- a/apps/pwa/test/moshpit-bulk-claim.test.mjs +++ b/apps/pwa/test/moshpit-bulk-claim.test.mjs @@ -41,7 +41,7 @@ test("parsing a pasted list", async (t) => { // and is now one ending pointed at another. Commas and newlines are the // separators, which is what the placeholder shows. assert.deepEqual(parseTldList("oranges\t\tmosh").entries, - [{ tld: "oranges", aliasOf: "mosh", priceUsd: null }]); + [{ tld: "oranges", label: null, aliasOf: "mosh", priceUsd: null }]); }); await t.test("# comments to end of line are dropped", () => { @@ -53,6 +53,42 @@ test("parsing a pasted list", async (t) => { assert.deepEqual(parseTldList(".Eggs\nEGGS\neggs").tlds, ["eggs"]); }); + await t.test("all four shapes of a pasted line are understood", () => { + // `foo`, `bar.foo`, `.whatever` and `.foo.whatever` — the ending and the + // name under it, each with and without the decorative leading dot. Anything + // with a dot used to be refused outright as "not a valid TLD", which + // described the field rather than the mistake. + const { tlds, names } = parseTldList("foo\nbar.foo\n.whatever\n.foo.whatever"); + assert.deepEqual(tlds, ["foo", "whatever"]); + assert.deepEqual(names, [ + { tld: "foo", label: "bar" }, + { tld: "whatever", label: "foo" }, + ]); + }); + + await t.test("an ending and a name under it are two entries, not one", () => { + // Deduplication keys on the whole thing. `.eggs` and `blue.eggs` share a + // TLD and nothing else, and collapsing them would drop whichever came second. + const { tlds, names } = parseTldList(".eggs\nblue.eggs\neggs\nblue.eggs"); + assert.deepEqual(tlds, ["eggs"]); + assert.deepEqual(names, [{ tld: "eggs", label: "blue" }]); + }); + + await t.test("a name carries per-line settings the same way an ending does", () => { + assert.deepEqual(parseTldList("blue.eggs $5 mosh").entries, + [{ tld: "eggs", label: "blue", aliasOf: "mosh", priceUsd: 5 }]); + }); + + await t.test("something that is neither is kept, to be rejected by name", () => { + // `a.b.c` is not a name and not an ending. Dropping it in the parser would + // leave it out of the report entirely, so it survives as its own bad text + // for registerTlds to refuse and say why. + const { entries, tlds, names } = parseTldList("a.b.c"); + assert.deepEqual(names, []); + assert.deepEqual(tlds, ["a.b.c"]); + assert.equal(entries[0].label, null); + }); + await t.test("blank input yields nothing rather than a phantom entry", () => { for (const input of ["", " \n\n ", null, undefined, "# only a comment"]) { assert.deepEqual(parseTldList(input).tlds, [], `for ${JSON.stringify(input)}`); @@ -117,13 +153,56 @@ test("claiming a pasted list", { skip: installed ? false : "pwa dependencies not await t.test("reserved and malformed endings are rejected with their reason", async () => { const good = uniq(); - const result = await m.registerTlds({ input: `bank\na\n${good}\nfoo.bar`, userId: ALICE }); + const result = await m.registerTlds({ input: `bank\na\n${good}\na.b.c`, userId: ALICE }); assert.deepEqual(result.claimed, [good], "one bad entry must not sink the list"); const rejected = Object.fromEntries(result.rejected.map((r) => [r.tld, r.error])); assert.match(rejected.bank, /reserved/); assert.match(rejected.a, /at least 2/); - assert.ok("foo.bar" in rejected, "a domain is not an ending"); + // Two labels is a name and one is an ending; three is neither, and it is + // refused by name rather than dropped out of the report. + assert.ok("a.b.c" in rejected, "three labels is neither an ending nor a name"); + }); + + await t.test("a pasted name claims the ending it needs, then registers under it", async () => { + const ending = uniq(); + const result = await m.registerTlds({ input: `blue.${ending}`, userId: ALICE }); + + assert.deepEqual(result.claimed, [ending], "the ending a name needs is claimed for you"); + assert.deepEqual(result.names, [`blue.${ending}`]); + assert.equal(result.rejected.length, 0); + assert.equal((await m.getName(ending, "blue")).user_id, ALICE); + }); + + await t.test("the ending is claimed once when both halves are pasted", async () => { + const ending = uniq(); + const result = await m.registerTlds({ input: `.${ending}\nblue.${ending}\ngreen.${ending}`, userId: ALICE }); + + assert.deepEqual(result.claimed, [ending]); + assert.deepEqual(result.names.sort(), [`blue.${ending}`, `green.${ending}`].sort()); + }); + + await t.test("a name under someone else's ending says who owns it, not 'no dots'", async () => { + // The report this replaces said "not a valid TLD — letters, digits and + // dashes only, no dots", which sent you off to fix a paste that was fine. + const theirs = uniq(); + await m.registerTld({ tld: theirs, userId: BOB }); + + const result = await m.registerTlds({ input: `blue.${theirs}`, userId: ALICE }); + assert.deepEqual(result.names, []); + assert.equal(result.namesRejected.length, 1); + assert.match(result.namesRejected[0].error, /do not own/); + assert.equal((await m.getTld(theirs)).user_id, BOB, "not stolen on the way past"); + }); + + await t.test("re-pasting a name you hold reads as already yours", async () => { + const ending = uniq(); + await m.registerTlds({ input: `blue.${ending}`, userId: ALICE }); + + const again = await m.registerTlds({ input: `blue.${ending}`, userId: ALICE }); + assert.deepEqual(again.namesMine, [`blue.${ending}`]); + assert.equal(again.names.length, 0); + assert.equal(again.namesTaken.length, 0); }); await t.test("the summary names what happened", async () => { @@ -266,7 +345,7 @@ test("a line can carry its own price and target", async (t) => { await t.test("reads tld, target and price off one line", () => { assert.deepEqual(parseTldList(".toplevel .redirect $2.00USD").entries, - [{ tld: "toplevel", aliasOf: "redirect", priceUsd: 2 }]); + [{ tld: "toplevel", label: null, aliasOf: "redirect", priceUsd: 2 }]); }); await t.test("accepts the shapes a person actually types", () => { @@ -278,8 +357,8 @@ test("a line can carry its own price and target", async (t) => { }); await t.test("order on the line does not matter", () => { - assert.deepEqual(parseTldList(".a $5 .b").entries, [{ tld: "a", aliasOf: "b", priceUsd: 5 }]); - assert.deepEqual(parseTldList(".a .b $5").entries, [{ tld: "a", aliasOf: "b", priceUsd: 5 }]); + assert.deepEqual(parseTldList(".a $5 .b").entries, [{ tld: "a", label: null, aliasOf: "b", priceUsd: 5 }]); + assert.deepEqual(parseTldList(".a .b $5").entries, [{ tld: "a", label: null, aliasOf: "b", priceUsd: 5 }]); }); await t.test("a bare list still means one ending per entry", () => { diff --git a/apps/pwa/test/moshpit-name-drift.test.mjs b/apps/pwa/test/moshpit-name-drift.test.mjs index aa7082a..7b670e4 100644 --- a/apps/pwa/test/moshpit-name-drift.test.mjs +++ b/apps/pwa/test/moshpit-name-drift.test.mjs @@ -60,9 +60,32 @@ test("vendored namespace rules match the published package", { ".Eggs\nEGGS\neggs", "", ]; + // The vendored copy reads `blue.eggs` as a name under `.eggs`; the published + // package still refuses anything with a dot. That is a capability the copy + // has and the package has not yet released, not a rule they disagree on — so + // the comparison drops the fields that carry it and holds the two to the + // same behaviour on everything an ending-only paste can express. + // + // Delete this shim, and the pastes below, when @moshcoder/moshpit-name ships + // name parsing: at that point the two must agree on names as well, and a + // straight deepEqual is the stronger test. + const sharedRules = (result) => ({ + ...result, + names: undefined, + entries: result.entries.map(({ label, ...rest }) => rest), + }); for (const paste of pastes) { - assert.deepEqual(vendored.parseTldList(paste), published.parseTldList(paste), JSON.stringify(paste)); + assert.deepEqual( + sharedRules(vendored.parseTldList(paste)), + sharedRules(published.parseTldList(paste)), + JSON.stringify(paste), + ); } + + // What the published package cannot do yet, asserted against the copy alone + // so the gap is recorded rather than assumed. + assert.deepEqual(vendored.parseTldList("blue.eggs").names, [{ tld: "eggs", label: "blue" }]); + assert.deepEqual(published.parseTldList("blue.eggs").tlds, ["blue.eggs"]); }); await t.test("resolution precedence agrees across the whole input space", () => { From 95cd284546fe396011ad7a34e5ead72f9a2612a2 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 1 Aug 2026 04:07:45 +0000 Subject: [PATCH 2/2] pit: take moshpit-name 0.2.0 and drop the drift shim The vendored parser was ahead of the published package for one commit, and the drift test held the two to their shared rules while that was true. 0.2.0 ships the same name parsing, so the comparison goes back to what it is for: a straight deepEqual over the whole result, names included. Co-Authored-By: Claude Opus 5 (1M context) --- apps/pwa/package-lock.json | 8 +++--- apps/pwa/package.json | 2 +- apps/pwa/test/moshpit-name-drift.test.mjs | 32 +++++++---------------- 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/apps/pwa/package-lock.json b/apps/pwa/package-lock.json index 8095617..d9b678e 100644 --- a/apps/pwa/package-lock.json +++ b/apps/pwa/package-lock.json @@ -18,7 +18,7 @@ "web-push": "^3.6.7" }, "devDependencies": { - "@moshcoder/moshpit-name": "^0.1.1" + "@moshcoder/moshpit-name": "^0.2.0" }, "engines": { "node": ">=20" @@ -181,9 +181,9 @@ ] }, "node_modules/@moshcoder/moshpit-name": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@moshcoder/moshpit-name/-/moshpit-name-0.1.1.tgz", - "integrity": "sha512-ltZbKeUljHSQSjFeOvxZBT8AWEnRkj+W0QpjBkADSgHp9T4IidK1cpuRAsGRVY+vt7gltSFh2531jhGHhRG5RA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@moshcoder/moshpit-name/-/moshpit-name-0.2.0.tgz", + "integrity": "sha512-FcIv8TkonMfBwVD/gBiQKI34jSK38qtr6rKmjFX2grPtIyNqrKqv15eSpKsgja1/uasUbFC9Y0pck4jbOsBLiw==", "dev": true, "license": "MIT", "bin": { diff --git a/apps/pwa/package.json b/apps/pwa/package.json index 7ad046d..bf46eee 100644 --- a/apps/pwa/package.json +++ b/apps/pwa/package.json @@ -24,6 +24,6 @@ "web-push": "^3.6.7" }, "devDependencies": { - "@moshcoder/moshpit-name": "^0.1.1" + "@moshcoder/moshpit-name": "^0.2.0" } } diff --git a/apps/pwa/test/moshpit-name-drift.test.mjs b/apps/pwa/test/moshpit-name-drift.test.mjs index 7b670e4..a8a50de 100644 --- a/apps/pwa/test/moshpit-name-drift.test.mjs +++ b/apps/pwa/test/moshpit-name-drift.test.mjs @@ -60,32 +60,18 @@ test("vendored namespace rules match the published package", { ".Eggs\nEGGS\neggs", "", ]; - // The vendored copy reads `blue.eggs` as a name under `.eggs`; the published - // package still refuses anything with a dot. That is a capability the copy - // has and the package has not yet released, not a rule they disagree on — so - // the comparison drops the fields that carry it and holds the two to the - // same behaviour on everything an ending-only paste can express. - // - // Delete this shim, and the pastes below, when @moshcoder/moshpit-name ships - // name parsing: at that point the two must agree on names as well, and a - // straight deepEqual is the stronger test. - const sharedRules = (result) => ({ - ...result, - names: undefined, - entries: result.entries.map(({ label, ...rest }) => rest), - }); for (const paste of pastes) { - assert.deepEqual( - sharedRules(vendored.parseTldList(paste)), - sharedRules(published.parseTldList(paste)), - JSON.stringify(paste), - ); + assert.deepEqual(vendored.parseTldList(paste), published.parseTldList(paste), JSON.stringify(paste)); } + }); - // What the published package cannot do yet, asserted against the copy alone - // so the gap is recorded rather than assumed. - assert.deepEqual(vendored.parseTldList("blue.eggs").names, [{ tld: "eggs", label: "blue" }]); - assert.deepEqual(published.parseTldList("blue.eggs").tlds, ["blue.eggs"]); + await t.test("both copies read a name under an ending the same way", () => { + // The half of a paste that is not an ending. This drifting is how + // `blue.eggs` becomes a name in one copy and an unregistrable string in + // the other, which is the bug the two of them were changed to fix. + for (const paste of ["blue.eggs", ".me.whatever\nfoo\nbar.foo", "a.b.c", "1.420", ".eggs\nblue.eggs"]) { + assert.deepEqual(vendored.parseTldList(paste), published.parseTldList(paste), JSON.stringify(paste)); + } }); await t.test("resolution precedence agrees across the whole input space", () => {