blob: 9d9fc613ab92ce405104ba36c7ddd5fe01288e5d [file]
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, /^ maka Start the TUI$/m);
assert.doesNotMatch(help.text, /maka-agent/);
assert.match(help.text, /^ maka run /m);
assert.match(help.text, /^ maka activate /m);
assert.match(help.text, /^ maka eval /m);
assert.match(help.text, /^ maka runtime-host serve /m);
assert.doesNotMatch(help.text, /cli:dev/);
});
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('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,
/^ npm run cli:dev -- runtime-host access issue --principal <id> --preset /m,
);
assert.match(help.text, /^ npm run cli:dev -- runtime-host project list /m);
assert.match(
help.text,
/^ npm run cli:dev -- runtime-host profile set .*--ssh-destination /m,
);
assert.match(help.text, /^ npm run cli:dev -- runtime-host profile set .*--plaintext-url /m);
assert.doesNotMatch(help.text, /^ maka runtime-host /m);
assert.doesNotMatch(help.text, /maka-agent/);
}
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'], '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; stderr: string }> {
const child = spawn(
process.execPath,
[fileURLToPath(new URL(`../${entrypoint}`, import.meta.url)), ...args],
{ env, stdio: ['ignore', 'ignore', 'pipe'], timeout: 15_000, killSignal: 'SIGKILL' },
);
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, 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;
}