Dice: the words of five dice each, in the library and in /create
For whoever does not trust the random numbers of a computer, as dice.go of
datekeys-go at 92e7154, with its texts: five dice for each word give a
number from 11111 to 66666, the position of the word in a list of 7776.
wordlist.ts gains DICE_LIST_SIZE, diceNumber, diceWord, diceWords and
diceList, the list numbered as the EFF publishes its own, and wordkey.ts
goFields, which splits at white space as strings.Fields of Go.
/create offers "Con dados" between the random words and the person's own:
it turns the numbers into words as they are typed, tells a number that is
not five dice or that gives a word again, and offers the list numbered for
dice, to print it, with its SHA-256. planCapsule takes dice and diceList,
and readDice reads the numbers one by one. The list loads once for random
words and dice, and an effect that finds it loaded writes no state, so it
does not run again without end.
testdata and wordlists at 92e7154, whose README of the lists records the
SHA-256 of each list numbered for dice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
// Tests of wordlist.ts: the cases of generate_test.go and dice_test.go of Go
// wordkey, with their texts; the lists of datekeys-go, read with the SHA-256
// that their README records; draws that are uniform and never repeat a word;
// and the words of dice, with the lists numbered for them.
Random words: readWordList, checkWordList, generateWords and wordBits
wordlist.ts does what wordkey.Generate, CheckList and Bits of Go do at
27a75ee, with their texts: generateWords draws different words, 7 by
default, with crypto.getRandomValues; wordBits is their strength;
checkWordList refuses a list of fewer than 2048 words, with two words that
are one once normalized or with a character outside the alphabet of its
language, which the code gives and not the list. readWordList takes a list
only with the SHA-256 pinned for its language, in UTF-8 and accepted by
checkWordList: no list is trusted, not even those of DateKeys.
wordkey.ts gains wordRules, which loads the Unicode tables once for a
caller that reads many words; normalizeWords and checkWords use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
import { readFileSync } from 'node:fs' ;
import { fileURLToPath } from 'node:url' ;
import { describe , expect , it } from 'vitest' ;
import { toHex , utf8Bytes } from './bytes.ts' ;
import type { RandomWords } from './random.ts' ;
Dice: the words of five dice each, in the library and in /create
For whoever does not trust the random numbers of a computer, as dice.go of
datekeys-go at 92e7154, with its texts: five dice for each word give a
number from 11111 to 66666, the position of the word in a list of 7776.
wordlist.ts gains DICE_LIST_SIZE, diceNumber, diceWord, diceWords and
diceList, the list numbered as the EFF publishes its own, and wordkey.ts
goFields, which splits at white space as strings.Fields of Go.
/create offers "Con dados" between the random words and the person's own:
it turns the numbers into words as they are typed, tells a number that is
not five dice or that gives a word again, and offers the list numbered for
dice, to print it, with its SHA-256. planCapsule takes dice and diceList,
and readDice reads the numbers one by one. The list loads once for random
words and dice, and an effect that finds it loaded writes no state, so it
does not run again without end.
testdata and wordlists at 92e7154, whose README of the lists records the
SHA-256 of each list numbered for dice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
import { checkWords , goFields , normalizeWords } from './wordkey.ts' ;
import {
checkWordList ,
DEFAULT_WORD_COUNT ,
DICE_LIST_SIZE ,
diceList ,
diceNumber ,
diceWord ,
diceWords ,
generateWords ,
MIN_LIST_SIZE ,
readWordList ,
WORD_LIST_SHA256 ,
wordBits ,
} from './wordlist.ts' ;
Random words: readWordList, checkWordList, generateWords and wordBits
wordlist.ts does what wordkey.Generate, CheckList and Bits of Go do at
27a75ee, with their texts: generateWords draws different words, 7 by
default, with crypto.getRandomValues; wordBits is their strength;
checkWordList refuses a list of fewer than 2048 words, with two words that
are one once normalized or with a character outside the alphabet of its
language, which the code gives and not the list. readWordList takes a list
only with the SHA-256 pinned for its language, in UTF-8 and accepted by
checkWordList: no list is trusted, not even those of DateKeys.
wordkey.ts gains wordRules, which loads the Unicode tables once for a
caller that reads many words; normalizeWords and checkWords use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
const listFile = ( name : string ) : Uint8Array = > new Uint8Array ( readFileSync ( fileURLToPath ( new URL ( ` ../../../wordlists/ ${ name } ` , import . meta . url ) ) ) ) ;
const cp = ( r : number ) : string = > String . fromCodePoint ( r ) ;
const hash = async ( b : Uint8Array ) : Promise < string > = > toHex ( new Uint8Array ( await crypto . subtle . digest ( 'SHA-256' , b as Uint8Array < ArrayBuffer > ) ) ) ;
// pal followed by three letters: palaaa, palaab, …, as in Go.
const base = Array . from ( { length : MIN_LIST_SIZE } , ( _ , i ) = > ` pal ${ String . fromCharCode ( 97 + Math . floor ( i / 676 ) , 97 + ( Math . floor ( i / 26 ) % 26 ) , 97 + ( i % 26 ) ) } ` ) ;
const withWord = ( i : number , w : string ) : string [ ] = > base . map ( ( x , j ) = > ( j === i ? w : x ) ) ;
const fileOf = ( words : readonly string [ ] ) : Uint8Array = > utf8Bytes ( ` ${ words . join ( '\n' ) } \ n ` ) ;
// A source of 32-bit words that repeats `seed`.
function repeating ( seed : readonly number [ ] ) : RandomWords {
let i = 0 ;
return ( ) = > seed [ i ++ % seed . length ] ! ;
}
describe ( 'readWordList' , ( ) = > {
it ( 'reads the lists en and es of datekeys-go, with the SHA-256 that its README records' , async ( ) = > {
Random words: readWordList, checkWordList, generateWords and wordBits
wordlist.ts does what wordkey.Generate, CheckList and Bits of Go do at
27a75ee, with their texts: generateWords draws different words, 7 by
default, with crypto.getRandomValues; wordBits is their strength;
checkWordList refuses a list of fewer than 2048 words, with two words that
are one once normalized or with a character outside the alphabet of its
language, which the code gives and not the list. readWordList takes a list
only with the SHA-256 pinned for its language, in UTF-8 and accepted by
checkWordList: no list is trusted, not even those of DateKeys.
wordkey.ts gains wordRules, which loads the Unicode tables once for a
caller that reads many words; normalizeWords and checkWords use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
const readme = new TextDecoder ( ) . decode ( listFile ( 'README.md' ) ) ;
expect ( Object . keys ( WORD_LIST_SHA256 ) . sort ( ) ) . toEqual ( [ 'en' , 'es' ] ) ;
for ( const lang of [ 'en' , 'es' ] ) expect ( readme ) . toContain ( ` | \` ${ lang } .txt \` | 7776 | \` ${ WORD_LIST_SHA256 [ lang ] } \` | ` ) ;
const es = await readWordList ( 'es' , listFile ( 'es.txt' ) ) ;
expect ( es ) . toHaveLength ( 7776 ) ;
expect ( [ es [ 0 ] , es . at ( - 1 ) ] ) . toEqual ( [ 'abad' , 'útil' ] ) ;
// The list of the EFF in its order: the dice 11111 give its first word
// and 66666 its last.
const en = await readWordList ( 'en' , listFile ( 'en.txt' ) ) ;
expect ( en ) . toHaveLength ( 7776 ) ;
expect ( [ en [ 0 ] , en [ 1 ] , en . at ( - 1 ) ] ) . toEqual ( [ 'abacus' , 'abdomen' , 'zoom' ] ) ;
expect ( en . filter ( ( w ) = > w . includes ( '-' ) ) ) . toEqual ( [ 'drop-down' , 'felt-tip' , 't-shirt' , 'yo-yo' ] ) ;
Random words: readWordList, checkWordList, generateWords and wordBits
wordlist.ts does what wordkey.Generate, CheckList and Bits of Go do at
27a75ee, with their texts: generateWords draws different words, 7 by
default, with crypto.getRandomValues; wordBits is their strength;
checkWordList refuses a list of fewer than 2048 words, with two words that
are one once normalized or with a character outside the alphabet of its
language, which the code gives and not the list. readWordList takes a list
only with the SHA-256 pinned for its language, in UTF-8 and accepted by
checkWordList: no list is trusted, not even those of DateKeys.
wordkey.ts gains wordRules, which loads the Unicode tables once for a
caller that reads many words; normalizeWords and checkWords use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
} ) ;
it ( 'refuses a list of another SHA-256 and a language without a list, with the texts of Go' , async ( ) = > {
const changed = listFile ( 'es.txt' ) ;
changed [ 0 ] ! ^= 1 ;
const refused = ` wordkey: the list "es" has the SHA-256 ${ await hash ( changed ) } , not ${ WORD_LIST_SHA256 [ 'es' ] } ` ;
await expect ( readWordList ( 'es' , changed ) ) . rejects . toThrow ( refused ) ;
await expect ( readWordList ( 'xx' , listFile ( 'es.txt' ) ) ) . rejects . toThrow ( /^wordkey: no word list for "xx"; the lists are en, es$/ ) ;
Random words: readWordList, checkWordList, generateWords and wordBits
wordlist.ts does what wordkey.Generate, CheckList and Bits of Go do at
27a75ee, with their texts: generateWords draws different words, 7 by
default, with crypto.getRandomValues; wordBits is their strength;
checkWordList refuses a list of fewer than 2048 words, with two words that
are one once normalized or with a character outside the alphabet of its
language, which the code gives and not the list. readWordList takes a list
only with the SHA-256 pinned for its language, in UTF-8 and accepted by
checkWordList: no list is trusted, not even those of DateKeys.
wordkey.ts gains wordRules, which loads the Unicode tables once for a
caller that reads many words; normalizeWords and checkWords use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
await expect ( readWordList ( 'es' , fileOf ( base ) , { fr : '00' , de : '11' } ) ) . rejects . toThrow ( /^wordkey: no word list for "es"; the lists are de, fr$/ ) ;
} ) ;
it ( 'refuses a list with its pin that is not UTF-8, or that checkWordList refuses, as Go wordkey.List' , async ( ) = > {
const pin = async ( lang : string , b : Uint8Array ) : Promise < Record < string , string > > = > ( { [ lang ] : await hash ( b ) } ) ;
const ok = fileOf ( base ) ;
await expect ( readWordList ( 'es' , ok , await pin ( 'es' , ok ) ) ) . resolves . toEqual ( base ) ;
// Without the end of its last line, too.
const unended = ok . slice ( 0 , - 1 ) ;
await expect ( readWordList ( 'es' , unended , await pin ( 'es' , unended ) ) ) . resolves . toEqual ( base ) ;
const cyrillic = fileOf ( withWord ( 5 , ` ${ cp ( 0x441 ) } asa ` ) ) ;
const err = ( await readWordList ( 'es' , cyrillic , await pin ( 'es' , cyrillic ) ) . catch ( ( e : unknown ) = > e ) ) as Error ;
expect ( err . message ) . toBe ( ` wordkey: the list "es": line 6, " ${ cp ( 0x441 ) } asa", holds U+0441, which is not in the alphabet of "es" ` ) ;
expect ( ( err . cause as Error ) . message ) . toBe ( ` line 6, " ${ cp ( 0x441 ) } asa", holds U+0441, which is not in the alphabet of "es" ` ) ;
// A BOM stays: it is an invisible character of the first word.
const bom = utf8Bytes ( ` ${ cp ( 0xfeff ) } ${ base . join ( '\n' ) } \ n ` ) ;
await expect ( readWordList ( 'es' , bom , await pin ( 'es' , bom ) ) ) . rejects . toThrow ( /^wordkey: the list "es": line 1: wordkey: the words hold the invisible character U\+FEFF$/ ) ;
const latin1 = fileOf ( base ) ;
latin1 [ 3 ] = 0xe1 ;
await expect ( readWordList ( 'es' , latin1 , await pin ( 'es' , latin1 ) ) ) . rejects . toThrow ( /^wordkey: the list "es" is not UTF-8$/ ) ;
await expect ( readWordList ( 'xx' , ok , await pin ( 'xx' , ok ) ) ) . rejects . toThrow ( /^wordkey: the list "xx": no alphabet for the language "xx"$/ ) ;
} ) ;
} ) ;
describe ( 'checkWordList' , ( ) = > {
it ( 'accepts a list of 2048 different words of the alphabet, and refuses the cases of Go wordkey.TestCheckList' , async ( ) = > {
await expect ( checkWordList ( 'es' , base ) ) . resolves . toBeUndefined ( ) ;
await expect ( checkWordList ( 'xx' , base ) ) . rejects . toThrow ( /^no alphabet for the language "xx"$/ ) ;
const cases : [ readonly string [ ] , string ] [ ] = [
[ base . slice ( 0 , MIN_LIST_SIZE - 1 ) , '2047 words, fewer than 2048' ] ,
[ withWord ( 5 , 'dos palabras' ) , 'line 6, "dos palabras", is not one word' ] ,
[ withWord ( 5 , ' ' ) , 'is not one word' ] ,
[ withWord ( 5 , 'mi' ) , '"mi", is not one word of 3 or more letters' ] ,
[ withWord ( 5 , ` casa ${ cp ( 0x200b ) } ` ) , 'invisible character U+200B' ] ,
// Only the letters of the alphabet of the language, as the list writes
// them: no capitals, no Cyrillic U+0441 that looks like a Latin c, no
// digits, no carriage return of a file with CRLF lines.
[ withWord ( 5 , 'Palaaf' ) , 'line 6, "Palaaf", holds U+0050, which is not in the alphabet of "es"' ] ,
[ withWord ( 5 , ` ${ cp ( 0x441 ) } asa ` ) , 'holds U+0441, which is not in the alphabet' ] ,
[ withWord ( 5 , 'pal1' ) , 'holds U+0031' ] ,
[ withWord ( 5 , ` palaaf ${ cp ( 0x0d ) } ` ) , ` line 6, "palaaf ${ cp ( 0x5c ) } r", holds U+000D ` ] ,
[ withWord ( 5 , base [ 4 ] ! ) , 'line 6, "palaae", is the same word as "palaae"' ] ,
[ withWord ( 5 , 'palaáe' ) , 'line 6, "palaáe", is the same word as "palaae"' ] ,
[ [ . . . withWord ( 0 , 'papá' ) , 'papa' ] , '"papa", is the same word as "papá"' ] ,
] ;
for ( const [ list , want ] of cases ) await expect ( checkWordList ( 'es' , list ) ) . rejects . toThrow ( want ) ;
} ) ;
it ( 'takes the hyphen as a letter of en and not of es, and an accent as one of es and not of en, as Go' , async ( ) = > {
await expect ( checkWordList ( 'en' , withWord ( 5 , 't-shirt' ) ) ) . resolves . toBeUndefined ( ) ;
await expect ( checkWordList ( 'es' , withWord ( 5 , 't-shirt' ) ) ) . rejects . toThrow ( /^line 6, "t-shirt", holds U\+002D, which is not in the alphabet of "es"$/ ) ;
await expect ( checkWordList ( 'en' , withWord ( 5 , 'palaáf' ) ) ) . rejects . toThrow ( /^line 6, "palaáf", holds U\+00E1, which is not in the alphabet of "en"$/ ) ;
} ) ;
Random words: readWordList, checkWordList, generateWords and wordBits
wordlist.ts does what wordkey.Generate, CheckList and Bits of Go do at
27a75ee, with their texts: generateWords draws different words, 7 by
default, with crypto.getRandomValues; wordBits is their strength;
checkWordList refuses a list of fewer than 2048 words, with two words that
are one once normalized or with a character outside the alphabet of its
language, which the code gives and not the list. readWordList takes a list
only with the SHA-256 pinned for its language, in UTF-8 and accepted by
checkWordList: no list is trusted, not even those of DateKeys.
wordkey.ts gains wordRules, which loads the Unicode tables once for a
caller that reads many words; normalizeWords and checkWords use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
} ) ;
describe ( 'generateWords' , ( ) = > {
it ( 'draws different words of the list that make a key, by default 7 with crypto.getRandomValues' , async ( ) = > {
const list = await readWordList ( 'es' , listFile ( 'es.txt' ) ) ;
const words = generateWords ( list ) ;
expect ( words ) . toHaveLength ( DEFAULT_WORD_COUNT ) ;
expect ( new Set ( words ) . size ) . toBe ( DEFAULT_WORD_COUNT ) ;
for ( const w of words ) expect ( list ) . toContain ( w ) ;
await expect ( checkWords ( await normalizeWords ( words . join ( ' ' ) ) ) ) . resolves . toBeUndefined ( ) ;
} ) ;
it ( 'reads nothing but its random words, and draws a word only once' , ( ) = > {
const seed = [ 7 , 1 , 200 , 33 , 0x9e3779b9 , 12345 ] ;
expect ( generateWords ( base , 6 , repeating ( seed ) ) ) . toEqual ( generateWords ( base , 6 , repeating ( seed ) ) ) ;
// Index 0 comes twice: the second is drawn again.
expect ( generateWords ( base , 6 , repeating ( [ 0 , 0 , 1 , 2 , 3 , 4 , 5 ] ) ) ) . toEqual ( base . slice ( 0 , 6 ) ) ;
expect ( ( ) = >
generateWords ( base , 6 , ( ) = > {
throw new Error ( 'no more randomness' ) ;
} ) ,
) . toThrow ( 'no more randomness' ) ;
} ) ;
it ( 'refuses fewer than 6 words and more than half the list, with the texts of Go' , ( ) = > {
expect ( ( ) = > generateWords ( base , 5 ) ) . toThrow ( /^wordkey: a key of words needs at least 6 words, not 5$/ ) ;
expect ( ( ) = > generateWords ( base , 6.5 ) ) . toThrow ( /^wordkey: a key of words needs at least 6 words, not 6\.5$/ ) ;
expect ( ( ) = > generateWords ( base , 1025 ) ) . toThrow ( /^wordkey: 1025 words of a list of 2048$/ ) ;
expect ( generateWords ( base , 1024 ) ) . toHaveLength ( 1024 ) ;
} ) ;
// As TestGenerateUniform of Go: over 7776·40 draws of one word, each index
// falls in its bucket of 64 between 0.8 and 1.2 times the mean.
it ( 'draws every word about as often' , async ( ) = > {
const list = await readWordList ( 'es' , listFile ( 'es.txt' ) ) ;
const index = new Map ( list . map ( ( w , i ) = > [ w , i ] ) ) ;
const buckets = 64 ;
const count = new Array < number > ( buckets ) . fill ( 0 ) ;
let draws = 0 ;
while ( draws < list . length * 40 ) {
for ( const w of generateWords ( list , 6 ) ) {
count [ Math . floor ( ( index . get ( w ) ! * buckets ) / list . length ) ] ! ++ ;
draws ++ ;
}
}
const mean = draws / buckets ;
for ( const c of count ) {
expect ( c ) . toBeGreaterThan ( 0.8 * mean ) ;
expect ( c ) . toBeLessThan ( 1.2 * mean ) ;
}
} ) ;
} ) ;
describe ( 'wordBits' , ( ) = > {
it ( 'is log2 of the draws in order, as Go wordkey.Bits' , ( ) = > {
expect ( wordBits ( 7776 , 1 ) ) . toBe ( Math . log2 ( 7776 ) ) ;
expect ( wordBits ( 7776 , 0 ) ) . toBe ( 0 ) ;
// 7 words of 7776 are a little under 90.5 bits, and 6 of 2048, the
// fewest, a little under 66.
expect ( wordBits ( 7776 , 7 ) ) . toBeCloseTo ( 90.4698 , 4 ) ;
expect ( wordBits ( 7776 , 8 ) ) . toBeCloseTo ( 103.3933 , 4 ) ;
expect ( wordBits ( 2048 , 6 ) ) . toBeCloseTo ( 65.9894 , 4 ) ;
} ) ;
} ) ;
Dice: the words of five dice each, in the library and in /create
For whoever does not trust the random numbers of a computer, as dice.go of
datekeys-go at 92e7154, with its texts: five dice for each word give a
number from 11111 to 66666, the position of the word in a list of 7776.
wordlist.ts gains DICE_LIST_SIZE, diceNumber, diceWord, diceWords and
diceList, the list numbered as the EFF publishes its own, and wordkey.ts
goFields, which splits at white space as strings.Fields of Go.
/create offers "Con dados" between the random words and the person's own:
it turns the numbers into words as they are typed, tells a number that is
not five dice or that gives a word again, and offers the list numbered for
dice, to print it, with its SHA-256. planCapsule takes dice and diceList,
and readDice reads the numbers one by one. The list loads once for random
words and dice, and an effect that finds it loaded writes no state, so it
does not run again without end.
testdata and wordlists at 92e7154, whose README of the lists records the
SHA-256 of each list numbered for dice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
describe ( 'the dice' , ( ) = > {
it ( 'number the positions of a list of 7776 words from 11111 to 66666, as Go wordkey.DiceNumber' , ( ) = > {
const cases : [ number , string ] [ ] = [
[ 0 , '11111' ] ,
[ 1 , '11112' ] ,
[ 5 , '11116' ] ,
[ 6 , '11121' ] ,
[ 35 , '11166' ] ,
[ 36 , '11211' ] ,
[ 1295 , '16666' ] ,
[ 1296 , '21111' ] ,
[ 7775 , '66666' ] ,
] ;
for ( const [ i , want ] of cases ) expect ( diceNumber ( i ) ) . toBe ( want ) ;
for ( const i of [ - 1 , 7776 , 1.5 ] ) expect ( ( ) = > diceNumber ( i ) ) . toThrow ( new RegExp ( ` ^wordkey: no dice give position ${ i } of a list of 7776 words $ ` ) ) ;
expect ( DICE_LIST_SIZE ) . toBe ( 7776 ) ;
} ) ;
it ( 'give the word of their number, as Go wordkey.DiceWord, and refuse what is not five dice' , async ( ) = > {
const en = await readWordList ( 'en' , listFile ( 'en.txt' ) ) ;
const es = await readWordList ( 'es' , listFile ( 'es.txt' ) ) ;
for ( let i = 0 ; i < en . length ; i ++ ) expect ( diceWord ( en , diceNumber ( i ) ) ) . toBe ( en [ i ] ) ;
const cases : [ readonly string [ ] , string , string ] [ ] = [
[ en , '11111' , 'abacus' ] ,
[ en , '11112' , 'abdomen' ] ,
[ en , '35214' , 'jovial' ] ,
[ en , '66666' , 'zoom' ] ,
[ es , '11111' , 'abad' ] ,
[ es , '35214' , 'glaciar' ] ,
[ es , '66666' , 'útil' ] ,
] ;
for ( const [ list , dice , want ] of cases ) expect ( diceWord ( list , dice ) ) . toBe ( want ) ;
for ( const dice of [ '' , '1111' , '111111' , '11110' , '11117' , 'a1111' , '1111 ' , ` ${ cp ( 0xff11 ) } 1111 ` , ` 11111 \ n ` ] ) {
// Go's %q of these is their JSON.
expect ( ( ) = > diceWord ( en , dice ) ) . toThrow ( ` wordkey: ${ JSON . stringify ( dice ) } is not five dice: five digits from 1 to 6 ` ) ;
}
expect ( ( ) = > diceWord ( en . slice ( 0 , 2048 ) , '11111' ) ) . toThrow ( /^wordkey: dice draw from a list of 7776 words, not 2048$/ ) ;
} ) ;
it ( 'give the words of several numbers, as Go wordkey.DiceWords: at least 6, and no word twice' , async ( ) = > {
const en = await readWordList ( 'en' , listFile ( 'en.txt' ) ) ;
const words = diceWords ( en , ` 11111 11112 \ t11113 \ n11114 11115 11116 11121 ` ) ;
expect ( words ) . toEqual ( [ 'abacus' , 'abdomen' , 'abdominal' , 'abide' , 'abiding' , 'ability' , 'ablaze' ] ) ;
await expect ( checkWords ( await normalizeWords ( words . join ( ' ' ) ) ) ) . resolves . toBeUndefined ( ) ;
const cases : [ string , string ] [ ] = [
[ '11111 11112 11113 11114 11115' , 'wordkey: a key of words needs at least 6 words, not 5' ] ,
[ '' , 'wordkey: a key of words needs at least 6 words, not 0' ] ,
[ '11111 11112 11113 11114 11115 1116' , 'wordkey: "1116" is not five dice: five digits from 1 to 6' ] ,
[ '11111 11112 11113 11114 11115 11111' , 'wordkey: the dice 11111 give "abacus" a second time; roll them again' ] ,
[ '11111,11112 11113 11114 11115 11116 11121' , 'wordkey: "11111,11112" is not five dice: five digits from 1 to 6' ] ,
] ;
for ( const [ dice , want ] of cases ) expect ( ( ) = > diceWords ( en , dice ) ) . toThrow ( want ) ;
expect ( ( ) = > diceWords ( en . slice ( 1 ) , '11111 11112 11113 11114 11115 11116' ) ) . toThrow ( /^wordkey: dice draw from a list of 7776 words, not 7775$/ ) ;
} ) ;
it ( 'number a whole list to print it, as Go wordkey.DiceList: for en, the file of the EFF' , async ( ) = > {
const want : Record < string , string > = {
en : 'addd35536511597a02fa0a9ff1e5284677b8883b83e986e43f15a3db996b903e' ,
es : '611f779a33df74587e9dfb486bb0185a9dfd4d1384e75993a21247ad31cefddb' ,
} ;
for ( const lang of [ 'en' , 'es' ] ) {
const list = await readWordList ( lang , listFile ( ` ${ lang } .txt ` ) ) ;
const text = diceList ( list ) ;
expect ( await hash ( utf8Bytes ( text ) ) ) . toBe ( want [ lang ] ) ;
expect ( text . startsWith ( ` 11111 \ t ${ list [ 0 ] } \ n ` ) && text . endsWith ( ` 66666 \ t ${ list [ 7775 ] } \ n ` ) ) . toBe ( true ) ;
}
expect ( ( ) = > diceList ( new Array < string > ( 2048 ) . fill ( 'x' ) ) ) . toThrow ( /^wordkey: dice draw from a list of 7776 words, not 2048$/ ) ;
} ) ;
it ( 'split numbers at white space as Go strings.Fields, without changing them' , ( ) = > {
expect ( goFields ( ` 1 \ u0301 2 \ t3 \ u00a0 4 \ u3000 ` ) ) . toEqual ( [ ` 1 ${ cp ( 0x301 ) } ` , '2' , '3' , '4' ] ) ;
expect ( goFields ( '' ) ) . toEqual ( [ ] ) ;
} ) ;
} ) ;