11import { afterEach , beforeEach , describe , expect , test } from "bun:test" ;
2- import { mkdtemp , readFile , rm , writeFile } from "node:fs/promises" ;
2+ import { mkdtemp , readFile , rename , rm , writeFile } from "node:fs/promises" ;
33import { tmpdir } from "node:os" ;
4- import { join } from "node:path" ;
4+ import { basename , join } from "node:path" ;
55import { parse } from "comment-json" ;
66import {
7+ __resetTuiPreferencesWatchTestHooks ,
8+ __setTuiPreferencesWatchTestHooks ,
79 computeEffectiveOrder ,
810 DEFAULT_PREFS ,
911 DEFAULT_SLOT_ORDER ,
@@ -13,6 +15,7 @@ import {
1315 readTuiPreferencesFile ,
1416 resolveMagicContextPrefs ,
1517 TUI_PREFS_FILE_ENV ,
18+ watchTuiPreferences ,
1619} from "./tui-preferences" ;
1720
1821let dir : string ;
@@ -28,6 +31,7 @@ beforeEach(async () => {
2831} ) ;
2932
3033afterEach ( async ( ) => {
34+ __resetTuiPreferencesWatchTestHooks ( ) ;
3135 for ( const key of ENV_KEYS ) {
3236 if ( savedEnv [ key ] === undefined ) delete process . env [ key ] ;
3337 else process . env [ key ] = savedEnv [ key ] ;
@@ -79,6 +83,52 @@ describe("readTuiPreferencesFile (tolerant)", () => {
7983 } ) ;
8084} ) ;
8185
86+ describe ( "watchTuiPreferences" , ( ) => {
87+ test ( "notifies when the initial read resolves after an atomic replacement" , async ( ) => {
88+ const previous = `{"magic-context":{"sections":{"memory":true}}}\n` ;
89+ const next = `{"magic-context":{"sections":{"memory":false}}}\n` ;
90+ await writeFile ( file , previous , "utf8" ) ;
91+
92+ let resolveInitialRead ! : ( text : string ) => void ;
93+ const pendingInitialRead = new Promise < string > ( ( resolve ) => {
94+ resolveInitialRead = resolve ;
95+ } ) ;
96+ let emitWatchEvent ! : ( event : string , filename : string | null ) => void ;
97+ let readCount = 0 ;
98+ __setTuiPreferencesWatchTestHooks ( {
99+ readFile : ( ) => {
100+ readCount += 1 ;
101+ return readCount === 1 ? pendingInitialRead : Promise . resolve ( next ) ;
102+ } ,
103+ watch : ( _directory , listener ) => {
104+ emitWatchEvent = listener ;
105+ return { close ( ) { } } ;
106+ } ,
107+ } ) ;
108+
109+ const observed = new Promise < { memory : boolean } > ( ( resolve ) => {
110+ const stop = watchTuiPreferences ( ( ) => {
111+ void readTuiPreferencesFile ( ) . then ( ( root ) => {
112+ stop ( ) ;
113+ resolve ( { memory : resolveMagicContextPrefs ( root ) . sections . memory } ) ;
114+ } ) ;
115+ } ) ;
116+ } ) ;
117+
118+ const replacement = `${ file } .external.tmp` ;
119+ await writeFile ( replacement , next , "utf8" ) ;
120+ await rename ( replacement , file ) ;
121+ emitWatchEvent ( "rename" , basename ( file ) ) ;
122+ resolveInitialRead ( next ) ;
123+
124+ const result = await Promise . race ( [
125+ observed ,
126+ new Promise < "timeout" > ( ( resolve ) => setTimeout ( ( ) => resolve ( "timeout" ) , 300 ) ) ,
127+ ] ) ;
128+ expect ( result ) . toEqual ( { memory : false } ) ;
129+ } ) ;
130+ } ) ;
131+
82132describe ( "resolveMagicContextPrefs (per-key validation)" , ( ) => {
83133 test ( "missing key → full defaults clone" , ( ) => {
84134 expect ( resolveMagicContextPrefs ( { } ) ) . toEqual ( DEFAULT_PREFS ) ;
0 commit comments