|
31 | 31 | * mechanism `mail-manifest-providers.contract.test.ts` already uses for the |
32 | 32 | * provider dropdown over that same devDependency. |
33 | 33 | * |
| 34 | + * ## #13189 — the accept set narrowed, and this file is where that is visible |
| 35 | + * |
| 36 | + * `isValidSmtpPort` now tests INTEGRALITY. That is a deliberate narrowing of |
| 37 | + * the set #12993 pinned, and the pin below was written to make exactly this |
| 38 | + * kind of change loud rather than to forbid it — so it was UPDATED, never |
| 39 | + * deleted: the legacy oracle stays, an exhaustive integer sweep says the |
| 40 | + * integer accept set did not move at all, and the one axis that did move is |
| 41 | + * named value by value. The sentence moved with the guard (`expected an |
| 42 | + * integer 1-65535`), because `587.5` is inside `1-65535` and a door that |
| 43 | + * refuses it while saying only that is stating a rule it does not enforce. |
| 44 | + * |
34 | 45 | * ## ⛔ The floor is 1 and must never become 0 |
35 | 46 | * |
36 | 47 | * The CLI's listen range floors at 0 ("let the OS choose"). This one floors at |
@@ -181,7 +192,7 @@ describe('#12993 — one SMTP port range, every door states it from there', () = |
181 | 192 | // independent spelling, so there is nothing left to drift. |
182 | 193 | expect(SMTP_PORT_RANGE_TEXT).toBe(`${SMTP_PORT_MIN}-${SMTP_PORT_MAX}`); |
183 | 194 | expect(formatInvalidSmtpPortNotice('abc')) |
184 | | - .toBe(`SmtpTransport: invalid port 'abc' (expected ${SMTP_PORT_MIN}-${SMTP_PORT_MAX})`); |
| 195 | + .toBe(`SmtpTransport: invalid port 'abc' (expected an integer ${SMTP_PORT_MIN}-${SMTP_PORT_MAX})`); |
185 | 196 |
|
186 | 197 | // The ceiling is typed exactly once even inside its own module — if the |
187 | 198 | // notice re-spelled the range, this would be 2. |
@@ -211,27 +222,146 @@ describe('#12993 — one SMTP port range, every door states it from there', () = |
211 | 222 | .toBe(true); |
212 | 223 | }); |
213 | 224 |
|
214 | | - it('refactors the enforcement without narrowing what it accepts', () => { |
215 | | - // The predicate `smtp.ts` had before this card, kept verbatim as the |
216 | | - // oracle. Reading the bound from the module here would assert `x === x`; |
217 | | - // the point is that the OLD expression and the NEW function agree. |
| 225 | + it('narrows the accept set in exactly ONE dimension — integrality — and nowhere else (#13189)', () => { |
| 226 | + // ⚠️ This case was `refactors the enforcement without narrowing what it |
| 227 | + // accepts` when #12993 moved the predicate here, and `587.5` sat in its |
| 228 | + // table as MEASURED, not endorsed. #13189 is the card that SPENDS that |
| 229 | + // pin: the accept set really does narrow now, and the pin's job was always |
| 230 | + // to make such a change visible rather than to prevent one. So the oracle |
| 231 | + // and the values stay exactly where they were; what changed is that the |
| 232 | + // two are now expected to disagree on ONE axis, asserted term by term so |
| 233 | + // that a second narrowing — or any widening — still fails right here. |
218 | 234 | const legacyAccepts = (port: number): boolean => |
219 | 235 | !(!Number.isFinite(port) || port < 1 || port > 65535); |
220 | 236 |
|
221 | | - const table = [ |
| 237 | + const integers = [ |
222 | 238 | 1, 25, 465, 587, 2525, 65535, // inside |
223 | 239 | 0, -1, 65536, 99999, // outside |
| 240 | + ]; |
| 241 | + const nonIntegers = [ |
| 242 | + 587.5, 1.5, 2525.25, 65534.5, // INSIDE the range — accepted until this card |
| 243 | + 0.5, 65535.5, -0.5, 65536.5, // outside it — the old bounds refused these too |
224 | 244 | Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, // not finite |
225 | | - 587.5, 0.5, 65535.5, // non-integers: accepted iff they were accepted before |
226 | 245 | ]; |
227 | | - for (const port of table) { |
| 246 | + |
| 247 | + // ── UNCHANGED: on every integer the predicate is still, bound for bound, |
| 248 | + // the expression `smtp.ts` carried before #12993. |
| 249 | + for (const port of integers) { |
228 | 250 | expect(isValidSmtpPort(port), `accept set changed for ${String(port)}`) |
229 | 251 | .toBe(legacyAccepts(port)); |
230 | 252 | } |
231 | 253 |
|
232 | | - // The table is not vacuous in either direction. |
233 | | - expect(table.filter(legacyAccepts).length).toBeGreaterThan(0); |
234 | | - expect(table.filter((p) => !legacyAccepts(p)).length).toBeGreaterThan(0); |
| 254 | + // ⭐ …and the strongest available form of "and nowhere else": EVERY |
| 255 | + // integer from below the floor to above the ceiling, not ten sampled |
| 256 | + // ones. 65k comparisons of two cheap predicates costs milliseconds and |
| 257 | + // closes the gap a table cannot. |
| 258 | + let divergences = 0; |
| 259 | + for (let port = -2; port <= SMTP_PORT_MAX + 2; port += 1) { |
| 260 | + if (isValidSmtpPort(port) !== legacyAccepts(port)) divergences += 1; |
| 261 | + } |
| 262 | + expect(divergences, 'the integer accept set moved somewhere the table does not sample') |
| 263 | + .toBe(0); |
| 264 | + |
| 265 | + // Control for that zero — the same sweep against a floor deliberately one |
| 266 | + // too high, which MUST find the one integer it disagrees about. Without |
| 267 | + // this the loop above could be counting nothing at all. |
| 268 | + let seen = 0; |
| 269 | + for (let port = -2; port <= SMTP_PORT_MAX + 2; port += 1) { |
| 270 | + if ((port >= 2 && port <= SMTP_PORT_MAX) !== legacyAccepts(port)) seen += 1; |
| 271 | + } |
| 272 | + expect(seen, 'the integer sweep is a dead loop').toBe(1); |
| 273 | + |
| 274 | + // ── THE ONE CHANGE: nothing non-integral is accepted any more. |
| 275 | + for (const port of nonIntegers) { |
| 276 | + expect(isValidSmtpPort(port), `${String(port)} is still accepted`).toBe(false); |
| 277 | + } |
| 278 | + |
| 279 | + // …and exactly WHICH values this card moved, spelled out rather than |
| 280 | + // summarised: accepted yesterday, refused today, and not one of them could |
| 281 | + // ever have completed a connection. |
| 282 | + // |
| 283 | + // ⚠️ MEASURED, and it corrected a first draft of this very list: a |
| 284 | + // fraction only moved if it was INSIDE the range, so `0.5` and `65535.5` |
| 285 | + // belong in the half below, not here. `0.5 < SMTP_PORT_MIN` and |
| 286 | + // `65535.5 > SMTP_PORT_MAX`, so the old bounds already refused both — and |
| 287 | + // listing them as "narrowed by this card" would have overstated the |
| 288 | + // change while still passing a weaker assertion. |
| 289 | + const moved = [587.5, 1.5, 2525.25, 65534.5]; |
| 290 | + expect(moved.filter(legacyAccepts), 'these were not accepted before, so nothing moved') |
| 291 | + .toEqual(moved); |
| 292 | + expect(moved.filter((port) => isValidSmtpPort(port)), 'a fractional port is accepted again') |
| 293 | + .toEqual([]); |
| 294 | + |
| 295 | + // The other half, asserted rather than left implied: every remaining |
| 296 | + // non-integer was ALREADY refused, so this card narrowed the in-range |
| 297 | + // fractions and nothing else. |
| 298 | + const unmoved = nonIntegers.filter((port) => !moved.includes(port)); |
| 299 | + expect(unmoved.filter(legacyAccepts), 'this card narrowed more than the in-range fractions') |
| 300 | + .toEqual([]); |
| 301 | + expect(unmoved.length, 'the unmoved half is empty — it asserts nothing').toBeGreaterThan(0); |
| 302 | + |
| 303 | + // The tables are not vacuous in either direction. |
| 304 | + expect(integers.filter(legacyAccepts).length).toBeGreaterThan(0); |
| 305 | + expect(integers.filter((p) => !legacyAccepts(p)).length).toBeGreaterThan(0); |
| 306 | + expect(nonIntegers.filter(legacyAccepts).length).toBeGreaterThan(0); |
| 307 | + }); |
| 308 | + |
| 309 | + it('⭐ states the rule it enforces — the sentence and the guard cannot disagree (#13189)', () => { |
| 310 | + // The defect this card repairs, in one line: `587.5` **is** inside |
| 311 | + // `1-65535`, so a refusal reading `(expected 1-65535)` described a door |
| 312 | + // that had just let it through. The sentence is the operator's only view |
| 313 | + // of the rule, so a guard that tests integrality without saying so would |
| 314 | + // have moved the lie rather than removed it. |
| 315 | + const notice = formatInvalidSmtpPortNotice(587.5); |
| 316 | + expect(notice, 'the refusal states a range the guard no longer enforces alone') |
| 317 | + .toContain('integer'); |
| 318 | + |
| 319 | + // ⭐ The mechanical form, and the reason this is not a `toContain` on a |
| 320 | + // word: read the range back OUT of the rendered sentence and confirm that |
| 321 | + // a value satisfying it is refused anyway — which is precisely why the |
| 322 | + // range can no longer be the whole sentence. |
| 323 | + const rendered = notice.match(/\((?:[^()]*?)(\d+)-(\d+)\)/); |
| 324 | + expect(rendered, 'the refusal no longer renders the range at all').not.toBeNull(); |
| 325 | + const low = Number(rendered![1]); |
| 326 | + const high = Number(rendered![2]); |
| 327 | + expect(low, 'the rendered floor drifted from the constant').toBe(SMTP_PORT_MIN); |
| 328 | + expect(high, 'the rendered ceiling drifted from the constant').toBe(SMTP_PORT_MAX); |
| 329 | + expect(587.5 >= low && 587.5 <= high, 'the example stopped being inside the stated range') |
| 330 | + .toBe(true); |
| 331 | + expect(isValidSmtpPort(587.5), '587.5 is accepted again').toBe(false); |
| 332 | + |
| 333 | + // ⛔ The range is still GENERATED, never re-typed — the word added above |
| 334 | + // is prose about the predicate and must not have dragged a literal in |
| 335 | + // with it. (`SMTP_PORT_RANGE_TEXT` is asserted against the constants two |
| 336 | + // cases up; this holds the notice to that same construct.) |
| 337 | + expect(notice).toContain(SMTP_PORT_RANGE_TEXT); |
| 338 | + }); |
| 339 | + |
| 340 | + it('refuses a fractional port AT CONSTRUCTION, under its own name (#13189)', () => { |
| 341 | + // BEFORE, MEASURED on `origin/main@56c5b1dbe` through the built |
| 342 | + // `dist/index.js`: construction ACCEPTED `587.5`, `describe().port` read |
| 343 | + // it straight back, and the operator's first sight of the problem came at |
| 344 | + // SEND time as a bare `RangeError` — `code: 'ECONNECTION'` once nodemailer |
| 345 | + // has re-coded `ERR_SOCKET_BAD_PORT` — reading `Port should be >= 0 and < |
| 346 | + // 65536. Received type number (587.5).`, which names a TCP rule and no |
| 347 | + // part of the Settings field the operator typed in. |
| 348 | + expect(() => new SmtpTransport({ host: 'smtp.example.test', port: 587.5 })) |
| 349 | + .toThrow(formatInvalidSmtpPortNotice(587.5)); |
| 350 | + |
| 351 | + // The refusal carries the operator's OWN value. Asserted through the |
| 352 | + // contract's generator above and then, separately, on the spelling — a |
| 353 | + // bare `.toThrow()` here would also pass on the `host is required` |
| 354 | + // refusal that guards the line before it. |
| 355 | + expect(() => new SmtpTransport({ host: 'smtp.example.test', port: 587.5 })) |
| 356 | + .toThrow(/invalid port '587\.5'/); |
| 357 | + |
| 358 | + // ⛔ The fence on the repair: integer ports still construct, at both |
| 359 | + // bounds. A guard that refused `587.5` by refusing everything would |
| 360 | + // satisfy every line above this one. |
| 361 | + for (const port of [SMTP_PORT_MIN, 25, 465, 587, SMTP_PORT_MAX]) { |
| 362 | + expect(new SmtpTransport({ host: 'smtp.example.test', port }).describe().port) |
| 363 | + .toBe(port); |
| 364 | + } |
235 | 365 | }); |
236 | 366 |
|
237 | 367 | it('⛔ floors at 1, not at 0 — this range is not the CLI listen range', () => { |
|
0 commit comments