Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ jobs:
if: needs.plan.outputs.code == 'true' || needs.plan.outputs.astryx_surface == 'true' || needs.plan.outputs.asf_source == 'true' || needs.plan.outputs.cli_package == 'true' || needs.plan.outputs.release_contract == 'true'
run: npm ci

- name: Check localized TUI copy boundaries
if: needs.plan.outputs.code == 'true'
run: |
npm run check:tui-copy
node --test scripts/check-tui-copy.test.mjs

# The header audit above remains install-free. The complete source gate
# also exercises generation and therefore runs after its pinned formatter
# dependency is installed, matching the source-candidate workflow.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"release:cli:qualify-state-root": "node scripts/qualify-released-cli-state-root.mjs",
"release:cli:eval": "node scripts/release-cli-eval-package.mjs",
"check:app-shell-hooks": "node scripts/check-app-shell-hooks.mjs",
"check:tui-copy": "node scripts/check-tui-copy.mjs",
"check:asf-headers": "node scripts/asf-license-headers.mjs check",
"write:asf-headers": "node scripts/asf-license-headers.mjs write",
"release:asf:source": "node scripts/asf-source-release.mjs create",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/__tests__/pi-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('Maka Pi TUI transcript', () => {
state.steering = ['s'.repeat(250)];
state.followup = ['f'.repeat(250)];

const lines = renderMakaPiPendingQueue(state, 400);
const lines = renderMakaPiPendingQueue(state, 400, process.platform, 'en');
for (const line of lines) {
assert.doesNotMatch(line, /[\r\n]/, `pending-queue row must be a single row: ${line}`);
}
Expand All @@ -150,7 +150,7 @@ describe('Maka Pi TUI transcript', () => {
const state = createMakaPiTranscriptState();
state.steering = ['Keep going'];
const renderFor = (platform: NodeJS.Platform) =>
renderMakaPiPendingQueue(state, 80, platform).map(stripAnsi);
renderMakaPiPendingQueue(state, 80, platform, 'en').map(stripAnsi);

assert.equal(renderFor('darwin').at(-1), '⌥+↑ take queued messages back to re-edit');
assert.equal(renderFor('linux').at(-1), 'Alt+↑ take queued messages back to re-edit');
Expand Down
265 changes: 86 additions & 179 deletions packages/cli/src/__tests__/pi-tui-mcp-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,119 +251,75 @@ describe('MCP management overlay', () => {
assert.match(text, /› ○ s4/u);
});

const editorCopy = {
en: {
add: 'Add MCP server',
transport: 'Transport',
protocol: 'Protocol preference',
confirmAdd: 'Add this MCP server?',
confirmRemove: 'Remove filesystem?',
confirmHint: 'y Confirm · Esc cancel',
serverId: 'Server ID',
command: 'Command',
args: 'Arguments',
argsHint: 'JSON string array, optional',
cwd: 'Working directory',
optionalHint: 'optional',
env: 'Environment',
mapHint: 'JSON string map, optional',
submitHint: 'Enter submit · Esc back',
testing: 'Testing MCP server…',
reconnecting: 'Reconnecting MCP server…',
applying: 'Applying MCP configuration…',
published: 'Configuration saved and tools refreshed.',
managerFailed: 'The MCP connection action failed.',
},
zh: {
add: '添加 MCP 服务器',
transport: '传输方式',
protocol: '协议偏好',
confirmAdd: '添加该 MCP 服务器?',
confirmRemove: '删除 filesystem?',
confirmHint: 'y 确认 · Esc 取消',
serverId: '服务器 ID',
command: '命令',
args: '参数',
argsHint: 'JSON string array, 可留空',
cwd: '工作目录',
optionalHint: '可留空',
env: '环境变量',
mapHint: 'JSON string map, 可留空',
submitHint: 'Enter 提交 · Esc 返回',
testing: '正在测试 MCP 服务器…',
reconnecting: '正在重连 MCP 服务器…',
applying: '正在应用 MCP 配置…',
published: '配置已保存,工具已刷新。',
managerFailed: 'MCP 连接操作失败。',
},
} as const;

for (const locale of ['en', 'zh'] as const) {
const expected = editorCopy[locale];

test(`walks the guided add flow with ${locale} headings, labels, and hints`, () => {
const overlay = new McpManagementOverlay({
locale,
tui: fakeTui(),
surface: surface(listSnapshot()),
viewportRows: () => 14,
onClose: () => undefined,
onChange: () => undefined,
});
const rendered = () => overlay.render(100).map(stripAnsi).join('\n');

overlay.handleInput('a');
assert.ok(rendered().includes(expected.add));
overlay.handleInput('g');
let text = rendered();
assert.ok(text.includes(expected.serverId));
assert.ok(text.includes(expected.submitHint));
for (const char of 'demo') overlay.handleInput(char);
overlay.handleInput('\r');
assert.ok(rendered().includes(expected.transport));
overlay.handleInput('1');
text = rendered();
assert.ok(text.includes(expected.command));
for (const char of 'echo') overlay.handleInput(char);
overlay.handleInput('\r');
text = rendered();
assert.ok(text.includes(expected.args));
assert.ok(text.includes(expected.argsHint));
overlay.handleInput('\r');
assert.ok(rendered().includes(expected.protocol));
overlay.handleInput('2');
text = rendered();
assert.ok(text.includes(expected.cwd));
assert.ok(text.includes(expected.optionalHint));
overlay.handleInput('\r');
text = rendered();
assert.ok(text.includes(expected.env));
assert.ok(text.includes(expected.mapHint));
overlay.handleInput('\r');
text = rendered();
assert.ok(text.includes(expected.confirmAdd));
assert.ok(text.includes('demo · stdio · auto'));
assert.ok(text.includes('echo'));
assert.ok(text.includes(expected.confirmHint));
test('routes the guided add flow through catalog headings, labels, and hints', () => {
const locale = 'en';
const expected = TUI_COPY_RESOURCES['mcp-status'][locale].editor;
const overlay = new McpManagementOverlay({
locale,
tui: fakeTui(),
surface: surface(listSnapshot()),
viewportRows: () => 14,
onClose: () => undefined,
onChange: () => undefined,
});
const rendered = () => overlay.render(100).map(stripAnsi).join('\n');

overlay.handleInput('a');
assert.ok(rendered().includes(expected.addTitle));
overlay.handleInput('g');
let text = rendered();
assert.ok(text.includes(expected.inputLabels.server_id));
assert.ok(text.includes(expected.hints.submit));
for (const char of 'demo') overlay.handleInput(char);
overlay.handleInput('\r');
assert.ok(rendered().includes(expected.transportTitle));
overlay.handleInput('1');
text = rendered();
assert.ok(text.includes(expected.inputLabels.command));
for (const char of 'echo') overlay.handleInput(char);
overlay.handleInput('\r');
text = rendered();
assert.ok(text.includes(expected.inputLabels.args));
assert.ok(text.includes(expected.hints.args));
overlay.handleInput('\r');
assert.ok(rendered().includes(expected.protocolTitle));
overlay.handleInput('2');
text = rendered();
assert.ok(text.includes(expected.inputLabels.cwd));
assert.ok(text.includes(expected.hints.optional));
overlay.handleInput('\r');
text = rendered();
assert.ok(text.includes(expected.inputLabels.env));
assert.ok(text.includes(expected.hints.map));
overlay.handleInput('\r');
text = rendered();
assert.ok(text.includes(expected.confirmAddTitle));
assert.ok(text.includes('demo · stdio · auto'));
assert.ok(text.includes('echo'));
assert.ok(text.includes(expected.confirmHint));
});

test(`renders the ${locale} remove confirmation with the server id`, () => {
const overlay = new McpManagementOverlay({
locale,
surface: surface(listSnapshot()),
viewportRows: () => 8,
onClose: () => undefined,
onChange: () => undefined,
});
overlay.render(100);

overlay.handleInput('d');
const text = overlay.render(100).map(stripAnsi).join('\n');
assert.ok(text.includes(expected.confirmRemove));
assert.ok(text.includes(expected.confirmHint));
test('routes remove confirmation copy and interpolates the server id', () => {
const locale = 'en';
const expected = TUI_COPY_RESOURCES['mcp-status'][locale].editor;
const overlay = new McpManagementOverlay({
locale,
surface: surface(listSnapshot()),
viewportRows: () => 8,
onClose: () => undefined,
onChange: () => undefined,
});
overlay.render(100);

overlay.handleInput('d');
const text = overlay.render(100).map(stripAnsi).join('\n');
assert.ok(text.includes(expected.confirmRemoveTitle.replace('{serverId}', 'filesystem')));
assert.ok(text.includes(expected.confirmHint));
});

for (const locale of ['en', 'zh'] as const) {
test(`labels the busy phase per action kind in ${locale}`, () => {
const expected = TUI_COPY_RESOURCES['mcp-status'][locale].editor;
const mcp = surface(listSnapshot());
mcp.execute = () => new Promise(() => undefined);
const overlay = new McpManagementOverlay({
Expand All @@ -376,41 +332,20 @@ describe('MCP management overlay', () => {
overlay.render(100);

const labels: [string, string][] = [
['t', expected.testing],
['r', expected.reconnecting],
[' ', expected.applying],
['t', expected.action.test],
['r', expected.action.reconnect],
[' ', expected.action.apply],
];
for (const [key, label] of labels) {
overlay.handleInput(key);
assert.ok(overlay.render(100).map(stripAnsi).join('\n').includes(label));
overlay.handleInput('\u001b');
}
});

test(`localizes ${locale} action result notices`, async () => {
const mcp = surface(listSnapshot());
mcp.execute = async () => ({ status: 'applied', effect: 'published' });
const overlay = new McpManagementOverlay({
locale,
surface: mcp,
viewportRows: () => 8,
onClose: () => undefined,
onChange: () => undefined,
});
overlay.render(100);

overlay.handleInput(' ');
await new Promise<void>((resolve) => setImmediate(resolve));
assert.ok(overlay.render(100).map(stripAnsi).join('\n').includes(expected.published));

mcp.execute = async () => ({ status: 'failed', reason: 'manager-failed' });
overlay.handleInput('t');
await new Promise<void>((resolve) => setImmediate(resolve));
assert.ok(overlay.render(100).map(stripAnsi).join('\n').includes(expected.managerFailed));
});
}

const RESULT_CASES: readonly [TuiMcpActionResult, string][] = [
const resultCopy = TUI_COPY_RESOURCES['mcp-status'].en.editor.results;
const RESULT_CASES: readonly [TuiMcpActionResult, keyof typeof resultCopy][] = [
[{ status: 'conflict', reason: 'exists' }, 'exists'],
[{ status: 'conflict', reason: 'stale_config' }, 'stale_config'],
[{ status: 'conflict', reason: 'stale_edit' }, 'stale_edit'],
Expand All @@ -434,52 +369,24 @@ describe('MCP management overlay', () => {
[{ status: 'tested', test: testResult(true), effect: 'pending_host' }, 'test_pending_host'],
];

for (const locale of ['en', 'zh'] as const) {
test(`routes every action result to its ${locale} catalog notice`, async () => {
const results = TUI_COPY_RESOURCES['mcp-status'][locale].editor.results;
for (const [result, code] of RESULT_CASES) {
const mcp = surface(listSnapshot());
mcp.execute = async () => result;
const overlay = new McpManagementOverlay({
locale,
surface: mcp,
viewportRows: () => 8,
onClose: () => undefined,
onChange: () => undefined,
});
overlay.render(100);

overlay.handleInput(' ');
await new Promise<void>((resolve) => setImmediate(resolve));
const text = overlay.render(100).map(stripAnsi).join('\n');
assert.ok(text.includes(results[code as keyof typeof results]), `${code} (${locale})`);
}
});
}

test('renders an unknown runtime result code as the raw code', async () => {
const mcp = surface(listSnapshot());
mcp.execute = async () =>
({ status: 'failed', reason: 'future-code' }) as unknown as TuiMcpActionResult;
const overlay = new McpManagementOverlay({
locale: 'en',
surface: mcp,
viewportRows: () => 8,
onClose: () => undefined,
onChange: () => undefined,
});
overlay.render(100);

overlay.handleInput(' ');
await new Promise<void>((resolve) => setImmediate(resolve));
assert.ok(overlay.render(100).map(stripAnsi).join('\n').includes('future-code'));
});
test('routes every action result to its catalog notice', async () => {
for (const [result, code] of RESULT_CASES) {
const mcp = surface(listSnapshot());
mcp.execute = async () => result;
const overlay = new McpManagementOverlay({
locale: 'en',
surface: mcp,
viewportRows: () => 8,
onClose: () => undefined,
onChange: () => undefined,
});
overlay.render(100);

test('keeps punctuation-sensitive catalog strings byte-for-byte', () => {
const editor = (locale: 'en' | 'zh') => TUI_COPY_RESOURCES['mcp-status'][locale].editor;
assert.equal(editor('zh').results.stale_import, '导入项已变化,请重新预览。');
assert.equal(editor('en').confirmImportTitle, 'Import MCP servers?');
assert.equal(editor('zh').confirmImportTitle, '导入 MCP 服务器?');
overlay.handleInput(' ');
await new Promise<void>((resolve) => setImmediate(resolve));
const text = overlay.render(100).map(stripAnsi).join('\n');
assert.ok(text.includes(resultCopy[code]), code);
}
});
});

Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/__tests__/pi-tui-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,13 +396,15 @@ describe('Maka Pi TUI runner', () => {
// The driver's raw error text is not product copy; the TUI reports the
// failure with its own localized notice (#2672).
await waitFor(() =>
plainTerminalOutput(terminal.output()).includes('Could not start a new session'),
plainTerminalOutput(terminal.output()).includes('a local command could not be stopped'),
);

// Identity untouched…
assert.equal(driver.getSessionId(), 'session-1');
// …and the transcript was not wiped by the aborted /new.
assert.match(plainTerminalOutput(terminal.screenOutput()), /Could not start a new session/);
const screen = plainTerminalOutput(terminal.screenOutput());
assert.match(screen, /a local command could not be stopped/u);
assert.match(screen, /Press Ctrl\+C to stop it/u);
assert.doesNotMatch(plainTerminalOutput(terminal.screenOutput()), /host_draining/);
assert.ok(transcriptBefore.length > 0);

Expand Down
21 changes: 0 additions & 21 deletions packages/cli/src/__tests__/tui-copy-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,27 +89,6 @@ describe('TUI copy resources', () => {
assert.equal(formatUiMessage(template, { count: 2 }, 'en'), '2 tools');
});

test('localizes rewind and skill notices in both locales', () => {
assert.equal(TUI_COPY_RESOURCES.rewind.en.noTargets, 'No turns to rewind to.');
assert.equal(TUI_COPY_RESOURCES.rewind.zh.noTargets, '没有可回退的轮次。');
assert.equal(
formatUiMessage(
TUI_COPY_RESOURCES.skills.en.failedItem,
{ request: 'nope', reason: TUI_COPY_RESOURCES.skills.en.failureReasons.not_found },
'en',
),
'/skill:nope (not found)',
);
assert.equal(
formatUiMessage(
TUI_COPY_RESOURCES.skills.zh.failedItem,
{ request: 'nope', reason: TUI_COPY_RESOURCES.skills.zh.failureReasons.not_found },
'zh',
),
'/skill:nope(未找到)',
);
});

test('localizes stable onboarding failure codes at the TUI boundary', () => {
assert.equal(
onboardingFailureMessage({ kind: 'rejected', reason: 'connection_not_found' }, 'en'),
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/pi-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1889,8 +1889,8 @@ const TUI_PENDING_QUEUE_COPY = resolveUiMessageCatalog(
export function renderMakaPiPendingQueue(
state: MakaPiTranscriptState,
width: number,
platform: NodeJS.Platform = process.platform,
locale: UiLocale = 'en',
platform: NodeJS.Platform,
locale: UiLocale,
): string[] {
if (state.steering.length === 0 && state.followup.length === 0) {
return [];
Expand Down
Loading