@@ -2,7 +2,13 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
22import { existsSync , mkdtempSync , readFileSync , rmSync } from "node:fs" ;
33import { tmpdir } from "node:os" ;
44import { join } from "node:path" ;
5- import { getOrCreateSessionAccount , readSessionAccount } from "./session-account.js" ;
5+ import {
6+ getOrCreateSessionAccount ,
7+ persistSessionAccount ,
8+ readSessionAccount ,
9+ } from "./session-account.js" ;
10+
11+ // ── getOrCreateSessionAccount ────────────────────────────────────────────────
612
713describe ( "getOrCreateSessionAccount" , ( ) => {
814 let tmp : string ;
@@ -23,46 +29,132 @@ describe("getOrCreateSessionAccount", () => {
2329 rmSync ( tmp , { recursive : true , force : true } ) ;
2430 } ) ;
2531
26- it ( "generates a new key on first call and persists the mnemonic " , async ( ) => {
32+ it ( "returns created=true and a valid key on first call" , async ( ) => {
2733 const { info, created } = await getOrCreateSessionAccount ( ) ;
2834
2935 expect ( created ) . toBe ( true ) ;
3036 expect ( info . mnemonic . split ( " " ) . length ) . toBeGreaterThanOrEqual ( 12 ) ;
3137 expect ( info . account . ss58Address ) . toMatch ( / ^ [ 1 - 9 A - H J - N P - Z a - k m - z ] + $ / ) ;
3238 expect ( info . account . h160Address ) . toMatch ( / ^ 0 x [ 0 - 9 a - f A - F ] { 40 } $ / ) ;
3339 expect ( typeof info . account . signer . signTx ) . toBe ( "function" ) ;
40+ } ) ;
41+
42+ it ( "does NOT write accounts.json before persistSessionAccount is called" , async ( ) => {
43+ await getOrCreateSessionAccount ( ) ;
3444
3545 const path = join ( tmp , "accounts.json" ) ;
36- expect ( existsSync ( path ) ) . toBe ( true ) ;
37- const stored = JSON . parse ( readFileSync ( path , "utf8" ) ) ;
38- expect ( stored . default ) . toBe ( info . mnemonic ) ;
46+ expect ( existsSync ( path ) ) . toBe ( false ) ;
3947 } ) ;
4048
41- it ( "returns the same key on subsequent calls with created=false" , async ( ) => {
49+ it ( "returns created=false and existing key when file already has a key" , async ( ) => {
50+ // Seed the file via persist on the first creation.
4251 const first = await getOrCreateSessionAccount ( ) ;
52+ await persistSessionAccount ( first . info ) ;
53+
4354 const second = await getOrCreateSessionAccount ( ) ;
4455
45- expect ( first . created ) . toBe ( true ) ;
4656 expect ( second . created ) . toBe ( false ) ;
4757 expect ( second . info . mnemonic ) . toBe ( first . info . mnemonic ) ;
4858 expect ( second . info . account . ss58Address ) . toBe ( first . info . account . ss58Address ) ;
4959 } ) ;
5060
51- it ( "ignores garbage in the store and regenerates a valid key" , async ( ) => {
61+ it ( "returns created=true (new key) on retry when file was never written (map_account failed)" , async ( ) => {
62+ // Simulate: first create, map_account throws → no persist → retry.
63+ const first = await getOrCreateSessionAccount ( ) ;
64+ expect ( first . created ) . toBe ( true ) ;
65+
66+ // map_account failed; we never called persistSessionAccount.
67+ // On retry getOrCreateSessionAccount should mint a fresh key.
68+ const retry = await getOrCreateSessionAccount ( ) ;
69+ expect ( retry . created ) . toBe ( true ) ;
70+ // A fresh mnemonic is generated each time.
71+ expect ( retry . info . mnemonic ) . not . toBe ( first . info . mnemonic ) ;
72+ } ) ;
73+
74+ it ( "ignores garbage in the store and returns a fresh key" , async ( ) => {
5275 const path = join ( tmp , "accounts.json" ) ;
53- await getOrCreateSessionAccount ( ) ;
54- const { writeFileSync } = await import ( "node:fs" ) ;
76+ // Write garbage into the file to simulate corruption.
77+ const { writeFileSync, mkdirSync } = await import ( "node:fs" ) ;
78+ const { dirname } = await import ( "node:path" ) ;
79+ mkdirSync ( dirname ( path ) , { recursive : true } ) ;
5580 writeFileSync ( path , JSON . stringify ( { default : { not : "a string" } } ) ) ;
5681
5782 const { info, created } = await getOrCreateSessionAccount ( ) ;
5883 expect ( created ) . toBe ( true ) ;
5984 expect ( info . mnemonic . split ( " " ) . length ) . toBeGreaterThanOrEqual ( 12 ) ;
6085
86+ // The garbage file is still there; it should not have been overwritten.
6187 const stored = JSON . parse ( readFileSync ( path , "utf8" ) ) ;
62- expect ( typeof stored . default ) . toBe ( "string" ) ;
88+ expect ( typeof stored . default ) . not . toBe ( "string" ) ;
6389 } ) ;
6490} ) ;
6591
92+ // ── persistSessionAccount ────────────────────────────────────────────────────
93+
94+ describe ( "persistSessionAccount" , ( ) => {
95+ let tmp : string ;
96+ let originalRoot : string | undefined ;
97+
98+ beforeEach ( ( ) => {
99+ tmp = mkdtempSync ( join ( tmpdir ( ) , "pg-session-persist-" ) ) ;
100+ originalRoot = process . env . POLKADOT_ROOT ;
101+ process . env . POLKADOT_ROOT = tmp ;
102+ } ) ;
103+
104+ afterEach ( ( ) => {
105+ if ( originalRoot === undefined ) {
106+ delete process . env . POLKADOT_ROOT ;
107+ } else {
108+ process . env . POLKADOT_ROOT = originalRoot ;
109+ }
110+ rmSync ( tmp , { recursive : true , force : true } ) ;
111+ } ) ;
112+
113+ it ( "writes accounts.json with the mnemonic under the 'default' key" , async ( ) => {
114+ const { info } = await getOrCreateSessionAccount ( ) ;
115+ await persistSessionAccount ( info ) ;
116+
117+ const path = join ( tmp , "accounts.json" ) ;
118+ expect ( existsSync ( path ) ) . toBe ( true ) ;
119+ const stored = JSON . parse ( readFileSync ( path , "utf8" ) ) ;
120+ expect ( stored . default ) . toBe ( info . mnemonic ) ;
121+ } ) ;
122+
123+ it ( "after persist, getOrCreateSessionAccount returns created=false" , async ( ) => {
124+ const { info } = await getOrCreateSessionAccount ( ) ;
125+ await persistSessionAccount ( info ) ;
126+
127+ const second = await getOrCreateSessionAccount ( ) ;
128+ expect ( second . created ) . toBe ( false ) ;
129+ expect ( second . info . mnemonic ) . toBe ( info . mnemonic ) ;
130+ } ) ;
131+
132+ it ( "map_account-fails scenario: no persist → retry mints new key and re-maps" , async ( ) => {
133+ // Step 1: create key in memory (map_account would be attempted here).
134+ const attempt1 = await getOrCreateSessionAccount ( ) ;
135+ expect ( attempt1 . created ) . toBe ( true ) ;
136+
137+ // Step 2: map_account throws — do NOT call persistSessionAccount.
138+ // (No action needed — file is untouched.)
139+
140+ // Step 3: retry — should produce a fresh key with created=true.
141+ const attempt2 = await getOrCreateSessionAccount ( ) ;
142+ expect ( attempt2 . created ) . toBe ( true ) ;
143+ // It's a different key, so the retry path would re-attempt map_account.
144+ expect ( attempt2 . info . mnemonic ) . not . toBe ( attempt1 . info . mnemonic ) ;
145+
146+ // Step 4: map_account succeeds — now persist.
147+ await persistSessionAccount ( attempt2 . info ) ;
148+
149+ // Step 5: subsequent call loads from disk.
150+ const loaded = await getOrCreateSessionAccount ( ) ;
151+ expect ( loaded . created ) . toBe ( false ) ;
152+ expect ( loaded . info . mnemonic ) . toBe ( attempt2 . info . mnemonic ) ;
153+ } ) ;
154+ } ) ;
155+
156+ // ── readSessionAccount ────────────────────────────────────────────────────────
157+
66158describe ( "readSessionAccount" , ( ) => {
67159 let tmp : string ;
68160 let originalRoot : string | undefined ;
@@ -87,10 +179,12 @@ describe("readSessionAccount", () => {
87179 } ) ;
88180
89181 it ( "returns the persisted key without creating a new one" , async ( ) => {
90- const { info : created } = await getOrCreateSessionAccount ( ) ;
182+ const { info } = await getOrCreateSessionAccount ( ) ;
183+ await persistSessionAccount ( info ) ;
184+
91185 const loaded = await readSessionAccount ( ) ;
92- expect ( loaded ?. mnemonic ) . toBe ( created . mnemonic ) ;
93- expect ( loaded ?. account . ss58Address ) . toBe ( created . account . ss58Address ) ;
186+ expect ( loaded ?. mnemonic ) . toBe ( info . mnemonic ) ;
187+ expect ( loaded ?. account . ss58Address ) . toBe ( info . account . ss58Address ) ;
94188 } ) ;
95189
96190 it ( "does not create a file when the store is empty (read-only)" , async ( ) => {
0 commit comments