blob: 4a8828f8c30b0646844dae8c865fc554b524a233 [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 { nextId } from '@maka/core/test-only/async-primitives';
import assert from 'node:assert/strict';
import { createTestToolRuntime } from './execution-boundary-test-helpers.js';
import { describe, test } from 'node:test';
import type { LlmConnection } from '@maka/core/llm-connections';
import type { SessionHeader } from '@maka/core/session';
import type { SessionEvent } from '@maka/core/events';
import {
deriveToolArtifactCandidates,
extractStdoutRedirectPath,
recordToolArtifactsSafely,
} from '../tool-artifacts.js';
import { ToolRuntime, type MakaTool, type ToolRuntimeInput } from '../tool-runtime.js';
describe('deriveToolArtifactCandidates', () => {
test('Write derives a file-backed candidate from structured result path', () => {
const [candidate] = deriveToolArtifactCandidates({
toolName: 'Write',
cwd: '/workspace/maka',
args: { path: 'docs/report.html', content: '<h1>Report</h1>' },
result: { ok: true, path: '/workspace/maka/docs/report.html', bytes: 15 },
});
assert.deepStrictEqual(candidate, {
kind: 'html',
name: 'report.html',
mimeType: 'text/html',
source: 'tool_result',
summary: 'Write tool output',
sourcePath: '/workspace/maka/docs/report.html',
});
});
test('Edit derives a diff candidate from structured edit args', () => {
const [candidate] = deriveToolArtifactCandidates({
toolName: 'Edit',
cwd: '/workspace/maka',
args: { path: 'src/main.ts', old_string: 'const a = 1;', new_string: 'const a = 2;' },
result: { ok: true, path: '/workspace/maka/src/main.ts', replacements: 1 },
});
assert.strictEqual(candidate?.kind, 'diff');
assert.strictEqual(candidate?.name, 'main.ts.diff');
assert.strictEqual(candidate?.mimeType, 'text/x-diff');
assert.strictEqual(
typeof candidate?.content === 'string' && candidate.content.includes('-const a = 1;'),
true,
);
assert.strictEqual(
typeof candidate?.content === 'string' && candidate.content.includes('+const a = 2;'),
true,
);
});
test('Bash derives only explicit stdout redirects and does not scan stdout/stderr text', () => {
const [candidate] = deriveToolArtifactCandidates({
toolName: 'Bash',
cwd: '/workspace/maka',
args: { command: 'npm run build > "reports/build.log" 2>&1' },
result: { stdout: 'wrote /tmp/guessed.html', stderr: 'see report.pdf' },
});
assert.strictEqual(candidate?.sourcePath, '/workspace/maka/reports/build.log');
assert.strictEqual(candidate?.kind, 'file');
assert.deepStrictEqual(
deriveToolArtifactCandidates({
toolName: 'Bash',
cwd: '/workspace/maka',
args: { command: 'echo "wrote reports/build.log"' },
result: { stdout: 'reports/build.log' },
}),
[],
);
});
test('extractStdoutRedirectPath ignores stderr and fd redirects', () => {
assert.strictEqual(extractStdoutRedirectPath('echo ok > out.txt'), 'out.txt');
assert.strictEqual(extractStdoutRedirectPath('echo ok >> ./out.txt'), './out.txt');
assert.strictEqual(extractStdoutRedirectPath('echo ok 2> err.log'), null);
assert.strictEqual(extractStdoutRedirectPath('echo ok >&2'), null);
});
});
describe('recordToolArtifactsSafely', () => {
test('recorder failure emits a generalized warning and never throws', async () => {
const warnings: string[] = [];
await recordToolArtifactsSafely(
{
sessionId: 'session-1',
turnId: 'turn-1',
toolUseId: 'tool-1',
toolName: 'Write',
cwd: '/workspace/maka',
args: { path: 'secret.txt' },
result: { ok: true, path: '/workspace/maka/secret.txt' },
},
async () => {
throw new Error('EACCES: sk-secret-token-should-not-leak');
},
(message) => warnings.push(message),
);
assert.strictEqual(warnings.length, 1);
assert.strictEqual(warnings[0]?.includes('Artifact recorder skipped:'), true);
assert.strictEqual(warnings[0]?.includes('sk-secret-token-should-not-leak'), false);
});
});
describe('ToolRuntime artifact recorder scheduling', () => {
test('ordinary tool results do not wait for a slow artifact recorder', async () => {
const calls: unknown[] = [];
const { runtime, events } = makeToolRuntime({
recordToolArtifacts: (input) => {
calls.push(input);
return new Promise(() => {});
},
});
const outcome = await Promise.race([
runtime
.settleToolCall({
tool: writeArtifactTool(),
turnId: 'turn-1',
toolCallId: 'tool-1',
input: { path: 'notes.md', content: 'hello' },
abortSignal: new AbortController().signal,
eventSink: {
push: (event) => events.push(event),
pushAndWaitUntilConsumed: async (event) => {
events.push(event);
},
},
})
.then(() => 'done' as const),
delay(20).then(() => 'timeout' as const),
]);
assert.strictEqual(outcome, 'done');
assert.strictEqual(calls.length, 1);
assert.strictEqual(
events.some((event) => event.type === 'tool_result' && event.toolUseId === 'tool-1'),
true,
);
});
});
function makeToolRuntime(overrides: Partial<ToolRuntimeInput> = {}): {
runtime: ToolRuntime;
events: SessionEvent[];
} {
const events: SessionEvent[] = [];
const runtime = createTestToolRuntime({
sessionId: 'session-1',
header: testHeader(),
connection: testConnection(),
modelId: 'mock-model',
newId: nextId(),
now: () => 1,
getPermissionPauseTarget: () => null,
...overrides,
});
return { runtime, events };
}
function writeArtifactTool(): MakaTool {
return {
name: 'Write',
description: 'write file',
parameters: {},
impl: async (args) => {
const path =
typeof (args as { path?: unknown }).path === 'string'
? (args as { path: string }).path
: 'notes.md';
return { ok: true, path: `/workspace/maka/${path}`, bytes: 5 };
},
};
}
function testHeader(): SessionHeader {
return {
id: 'session-1',
workspaceRoot: '/workspace/maka',
cwd: '/workspace/maka',
createdAt: 1,
name: 'Test',
titleIsManual: true,
isFlagged: false,
labels: [],
isArchived: false,
status: 'active',
statusUpdatedAt: 1,
hasUnread: false,
backend: 'ai-sdk',
llmConnectionSlug: 'anthropic-main',
connectionLocked: true,
model: 'mock-model',
permissionMode: 'ask',
schemaVersion: 1,
};
}
function testConnection(): LlmConnection {
return {
slug: 'anthropic-main',
name: 'Anthropic',
providerType: 'anthropic',
defaultModel: 'mock-model',
enabled: true,
createdAt: 1,
updatedAt: 1,
};
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}