blob: 5061e9294c9936e6ba8be60af7aced41e4690237 [file]
import { createTestToolRuntime } from './execution-boundary-test-helpers.js';
import { describe, test } from 'node:test';
import type { LlmConnection, SessionHeader } from '@maka/core';
import type { SessionEvent } from '@maka/core';
import {
deriveToolArtifactCandidates,
extractStdoutRedirectPath,
recordToolArtifactsSafely,
} from '../tool-artifacts.js';
import { ToolRuntime, type MakaTool, type ToolRuntimeInput } from '../tool-runtime.js';
import { expect } from '../test-helpers.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 },
});
expect(candidate).toEqual({
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 },
});
expect(candidate?.kind).toBe('diff');
expect(candidate?.name).toBe('main.ts.diff');
expect(candidate?.mimeType).toBe('text/x-diff');
expect(
typeof candidate?.content === 'string' && candidate.content.includes('-const a = 1;'),
).toBe(true);
expect(
typeof candidate?.content === 'string' && candidate.content.includes('+const a = 2;'),
).toBe(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' },
});
expect(candidate?.sourcePath).toBe('/workspace/maka/reports/build.log');
expect(candidate?.kind).toBe('file');
expect(
deriveToolArtifactCandidates({
toolName: 'Bash',
cwd: '/workspace/maka',
args: { command: 'echo "wrote reports/build.log"' },
result: { stdout: 'reports/build.log' },
}),
).toEqual([]);
});
test('extractStdoutRedirectPath ignores stderr and fd redirects', () => {
expect(extractStdoutRedirectPath('echo ok > out.txt')).toBe('out.txt');
expect(extractStdoutRedirectPath('echo ok >> ./out.txt')).toBe('./out.txt');
expect(extractStdoutRedirectPath('echo ok 2> err.log')).toBe(null);
expect(extractStdoutRedirectPath('echo ok >&2')).toBe(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),
);
expect(warnings.length).toBe(1);
expect(warnings[0]?.includes('Artifact recorder skipped:')).toBe(true);
expect(warnings[0]?.includes('sk-secret-token-should-not-leak')).toBe(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),
]);
expect(outcome).toBe('done');
expect(calls.length).toBe(1);
expect(
events.some((event) => event.type === 'tool_result' && event.toolUseId === 'tool-1'),
).toBe(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',
appendMessage: async () => {},
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,
lastUsedAt: 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 nextId(): () => string {
let id = 0;
return () => `id-${++id}`;
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}