11import assert from 'node:assert/strict' ;
22import { spawn } from 'node:child_process' ;
3+ import { createHash } from 'node:crypto' ;
34import { once } from 'node:events' ;
5+ import { access , mkdtemp , mkdir , readFile , readdir , rm , writeFile } from 'node:fs/promises' ;
6+ import { tmpdir } from 'node:os' ;
7+ import { join } from 'node:path' ;
48import { describe , test } from 'node:test' ;
5- import { parseMakaCliArgs } from '../cli.js' ;
9+ import { fileURLToPath } from 'node:url' ;
10+ import { parseMakaCliArgs , runMakaCli } from '../cli.js' ;
611
712describe ( 'Maka CLI args' , ( ) => {
813 test ( 'selects a Runtime Host and Project for TUI startup' , ( ) => {
@@ -13,6 +18,34 @@ describe('Maka CLI args', () => {
1318 } ) ;
1419 } ) ;
1520
21+ test ( 'uses the active launcher in development help and rejects an empty profile name' , async ( ) => {
22+ const help = parseMakaCliArgs ( [ '--help' ] , '0.1.0' , 'npm run cli:dev --' ) ;
23+ assert . equal ( help . kind , 'help' ) ;
24+ if ( help . kind === 'help' ) {
25+ assert . match ( help . text , / ^ U s a g e : n p m r u n c l i : d e v - - $ / m) ;
26+ assert . match (
27+ help . text ,
28+ / ^ n p m r u n c l i : d e v - - r u n t i m e - h o s t a c c e s s i s s u e - - p r i n c i p a l < i d > - - p r e s e t / m,
29+ ) ;
30+ assert . match ( help . text , / ^ n p m r u n c l i : d e v - - r u n t i m e - h o s t p r o j e c t l i s t / m) ;
31+ assert . match (
32+ help . text ,
33+ / ^ n p m r u n c l i : d e v - - r u n t i m e - h o s t p r o f i l e s e t .* - - s s h - d e s t i n a t i o n / m,
34+ ) ;
35+ assert . match ( help . text , / ^ n p m r u n c l i : d e v - - r u n t i m e - h o s t p r o f i l e s e t .* - - p l a i n t e x t - u r l / m) ;
36+ assert . doesNotMatch ( help . text , / ^ m a k a r u n t i m e - h o s t / m) ;
37+ assert . doesNotMatch ( help . text , / m a k a - a g e n t / ) ;
38+ }
39+ await assert . rejects (
40+ runMakaCli ( [ '--version' ] , {
41+ dataProfileName : '' ,
42+ cliCommand : 'invalid' ,
43+ capabilityProviderIdentityScope : 'client-data-root' ,
44+ } ) ,
45+ / p r o f i l e n a m e m u s t b e a n o n - e m p t y p a t h s e g m e n t / ,
46+ ) ;
47+ } ) ;
48+
1649 test ( 'establishes the fatal exit before reporting can throw' , async ( ) => {
1750 const cliUrl = new URL ( '../cli.js' , import . meta. url ) . href ;
1851 const childSource = `
@@ -29,4 +62,199 @@ describe('Maka CLI args', () => {
2962 assert . equal ( signal , null ) ;
3063 assert . equal ( code , 1 ) ;
3164 } ) ;
65+
66+ test ( 'compiled release and repository entries isolate profile writes' , async ( t ) => {
67+ const root = await mkdtemp ( join ( tmpdir ( ) , 'maka-cli-launch-profile-' ) ) ;
68+ t . after ( ( ) => rm ( root , { recursive : true , force : true } ) ) ;
69+ const home = join ( root , 'home' ) ;
70+ const applicationData = join ( root , 'application-data' ) ;
71+ const releaseRoot = platformProfileRoot ( home , applicationData , 'Maka' ) ;
72+ const developmentRoot = platformProfileRoot ( home , applicationData , 'Maka Dev' ) ;
73+ const env = {
74+ ...process . env ,
75+ HOME : home ,
76+ USERPROFILE : home ,
77+ APPDATA : applicationData ,
78+ XDG_CONFIG_HOME : applicationData ,
79+ MAKA_TEST_RUNTIME_HOST_CREDENTIAL : 'opaque-token' ,
80+ } ;
81+ const profileArgs = [
82+ 'runtime-host' ,
83+ 'profile' ,
84+ 'set' ,
85+ '--id' ,
86+ 'office' ,
87+ '--name' ,
88+ 'Office' ,
89+ '--tls-url' ,
90+ 'wss://runtime.example.com/runtime-host' ,
91+ '--expected-root' ,
92+ 'a' . repeat ( 64 ) ,
93+ '--credential-env' ,
94+ 'MAKA_TEST_RUNTIME_HOST_CREDENTIAL' ,
95+ ] ;
96+
97+ await mkdir ( releaseRoot , { recursive : true } ) ;
98+ await writeFile ( join ( releaseRoot , 'sentinel.txt' ) , 'release-data\n' , 'utf8' ) ;
99+ const development = await runCompiledCli ( 'dev-cli.js' , profileArgs , env ) ;
100+ assert . equal ( development . signal , null ) ;
101+ assert . equal ( development . code , 0 , development . stderr ) ;
102+ await assertProfileFiles ( developmentRoot ) ;
103+ assert . deepEqual ( await readdir ( releaseRoot ) , [ 'sentinel.txt' ] ) ;
104+ assert . equal ( await readFile ( join ( releaseRoot , 'sentinel.txt' ) , 'utf8' ) , 'release-data\n' ) ;
105+ await assert . rejects ( access ( join ( developmentRoot , 'sentinel.txt' ) ) , { code : 'ENOENT' } ) ;
106+
107+ const developmentProfile = await readFile (
108+ join ( developmentRoot , 'runtime-host-profiles.json' ) ,
109+ 'utf8' ,
110+ ) ;
111+ const release = await runCompiledCli ( 'cli.js' , profileArgs , env ) ;
112+ assert . equal ( release . signal , null ) ;
113+ assert . equal ( release . code , 0 , release . stderr ) ;
114+ await assertProfileFiles ( releaseRoot ) ;
115+ assert . equal (
116+ await readFile ( join ( developmentRoot , 'runtime-host-profiles.json' ) , 'utf8' ) ,
117+ developmentProfile ,
118+ ) ;
119+ assert . equal ( await readFile ( join ( releaseRoot , 'sentinel.txt' ) , 'utf8' ) , 'release-data\n' ) ;
120+ } ) ;
121+
122+ test ( 'compiled launchers isolate capability-provider identities' , async ( t ) => {
123+ const root = await mkdtemp ( join ( tmpdir ( ) , 'maka-cli-provider-identity-' ) ) ;
124+ t . after ( ( ) => rm ( root , { recursive : true , force : true } ) ) ;
125+ const home = join ( root , 'home' ) ;
126+ const applicationData = join ( root , 'application-data' ) ;
127+ const releaseRoot = platformProfileRoot ( home , applicationData , 'Maka' ) ;
128+ const developmentRoot = platformProfileRoot ( home , applicationData , 'Maka Dev' ) ;
129+ const configPath = join ( root , 'mcp.json' ) ;
130+ const url = 'https://invalid.example/runtime-host' ;
131+ const identityFile = providerIdentityFileName ( url , configPath ) ;
132+ const developmentIdentityPath = join (
133+ developmentRoot ,
134+ 'runtime-host-capability-providers' ,
135+ identityFile ,
136+ ) ;
137+ const releaseIdentityPath = join (
138+ home ,
139+ '.maka' ,
140+ 'runtime-host-capability-providers' ,
141+ identityFile ,
142+ ) ;
143+ const releaseProfileIdentityPath = join (
144+ releaseRoot ,
145+ 'runtime-host-capability-providers' ,
146+ identityFile ,
147+ ) ;
148+ const explicitUrl = 'https://explicit.invalid/runtime-host' ;
149+ const explicitDefaultIdentityPath = join (
150+ developmentRoot ,
151+ 'runtime-host-capability-providers' ,
152+ providerIdentityFileName ( explicitUrl , configPath ) ,
153+ ) ;
154+ const explicitIdentityPath = join ( root , 'explicit-provider-identity.json' ) ;
155+ const env = {
156+ ...process . env ,
157+ HOME : home ,
158+ USERPROFILE : home ,
159+ APPDATA : applicationData ,
160+ XDG_CONFIG_HOME : applicationData ,
161+ MAKA_TEST_RUNTIME_HOST_CREDENTIAL : 'opaque-token' ,
162+ } ;
163+ await writeFile ( configPath , '{"mcpServers": {}}\n' , 'utf8' ) ;
164+ const providerArgs = [
165+ 'runtime-host' ,
166+ 'capability-provider' ,
167+ 'serve' ,
168+ '--url' ,
169+ url ,
170+ '--mcp-config' ,
171+ configPath ,
172+ '--expected-root' ,
173+ 'b' . repeat ( 64 ) ,
174+ '--credential-env' ,
175+ 'MAKA_TEST_RUNTIME_HOST_CREDENTIAL' ,
176+ ] ;
177+
178+ const development = await runCompiledCli ( 'dev-cli.js' , providerArgs , env ) ;
179+ assert . equal ( development . signal , null ) ;
180+ assert . equal ( development . code , 1 ) ;
181+ const developmentIdentity = await readClientInstanceId ( developmentIdentityPath ) ;
182+ await assert . rejects ( access ( releaseIdentityPath ) , { code : 'ENOENT' } ) ;
183+
184+ const release = await runCompiledCli ( 'cli.js' , providerArgs , env ) ;
185+ assert . equal ( release . signal , null ) ;
186+ assert . equal ( release . code , 1 ) ;
187+ const releaseIdentity = await readClientInstanceId ( releaseIdentityPath ) ;
188+ assert . notEqual ( releaseIdentity , developmentIdentity ) ;
189+ await assert . rejects ( access ( releaseProfileIdentityPath ) , { code : 'ENOENT' } ) ;
190+
191+ const developmentIdentityBeforeOverride = await readFile ( developmentIdentityPath , 'utf8' ) ;
192+ const explicitProviderArgs = providerArgs . map ( ( arg ) => ( arg === url ? explicitUrl : arg ) ) ;
193+ const explicit = await runCompiledCli (
194+ 'dev-cli.js' ,
195+ [ ...explicitProviderArgs , '--client-identity' , explicitIdentityPath ] ,
196+ env ,
197+ ) ;
198+ assert . equal ( explicit . signal , null ) ;
199+ assert . equal ( explicit . code , 1 ) ;
200+ const explicitIdentity = await readClientInstanceId ( explicitIdentityPath ) ;
201+ assert . notEqual ( explicitIdentity , developmentIdentity ) ;
202+ assert . notEqual ( explicitIdentity , releaseIdentity ) ;
203+ await assert . rejects ( access ( explicitDefaultIdentityPath ) , { code : 'ENOENT' } ) ;
204+ assert . equal (
205+ await readFile ( developmentIdentityPath , 'utf8' ) ,
206+ developmentIdentityBeforeOverride ,
207+ ) ;
208+ } ) ;
32209} ) ;
210+
211+ async function runCompiledCli (
212+ entrypoint : string ,
213+ args : readonly string [ ] ,
214+ env : NodeJS . ProcessEnv ,
215+ ) : Promise < { code : number | null ; signal : NodeJS . Signals | null ; stderr : string } > {
216+ const child = spawn (
217+ process . execPath ,
218+ [ fileURLToPath ( new URL ( `../${ entrypoint } ` , import . meta. url ) ) , ...args ] ,
219+ { env, stdio : [ 'ignore' , 'ignore' , 'pipe' ] , timeout : 15_000 , killSignal : 'SIGKILL' } ,
220+ ) ;
221+ let stderr = '' ;
222+ child . stderr . setEncoding ( 'utf8' ) ;
223+ child . stderr . on ( 'data' , ( chunk : string ) => {
224+ stderr += chunk ;
225+ } ) ;
226+ const [ code , signal ] = ( await once ( child , 'close' ) ) as [ number | null , NodeJS . Signals | null ] ;
227+ return { code, signal, stderr } ;
228+ }
229+
230+ function platformProfileRoot ( home : string , applicationData : string , profileName : string ) : string {
231+ if ( process . platform === 'darwin' ) {
232+ return join ( home , 'Library' , 'Application Support' , profileName ) ;
233+ }
234+ return join ( applicationData , profileName ) ;
235+ }
236+
237+ async function assertProfileFiles ( root : string ) : Promise < void > {
238+ await access ( join ( root , 'runtime-host-profiles.json' ) ) ;
239+ await access ( join ( root , 'runtime-host-client' , 'credentials.json' ) ) ;
240+ }
241+
242+ function providerIdentityFileName ( url : string , configPath : string ) : string {
243+ const identity = createHash ( 'sha256' )
244+ . update ( `runtime-host-capability-provider\0${ url } \0${ configPath } ` )
245+ . digest ( 'hex' )
246+ . slice ( 0 , 24 ) ;
247+ return `${ identity } .json` ;
248+ }
249+
250+ async function readClientInstanceId ( path : string ) : Promise < string > {
251+ const document = JSON . parse ( await readFile ( path , 'utf8' ) ) as {
252+ schemaVersion ?: unknown ;
253+ clientInstanceId ?: unknown ;
254+ } ;
255+ assert . equal ( document . schemaVersion , 1 ) ;
256+ if ( typeof document . clientInstanceId !== 'string' ) {
257+ assert . fail ( 'Expected a persisted Client instance id' ) ;
258+ }
259+ return document . clientInstanceId ;
260+ }
0 commit comments