| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import assert from 'node:assert/strict'; |
| import { spawn } from 'node:child_process'; |
| import { createHash } from 'node:crypto'; |
| import { once } from 'node:events'; |
| import { access, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; |
| import { tmpdir } from 'node:os'; |
| import { join } from 'node:path'; |
| import { describe, test } from 'node:test'; |
| import { fileURLToPath } from 'node:url'; |
| import { parseMakaCliArgs, runMakaCli } from '../cli-core.js'; |
| |
| describe('Maka CLI args', () => { |
| test('declares a bin-only package surface', async () => { |
| const manifest = JSON.parse( |
| await readFile(new URL('../../package.json', import.meta.url), 'utf8'), |
| ) as Record<string, unknown>; |
| assert.deepEqual(manifest.bin, { |
| maka: './dist/cli.js', |
| }); |
| assert.deepEqual(manifest.exports, {}); |
| assert.equal(Object.hasOwn(manifest, 'main'), false); |
| assert.equal(Object.hasOwn(manifest, 'types'), false); |
| await assert.rejects(access(new URL('../index.js', import.meta.url)), { code: 'ENOENT' }); |
| }); |
| |
| test('publishes the supported release command surface', () => { |
| const help = parseMakaCliArgs(['--help'], '0.1.0'); |
| assert.equal(help.kind, 'help'); |
| if (help.kind !== 'help') return; |
| assert.match(help.text, /^ {2}maka {2,}Start the TUI$/m); |
| assert.doesNotMatch(help.text, /maka-agent/); |
| assert.match(help.text, /^ {2}maka run /m); |
| assert.match(help.text, /^ {2}maka activate /m); |
| assert.match(help.text, /^ {2}maka eval /m); |
| assert.match(help.text, /^ {2}maka update /m); |
| assert.match( |
| help.text, |
| /^ {2}maka --acp {2,}Serve ACP v1 over stdio \(sessions, prompts, streaming, cancellation\)$/m, |
| ); |
| // Runtime Host owns its own help; the root lists it once and points there. |
| assert.match(help.text, /^ {2}maka runtime-host \.\.\. {2,}Serve and manage a Runtime Host$/m); |
| assert.doesNotMatch(help.text, /^ {2}maka runtime-host (?:serve|service|access) /m); |
| assert.ok(help.text.split('\n').length < 30, 'the root help stays a single screen'); |
| assert.doesNotMatch(help.text, /cli:dev/); |
| }); |
| |
| test('requires an explicit installed update target and interruption choice', () => { |
| assert.deepEqual(parseMakaCliArgs(['update', '--target', 'next'], '0.1.0'), { |
| kind: 'runtime-host-installed-update', |
| selector: { kind: 'channel', channel: 'next' }, |
| allowInterruptActiveTasks: false, |
| }); |
| assert.deepEqual( |
| parseMakaCliArgs(['update', '--target', '1.2.3', '--allow-interrupt-active-tasks'], '0.1.0'), |
| { |
| kind: 'runtime-host-installed-update', |
| selector: { kind: 'exact', version: '1.2.3' }, |
| allowInterruptActiveTasks: true, |
| }, |
| ); |
| assert.deepEqual(parseMakaCliArgs(['update'], '0.1.0'), { |
| kind: 'error', |
| message: 'update requires --target <latest|next|version>', |
| exitCode: 2, |
| }); |
| }); |
| |
| test('selects a Runtime Host and Project for TUI startup', () => { |
| assert.deepEqual(parseMakaCliArgs(['--host', 'office', '--project', 'project-1'], '0.1.0'), { |
| kind: 'tui', |
| hostProfileId: 'office', |
| projectId: 'project-1', |
| }); |
| }); |
| |
| test('parses the ACP stdio command before TUI flags', () => { |
| assert.deepEqual(parseMakaCliArgs(['--acp'], '0.1.0'), { kind: 'acp' }); |
| assert.deepEqual(parseMakaCliArgs(['--acp', 'extra'], '0.1.0'), { |
| kind: 'error', |
| message: 'maka --acp does not accept arguments', |
| exitCode: 2, |
| showHelp: false, |
| }); |
| assert.deepEqual(parseMakaCliArgs(['--host', 'office', '--project', 'project-1'], '0.1.0'), { |
| kind: 'tui', |
| hostProfileId: 'office', |
| projectId: 'project-1', |
| }); |
| }); |
| |
| test('compiled ACP launcher rejects trailing arguments without help output', async () => { |
| const result = await runCompiledCli('dev-cli.js', ['--acp', 'extra'], process.env); |
| |
| assert.equal(result.signal, null); |
| assert.equal(result.code, 2); |
| assert.equal(result.stdout, ''); |
| assert.equal(result.stderr, 'maka --acp does not accept arguments\n'); |
| }); |
| |
| test('uses the active launcher in development help and rejects an empty profile name', async () => { |
| const help = parseMakaCliArgs(['--help'], '0.1.0', 'npm run cli:dev --'); |
| assert.equal(help.kind, 'help'); |
| if (help.kind === 'help') { |
| assert.match(help.text, /^Usage: npm run cli:dev --$/m); |
| assert.match(help.text, /^ {2}npm run cli:dev -- runtime-host \.\.\. /m); |
| assert.doesNotMatch(help.text, /^ {2}maka runtime-host /m); |
| assert.doesNotMatch(help.text, /maka-agent/); |
| } |
| // The launcher name has to survive every layer, not just the root. |
| const hostHelp = parseMakaCliArgs(['runtime-host', '--help'], '0.1.0', 'npm run cli:dev --'); |
| assert.equal(hostHelp.kind, 'help'); |
| if (hostHelp.kind === 'help') { |
| assert.match(hostHelp.text, /^Usage: npm run cli:dev -- runtime-host <command>/m); |
| } |
| const accessHelp = parseMakaCliArgs( |
| ['runtime-host', 'access', '--help'], |
| '0.1.0', |
| 'npm run cli:dev --', |
| ); |
| assert.equal(accessHelp.kind, 'help'); |
| if (accessHelp.kind === 'help') { |
| assert.match( |
| accessHelp.text, |
| /^ {2}npm run cli:dev -- runtime-host access issue --principal <id> --preset /m, |
| ); |
| } |
| await assert.rejects( |
| runMakaCli(['--version'], { |
| dataProfileName: '', |
| cliCommand: 'invalid', |
| capabilityProviderIdentityScope: 'client-data-root', |
| }), |
| /profile name must be a non-empty path segment/, |
| ); |
| }); |
| |
| test('does not add a discoverable global locale flag', () => { |
| assert.deepEqual(parseMakaCliArgs(['--locale', 'zh-CN'], '0.1.0'), { |
| kind: 'error', |
| message: 'Unexpected argument: --locale', |
| exitCode: 2, |
| }); |
| }); |
| |
| test('establishes the fatal exit before reporting can throw', async () => { |
| const cliUrl = new URL('../cli-core.js', import.meta.url).href; |
| const childSource = ` |
| import { handleMakaCliProcessExit } from ${JSON.stringify(cliUrl)}; |
| try { |
| handleMakaCliProcessExit(1, new Error('fatal'), () => { throw new Error('writer failed'); }); |
| } catch {} |
| `; |
| const child = spawn(process.execPath, ['--input-type=module', '-e', childSource], { |
| stdio: 'ignore', |
| }); |
| const [code, signal] = (await once(child, 'exit')) as [number | null, NodeJS.Signals | null]; |
| |
| assert.equal(signal, null); |
| assert.equal(code, 1); |
| }); |
| |
| test('compiled release and repository entries isolate profile writes', async (t) => { |
| const root = await mkdtemp(join(tmpdir(), 'maka-cli-launch-profile-')); |
| t.after(() => rm(root, { recursive: true, force: true })); |
| const home = join(root, 'home'); |
| const applicationData = join(root, 'application-data'); |
| const releaseRoot = platformProfileRoot(home, applicationData, 'Maka'); |
| const developmentRoot = platformProfileRoot(home, applicationData, 'Maka Dev'); |
| const env = { |
| ...process.env, |
| HOME: home, |
| USERPROFILE: home, |
| APPDATA: applicationData, |
| XDG_CONFIG_HOME: applicationData, |
| MAKA_TEST_RUNTIME_HOST_CREDENTIAL: 'opaque-token', |
| }; |
| const profileArgs = [ |
| 'runtime-host', |
| 'profile', |
| 'set', |
| '--id', |
| 'office', |
| '--name', |
| 'Office', |
| '--tls-url', |
| 'wss://runtime.example.com/runtime-host', |
| '--expected-root', |
| 'a'.repeat(64), |
| '--credential-env', |
| 'MAKA_TEST_RUNTIME_HOST_CREDENTIAL', |
| ]; |
| |
| await mkdir(releaseRoot, { recursive: true }); |
| await writeFile(join(releaseRoot, 'sentinel.txt'), 'release-data\n', 'utf8'); |
| const development = await runCompiledCli('dev-cli.js', profileArgs, env); |
| assert.equal(development.signal, null); |
| assert.equal(development.code, 0, development.stderr); |
| await assertProfileFiles(developmentRoot); |
| assert.deepEqual(await readdir(releaseRoot), ['sentinel.txt']); |
| assert.equal(await readFile(join(releaseRoot, 'sentinel.txt'), 'utf8'), 'release-data\n'); |
| await assert.rejects(access(join(developmentRoot, 'sentinel.txt')), { code: 'ENOENT' }); |
| |
| const developmentProfile = await readFile( |
| join(developmentRoot, 'runtime-host-profiles.json'), |
| 'utf8', |
| ); |
| const release = await runCompiledCli('cli.js', profileArgs, env); |
| assert.equal(release.signal, null); |
| assert.equal(release.code, 0, release.stderr); |
| await assertProfileFiles(releaseRoot); |
| assert.equal( |
| await readFile(join(developmentRoot, 'runtime-host-profiles.json'), 'utf8'), |
| developmentProfile, |
| ); |
| assert.equal(await readFile(join(releaseRoot, 'sentinel.txt'), 'utf8'), 'release-data\n'); |
| }); |
| |
| test('compiled launchers isolate capability-provider identities', async (t) => { |
| const root = await mkdtemp(join(tmpdir(), 'maka-cli-provider-identity-')); |
| t.after(() => rm(root, { recursive: true, force: true })); |
| const home = join(root, 'home'); |
| const applicationData = join(root, 'application-data'); |
| const releaseRoot = platformProfileRoot(home, applicationData, 'Maka'); |
| const developmentRoot = platformProfileRoot(home, applicationData, 'Maka Dev'); |
| const configPath = join(root, 'mcp.json'); |
| const url = 'https://invalid.example/runtime-host'; |
| const identityFile = providerIdentityFileName(url, configPath); |
| const developmentIdentityPath = join( |
| developmentRoot, |
| 'runtime-host-capability-providers', |
| identityFile, |
| ); |
| const releaseIdentityPath = join( |
| home, |
| '.maka', |
| 'runtime-host-capability-providers', |
| identityFile, |
| ); |
| const releaseProfileIdentityPath = join( |
| releaseRoot, |
| 'runtime-host-capability-providers', |
| identityFile, |
| ); |
| const explicitUrl = 'https://explicit.invalid/runtime-host'; |
| const explicitDefaultIdentityPath = join( |
| developmentRoot, |
| 'runtime-host-capability-providers', |
| providerIdentityFileName(explicitUrl, configPath), |
| ); |
| const explicitIdentityPath = join(root, 'explicit-provider-identity.json'); |
| const env = { |
| ...process.env, |
| HOME: home, |
| USERPROFILE: home, |
| APPDATA: applicationData, |
| XDG_CONFIG_HOME: applicationData, |
| MAKA_TEST_RUNTIME_HOST_CREDENTIAL: 'opaque-token', |
| }; |
| await writeFile(configPath, '{"mcpServers": {}}\n', 'utf8'); |
| const providerArgs = [ |
| 'runtime-host', |
| 'capability-provider', |
| 'serve', |
| '--url', |
| url, |
| '--mcp-config', |
| configPath, |
| '--expected-root', |
| 'b'.repeat(64), |
| '--credential-env', |
| 'MAKA_TEST_RUNTIME_HOST_CREDENTIAL', |
| ]; |
| |
| const development = await runCompiledCli('dev-cli.js', providerArgs, env); |
| assert.equal(development.signal, null); |
| assert.equal(development.code, 1); |
| const developmentIdentity = await readClientInstanceId(developmentIdentityPath); |
| await assert.rejects(access(releaseIdentityPath), { code: 'ENOENT' }); |
| |
| const release = await runCompiledCli('cli.js', providerArgs, env); |
| assert.equal(release.signal, null); |
| assert.equal(release.code, 1); |
| const releaseIdentity = await readClientInstanceId(releaseIdentityPath); |
| assert.notEqual(releaseIdentity, developmentIdentity); |
| await assert.rejects(access(releaseProfileIdentityPath), { code: 'ENOENT' }); |
| |
| const developmentIdentityBeforeOverride = await readFile(developmentIdentityPath, 'utf8'); |
| const explicitProviderArgs = providerArgs.map((arg) => (arg === url ? explicitUrl : arg)); |
| const explicit = await runCompiledCli( |
| 'dev-cli.js', |
| [...explicitProviderArgs, '--client-identity', explicitIdentityPath], |
| env, |
| ); |
| assert.equal(explicit.signal, null); |
| assert.equal(explicit.code, 1); |
| const explicitIdentity = await readClientInstanceId(explicitIdentityPath); |
| assert.notEqual(explicitIdentity, developmentIdentity); |
| assert.notEqual(explicitIdentity, releaseIdentity); |
| await assert.rejects(access(explicitDefaultIdentityPath), { code: 'ENOENT' }); |
| assert.equal( |
| await readFile(developmentIdentityPath, 'utf8'), |
| developmentIdentityBeforeOverride, |
| ); |
| }); |
| }); |
| |
| async function runCompiledCli( |
| entrypoint: string, |
| args: readonly string[], |
| env: NodeJS.ProcessEnv, |
| ): Promise<{ code: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string }> { |
| const child = spawn( |
| process.execPath, |
| [fileURLToPath(new URL(`../${entrypoint}`, import.meta.url)), ...args], |
| { env, stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000, killSignal: 'SIGKILL' }, |
| ); |
| let stdout = ''; |
| child.stdout.setEncoding('utf8'); |
| child.stdout.on('data', (chunk: string) => { |
| stdout += chunk; |
| }); |
| let stderr = ''; |
| child.stderr.setEncoding('utf8'); |
| child.stderr.on('data', (chunk: string) => { |
| stderr += chunk; |
| }); |
| const [code, signal] = (await once(child, 'close')) as [number | null, NodeJS.Signals | null]; |
| return { code, signal, stdout, stderr }; |
| } |
| |
| function platformProfileRoot(home: string, applicationData: string, profileName: string): string { |
| if (process.platform === 'darwin') { |
| return join(home, 'Library', 'Application Support', profileName); |
| } |
| return join(applicationData, profileName); |
| } |
| |
| async function assertProfileFiles(root: string): Promise<void> { |
| await access(join(root, 'runtime-host-profiles.json')); |
| await access(join(root, 'runtime-host-client', 'credentials.json')); |
| } |
| |
| function providerIdentityFileName(url: string, configPath: string): string { |
| const identity = createHash('sha256') |
| .update(`runtime-host-capability-provider\0${url}\0${configPath}`) |
| .digest('hex') |
| .slice(0, 24); |
| return `${identity}.json`; |
| } |
| |
| async function readClientInstanceId(path: string): Promise<string> { |
| const document = JSON.parse(await readFile(path, 'utf8')) as { |
| schemaVersion?: unknown; |
| clientInstanceId?: unknown; |
| }; |
| assert.equal(document.schemaVersion, 1); |
| if (typeof document.clientInstanceId !== 'string') { |
| assert.fail('Expected a persisted Client instance id'); |
| } |
| return document.clientInstanceId; |
| } |
| |
| describe('layered help coverage', () => { |
| const screens = (launcher = 'maka') => { |
| const texts: string[] = []; |
| const grab = (argv: string[]) => { |
| const command = parseMakaCliArgs(argv, '0.1.0', launcher); |
| if (command.kind === 'help') texts.push(command.text); |
| }; |
| grab(['--help']); |
| grab(['update', '--help']); |
| grab(['session-export', '--help']); |
| grab(['session-import', '--help']); |
| grab(['runtime-host', '--help']); |
| for (const topic of [ |
| 'activate', |
| 'serve', |
| 'setup', |
| 'service', |
| 'access', |
| 'project', |
| 'plugin', |
| 'profile', |
| 'capability-provider', |
| ]) { |
| grab(['runtime-host', topic, '--help']); |
| } |
| return texts.join('\n'); |
| }; |
| |
| test('every command the parser accepts has a help screen', () => { |
| const all = screens(); |
| for (const command of [ |
| 'runtime-host activate', |
| 'runtime-host serve', |
| 'runtime-host setup', |
| 'runtime-host service', |
| 'runtime-host access', |
| 'runtime-host project', |
| 'runtime-host plugin', |
| 'runtime-host profile', |
| 'runtime-host capability-provider', |
| 'session-export', |
| 'session-import', |
| 'update', |
| ]) { |
| assert.ok(all.includes(`maka ${command}`), `${command} is unreachable from any help screen`); |
| } |
| }); |
| |
| test('the credential environment variable stays documented', () => { |
| // Its only other home was the root screen, which the layering shrank. |
| assert.match(screens(), /MAKA_RUNTIME_HOST_ACCESS_CREDENTIAL/); |
| }); |
| |
| test('every summary in the root screen shares one column', () => { |
| const help = parseMakaCliArgs(['--help'], '0.1.0'); |
| assert.equal(help.kind, 'help'); |
| if (help.kind !== 'help') return; |
| const summaryColumns = help.text |
| .split('\n') |
| .filter((line) => /^ {2}maka\b/.test(line)) |
| .map((line) => line.search(/(?<= {2})\S(?!.*\s{2}\S)/u)) |
| .filter((column) => column > 0); |
| assert.ok(summaryColumns.length > 5, 'the root screen should list several commands'); |
| assert.equal( |
| new Set(summaryColumns).size, |
| 1, |
| `summaries start at ${[...new Set(summaryColumns)].join(', ')}`, |
| ); |
| }); |
| }); |