import { describe, expect, it } from "@suaveplan/testing/runner"; import { assess } from "./assess/assess.js"; import { dictionaryMatch } from "./matchers/dictionary/dictionary.js"; import type { Match } from "./matchers/types.js"; import { estimateGuesses } from "./scoring/estimate-guesses.js"; /** * Wheeler 2016 corpus agreement tests. We do not vendor the upstream test * vector JSON (GPLv2 half of the dual licence); instead we assemble a curated * set of canonical inputs and verify the per-pattern guess estimates agree * within ≤1 bit log10 against the closed-form formulae documented in * Wheeler §4. */ describe("Wheeler corpus agrees to <=1 bit log10", () => { const dicts = { passwords: new Map([["password", 1]]) }; const cases: { match: Match; password: string; expectedLog10: number }[] = [ { match: { pattern: "dictionary", i: 0, j: 7, token: "password", matchedWord: "password", rank: 1, dictionaryName: "passwords", reversed: false, l33t: false, }, password: "password", expectedLog10: 0, // log10(1) = 0 }, { match: { pattern: "sequence", i: 0, j: 3, token: "abcd", sequenceName: "lower", sequenceSpace: 26, ascending: true, }, password: "abcd", // 4 (base) * 4 (length) * 1 = 16 → log10 ≈ 1.20 expectedLog10: 1.2, }, { match: { pattern: "date", i: 0, j: 9, token: "11/22/1985", year: 1985, month: 11, day: 22, separator: "/", }, password: "11/22/1985", // 365 * 41 * 4 = 59860 → log10 ≈ 4.78 expectedLog10: 4.78, }, { match: { pattern: "regex", i: 0, j: 3, token: "1985", regexName: "recent_year", regexMatch: ["1985"] as unknown as RegExpMatchArray, }, password: "1985", // |1985 - 2026| = 41 → log10 ≈ 1.61 expectedLog10: 1.61, }, ]; for (const c of cases) { it(`${c.match.pattern} / ${c.match.token}`, { timeout: 5000 }, () => { const g = estimateGuesses(c.match, c.password, { referenceYear: 2026, }); const log10 = Math.log10(g); expect(Math.abs(log10 - c.expectedLog10)).toBeLessThanOrEqual(1); }); } it("full assess agrees with corpus reference within bucket", { timeout: 5000, }, () => { const r = assess("password", { dictionaries: { passwords: dicts.passwords }, }); expect(r.score).toBe(0); const matches = dictionaryMatch("password", dicts); expect(matches.length).toBeGreaterThanOrEqual(1); }); });