blob: c63c0bae8e76c601ff18c97badf2da347f7acb53 [file]
/*
* 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 { describe, test } from 'node:test';
import { TERMINAL_MOUSE_EVENTS } from '@maka/core/terminal-input';
import {
buildLocalForegroundBashTool,
buildManagedBashTool,
createWriteStdinSchemas,
shapeTerminalResult,
WRITE_STDIN_EXAMPLE_REF,
WRITE_STDIN_MINIMAL_EXAMPLES,
type ShellRunLauncher,
} from '../shell-tools.js';
import type { ShellPlan } from '../shell-detect.js';
import { ShellPreferenceError } from '../shell-detect.js';
const pwshPlan: ShellPlan = {
kind: 'pwsh',
displayName: 'PowerShell 7 (pwsh)',
exe: 'C:\\pf\\pwsh.exe',
};
test('an explicit Git Bash tool declares POSIX syntax', () => {
const tool = buildLocalForegroundBashTool({
shell: {
plan: {
kind: 'git-bash',
displayName: 'Git Bash',
exe: 'C:\\Program Files\\Git\\bin\\bash.exe',
},
},
});
assert.match(tool.description, /Git Bash/);
assert.match(tool.description, /POSIX shell syntax/);
assert.doesNotMatch(tool.description, /write PowerShell syntax/);
});
describe('Bash tool fails closed when the turn shell plan carries a setup error', () => {
const brokenShell = {
plan: { kind: 'cmd', displayName: 'cmd.exe' } as ShellPlan,
setupError: new ShellPreferenceError(
'executable_missing',
'The configured Git Bash was not found',
),
};
test('description declares the outage instead of dialect guidance', () => {
const tool = buildLocalForegroundBashTool({ shell: brokenShell });
assert.match(tool.description, /unavailable this turn/);
assert.doesNotMatch(tool.description, /write cmd syntax/);
});
test('local foreground execution throws the setup error before spawning', async () => {
const tool = buildLocalForegroundBashTool({ shell: brokenShell });
await assert.rejects(
async () => {
await tool.impl({ command: 'echo never-runs' }, fakeToolContext());
},
(error: unknown) => {
assert.ok(error instanceof ShellPreferenceError);
assert.equal(error.code, 'executable_missing');
return true;
},
);
});
test('managed execution throws before any shell-run reaches the host', async () => {
const controller: ShellRunLauncher = {
runForegroundBash: () => Promise.reject(new Error('must not be called')),
runBackgroundBash: () => Promise.reject(new Error('must not be called')),
};
const tool = buildManagedBashTool(controller, { shell: brokenShell });
await assert.rejects(async () => {
await tool.impl({ command: 'echo never-runs' }, fakeToolContext());
}, ShellPreferenceError);
await assert.rejects(async () => {
await tool.impl({ command: 'echo never-runs', run_in_background: true }, fakeToolContext());
}, ShellPreferenceError);
});
});
describe('Bash tool shell is threaded through to execution, not just the description', () => {
test('foreground tool executes with the same shell it declares', async () => {
// /bin/echo stands in for pwsh.exe: if the tool's shell reaches the
// spawn, stdout echoes the PowerShell flags and wrapper back. A shell that only
// reached the description would run via the default POSIX shell and
// print a bare 'wired-marker'.
const tool = buildLocalForegroundBashTool({
shell: { plan: { kind: 'pwsh', displayName: 'PowerShell 7 (pwsh)', exe: '/bin/echo' } },
});
const result = (await tool.impl({ command: 'echo wired-marker' }, fakeToolContext())) as {
output: { stdout: string };
};
assert.ok(
result.output.stdout.startsWith(
'-NoLogo -NoProfile -NonInteractive -Command $__makaUtf8 = [System.Text.UTF8Encoding]::new($false)\n',
),
`expected declared shell to execute, got: ${result.output.stdout}`,
);
});
test('background tool forwards its shell to the shell-run controller', async () => {
const captured: unknown[] = [];
const controller: ShellRunLauncher = {
runForegroundBash: () => Promise.reject(new Error('not used')),
runBackgroundBash: (input: unknown) => {
captured.push(input);
return Promise.resolve({
kind: 'shell_run',
ref: 'maka://runtime/background-tasks/sr_test',
mode: 'pipes',
status: 'running',
cwd: '.',
cmd: '',
startedAt: 1,
updatedAt: 1,
revision: 1,
});
},
};
const tool = buildManagedBashTool(controller, { shell: { plan: pwshPlan } });
await tool.impl({ command: 'echo hi', run_in_background: true }, fakeToolContext());
assert.deepEqual((captured[0] as { shell?: unknown }).shell, pwshPlan);
});
test('managed completion callback remains exactly-once when the launcher settles it', async () => {
let completionCount = 0;
const controller: ShellRunLauncher = {
async runForegroundBash(input) {
input.onCompletion?.({ successful: true });
return {
kind: 'terminal',
cwd: input.cwd,
cmd: input.command,
status: 'completed',
exitCode: 0,
output: {
mode: 'pipes',
stdout: '',
stderr: '',
stdoutTruncated: false,
stderrTruncated: false,
redacted: false,
},
};
},
runBackgroundBash: () => Promise.reject(new Error('not used')),
};
const tool = buildManagedBashTool(controller, {
transformCommand: ({ ctx }) => ({
cwd: ctx.cwd,
onCompletion: () => {
completionCount += 1;
},
}),
});
await tool.impl({ command: 'true' }, fakeToolContext());
assert.equal(completionCount, 1);
});
});
describe('Bash provider-facing result projection', () => {
test('managed Bash removes only the duplicated foreground command', async () => {
const tool = buildManagedBashTool(fakeShellRuns());
const terminal = {
kind: 'terminal',
cwd: '/workspace',
cmd: 'printf foreground',
status: 'completed',
exitCode: 0,
output: {
mode: 'pipes',
stdout: 'foreground',
stderr: '',
stdoutTruncated: false,
stderrTruncated: false,
redacted: false,
},
};
const background = {
kind: 'shell_run',
ref: 'maka://runtime/background-tasks/sr_test',
mode: 'pipes',
status: 'running',
cwd: '/workspace',
cmd: 'printf background',
startedAt: 1,
updatedAt: 1,
revision: 1,
};
assert.deepEqual(
await tool.toModelOutput?.({ toolCallId: 'foreground', input: {}, output: terminal }),
{
type: 'json',
value: {
kind: 'terminal',
cwd: '/workspace',
status: 'completed',
exitCode: 0,
output: terminal.output,
},
},
);
assert.deepEqual(
await tool.toModelOutput?.({ toolCallId: 'background', input: {}, output: background }),
{ type: 'json', value: background },
);
assert.equal(terminal.cmd, 'printf foreground');
});
});
describe('shapeTerminalResult projection', () => {
test('preserves foreground command output verbatim', () => {
const command = 'curl -H "Authorization: Bearer token-value" example.test';
const stdout = 'Authorization: Bearer token-value';
const stderr = 'api_key=token-value';
const result = shapeTerminalResult({
cwd: '/ws',
command,
result: { exitCode: 7, stdout, stderr },
});
assert.equal(result.cmd, command);
assert.ok(result.output.mode === 'pipes');
assert.equal(result.output.stdout, stdout);
assert.equal(result.output.stderr, stderr);
assert.equal(result.output.redacted, false);
});
test('surfaces sandboxDenial when a sandboxed command fails with a denial message', () => {
const result = shapeTerminalResult({
cwd: '/ws',
command: 'rm -rf /',
result: {
exitCode: 1,
stdout: '',
stderr: 'Operation not permitted',
sandboxed: true,
sandboxType: 'macos-seatbelt',
},
});
assert.deepEqual(result.sandboxDenial, {
likely: true,
backend: 'macos-seatbelt',
});
});
test('omits sandboxDenial when sandboxed is false', () => {
const result = shapeTerminalResult({
cwd: '/ws',
command: 'ls /no-such-dir',
result: {
exitCode: 1,
stdout: '',
stderr: 'Operation not permitted',
sandboxed: false,
},
});
assert.equal(result.sandboxDenial, undefined);
});
test('omits sandboxDenial when sandboxed field is absent (BoundedShellResult shape)', () => {
const result = shapeTerminalResult({
cwd: '/ws',
command: 'ls',
result: {
exitCode: 1,
stdout: '',
stderr: 'Operation not permitted',
stdoutTruncated: false,
stderrTruncated: false,
timedOut: false,
aborted: false,
},
});
assert.equal(result.sandboxDenial, undefined);
});
});
describe('WriteStdin provider/strict contract conformance', () => {
const { providerParameters, strictParameters } = createWriteStdinSchemas();
// Each example's first action, or undefined for a resize-only (action-less) example.
const firstActionOf = (payload: Readonly<Record<string, unknown>>) => {
const actions = payload.actions;
return Array.isArray(actions) ? (actions[0] as Record<string, unknown> | undefined) : undefined;
};
const acceptedByBothLayers = (
label: string,
payload: Readonly<Record<string, unknown>>,
): boolean => {
const provider = providerParameters.safeParse(payload);
assert.ok(
provider.success,
`provider schema rejected the documented "${label}" example: ${
provider.success ? '' : provider.error.message
}`,
);
const strict = strictParameters.safeParse(payload);
assert.ok(
strict.success,
`strict validator rejected the documented "${label}" example: ${
strict.success ? '' : strict.error.message
}`,
);
return provider.success && strict.success;
};
test('every documented minimal example is accepted by BOTH the provider and strict layers', () => {
for (const { label, payload } of WRITE_STDIN_MINIMAL_EXAMPLES) {
acceptedByBothLayers(label, payload);
}
});
test('every mouse event has a covering example that passes both layers', () => {
// Derives coverage from the source of truth: adding a new mouse event to
// TERMINAL_MOUSE_EVENTS fails here until a minimal example is pinned for it.
for (const event of TERMINAL_MOUSE_EVENTS) {
const covering = WRITE_STDIN_MINIMAL_EXAMPLES.filter(({ payload }) => {
const action = firstActionOf(payload);
return action?.type === 'mouse' && action?.event === event;
});
assert.ok(
covering.length >= 1,
`no minimal WriteStdin example covers the '${event}' mouse event`,
);
for (const { label, payload } of covering) {
acceptedByBothLayers(label, payload);
}
}
});
test('every action type has a covering example', () => {
// Same drift-proofing for the action `type` enum itself.
const actionTypes = ['text', 'key', 'mouse'] as const;
for (const type of actionTypes) {
const covered = WRITE_STDIN_MINIMAL_EXAMPLES.some(
({ payload }) => firstActionOf(payload)?.type === type,
);
assert.ok(covered, `no minimal WriteStdin example covers the '${type}' action type`);
}
});
test('provider null/0/empty placeholders are tolerated and normalized away by the strict layer', () => {
// A provider that fills every optional field with a null/0/'' placeholder
// rather than omitting it must still round-trip to the minimal legal action.
const withPlaceholders = {
ref: WRITE_STDIN_EXAMPLE_REF,
actions: [
{
type: 'key',
key: 'enter',
text: '',
event: null,
x: 0,
y: 0,
button: null,
direction: null,
modifiers: [],
},
],
size: null,
};
const strict = strictParameters.safeParse(withPlaceholders);
assert.ok(
strict.success,
`strict validator should normalize provider placeholders, got: ${
strict.success ? '' : strict.error.message
}`,
);
assert.deepEqual(strict.data.actions, [{ type: 'key', key: 'enter' }]);
});
test('strict validation is not vacuous: contract violations are rejected', () => {
// Mouse click without a button is structurally invalid.
assert.equal(
strictParameters.safeParse({
ref: WRITE_STDIN_EXAMPLE_REF,
actions: [{ type: 'mouse', event: 'click', x: 0, y: 0 }],
}).success,
false,
);
// A non-canonical ref must be refused even when the actions are legal.
assert.equal(
strictParameters.safeParse({
ref: 'not-a-runtime-ref',
actions: [{ type: 'key', key: 'enter' }],
}).success,
false,
);
// input and actions are mutually exclusive.
assert.equal(
strictParameters.safeParse({
ref: WRITE_STDIN_EXAMPLE_REF,
input: 'x',
actions: [{ type: 'key', key: 'enter' }],
}).success,
false,
);
});
});
function fakeToolContext() {
return {
sessionId: 'session-1',
turnId: 'turn-1',
toolCallId: 'tool-1',
cwd: process.cwd(),
abortSignal: new AbortController().signal,
emitOutput: () => {},
};
}
function fakeShellRuns() {
return {
runForegroundBash: () => Promise.reject(new Error('not used')),
runBackgroundBash: () => Promise.reject(new Error('not used')),
};
}