blob: cc874e0a5f7d6057f7e52a8d24faec0a34a500d2 [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 test from 'node:test';
import {
updateToolActivity,
latestToolActivity,
toolActivitySummary,
} from '../../products/maka/terminal-shell/app/toolActivity.ts';
import {
mergeChunks,
replayStartCursor,
updateHistoryMetadata,
} from '../../products/maka/terminal-shell/app/history.ts';
import { thinkingSummary } from '../../products/maka/terminal-shell/app/thinkingSummary.ts';
test('tool summaries use UI-rendered labels while retaining the tool source details', () => {
const tool = {
phase: 'preparing',
name: '搜索源代码',
title: { fallback: 'Opaque fallback' },
detail: ' 搜索\n 原文😀 ',
};
assert.equal(
toolActivitySummary(tool, '正在准备工具', '查找定义'),
'正在准备工具 · 搜索源代码 · 查找定义 · 搜索 原文😀',
);
assert.equal(
toolActivitySummary({ ...tool, phase: 'running' }, 'Running tool'),
'Running tool · 搜索源代码 · 搜索 原文😀',
);
});
test('thinking summary retains the last nonempty paragraph and its Unicode text', () => {
for (const [source, expected] of [
['', ''],
[' \r\n\t\u2003', ''],
['**first**\nline', 'first line'],
['old\n\n**最新**\n 行\r\n \t', '最新 行'],
['old\r\n\t \r\n**最新**\r\n\t\r\n \t', '最新'],
['old\n\n**\n\n', ''],
['old\n\r \nnew', 'old new'],
['earlier\n\n\n **tail**\n\n\n', 'tail'],
['earlier\n\n'.repeat(20000) + '**尾 e\u0301**\nline', '尾 e\u0301 line'],
]) {
assert.equal(thinkingSummary(source), expected);
}
});
test('unchanged history pages retain rows while fragments and fenced metadata still advance', () => {
const first = {
position: { sourceSequence: 10, itemIndex: 0 },
messageId: 'message',
turnId: 'turn',
timestamp: 100,
field: 'body',
role: 'user',
offset: 0,
text: '界',
totalBytes: 7,
};
const initial = mergeChunks([], [first], false, 20);
assert.equal(initial[0].text, '界');
assert.strictEqual(mergeChunks(initial, [], false, 99), initial);
assert.strictEqual(mergeChunks(initial, [structuredClone(first)], false, 20), initial);
assert.strictEqual(mergeChunks(initial, [{ ...first, text: 'old' }], true, 19), initial);
const extended = mergeChunks(initial, [{ ...first, offset: 3, text: '🙂' }], false, 21);
assert.equal(extended[0].text, '界🙂');
assert.deepEqual(extended[0].fragments, [
{ offset: 0, text: '界' },
{ offset: 3, text: '🙂' },
]);
assert.equal(extended[0].metadataThrough, 21);
const refreshed = {
...first,
text: 'metadata pages must not replace loaded text',
attachments: [{ name: 'notes.txt', mimeType: 'text/plain', bytes: 12, kind: 'doc' }],
};
const reply = {
...first,
position: { sourceSequence: 11, itemIndex: 0 },
messageId: 'reply',
role: 'assistant',
text: 'reply',
totalBytes: 5,
};
const withReply = mergeChunks(extended, [reply], false, 21);
const completed = {
...reply,
timing: { startedAt: 100, finished: { at: 200, outcome: 'completed' } },
model: { kind: 'native', requestedModelId: 'requested', reportedModelId: 'reported' },
finalOutput: true,
};
const updated = updateHistoryMetadata(withReply, [refreshed, completed], 22);
assert.equal(updated[0].text, '界🙂');
assert.strictEqual(updated[0].fragments, extended[0].fragments);
assert.deepEqual(updated[0].attachments, [
{ name: 'notes.txt', mimeType: 'text/plain', bytes: 12, kind: 'doc' },
]);
assert.deepEqual(updated[1].timing, {
startedAt: 100,
finished: { at: 200, outcome: 'completed' },
});
assert.deepEqual(updated[1].model, {
kind: 'native',
requestedModelId: 'requested',
reportedModelId: 'reported',
});
assert.equal(updated[1].finalOutput, true);
assert.equal(updated[0].metadataThrough, 22);
assert.strictEqual(updateHistoryMetadata(updated, [], 99), updated);
assert.strictEqual(
updateHistoryMetadata(updated, structuredClone([refreshed, completed]), 22),
updated,
);
assert.strictEqual(updateHistoryMetadata(updated, [{ ...first, attachments: [] }], 21), updated);
const advanced = updateHistoryMetadata(updated, [refreshed, completed], 23);
assert.equal(advanced[0].metadataThrough, 23);
assert.equal(advanced[0].text, '界🙂');
});
test('complete active tool identities survive 129 starts, updates and exact terminal removal', () => {
let active = [];
const event = (index, fact, invocationId = 'invocation') => ({
invocationId,
turnId: 'turn',
sequence: index + 1,
fact,
});
const id = (index) => 'tool_' + index.toString(16).padStart(64, '0');
for (let index = 0; index < 129; index++) {
active = updateToolActivity(
active,
event(index, {
kind: 'executor_tool_start',
toolUseId: id(index),
name: 'echo',
}),
);
}
assert.deepEqual(
active.map((tool) => tool.toolUseId),
Array.from({ length: 129 }, (_, index) => id(index)),
);
assert.equal(latestToolActivity(active, 'invocation', 'turn').toolUseId, id(128));
active = updateToolActivity(
active,
event(130, {
kind: 'executor_tool_progress',
toolUseId: id(128),
text: 'original progress',
}),
);
assert.equal(active.length, 129);
assert.equal(active[128].detail, 'original progress');
assert.equal(active[128].startSequence, 129);
active = updateToolActivity(
active,
event(131, { kind: 'executor_tool_start', toolUseId: id(0), name: 'independent' }, 'other'),
);
assert.equal(active.length, 130);
active = updateToolActivity(
active,
event(132, { kind: 'executor_tool_result', toolUseId: id(128) }),
);
assert.equal(active.length, 129);
assert.equal(latestToolActivity(active, 'invocation', 'turn').toolUseId, id(127));
assert.equal(latestToolActivity(active, 'other', 'turn').name, 'independent');
active = updateToolActivity(active, event(133, { kind: 'ended' }));
assert.deepEqual(
active.map((tool) => [tool.invocationId, tool.toolUseId, tool.name]),
[['other', id(0), 'independent']],
);
});
test('replay cursor preserves the earliest complete part and tool seed without argument limits', () => {
assert.equal(replayStartCursor(9, [], []), 9);
assert.equal(replayStartCursor(2, [{ startSequence: 8 }], [{ startSequence: 7 }]), 2);
assert.equal(
replayStartCursor(100, [{ startSequence: 9 }, { startSequence: 3 }], [{ startSequence: 8 }]),
2,
);
assert.equal(
replayStartCursor(100, [{ startSequence: 9 }], [{ startSequence: 8 }, { startSequence: 2 }]),
1,
);
const parts = Array.from({ length: 200000 }, (_, index) => ({
startSequence: index + 100,
}));
const tools = Array.from({ length: 200000 }, (_, index) => ({
startSequence: index + 200,
}));
parts[parts.length - 1] = { startSequence: 3 };
assert.equal(replayStartCursor(900000, parts, tools), 2);
tools[tools.length - 1] = { startSequence: 1 };
assert.equal(replayStartCursor(900000, parts, tools), 0);
});