Files
qinglong/packages/ql3-local-mcp-server/test/taskListProjection.test.cjs
T

163 lines
4.3 KiB
JavaScript

const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
BUILTIN_TASK_LIST_TOOL_DEFINITION,
BuiltInTaskListToolUnavailableError,
InvalidBuiltInTaskListToolError,
executeBuiltInTaskListTool,
} = require('../dist/tool-projection/taskList.js');
function definition(taskId, overrides = {}) {
return Object.freeze({
projectId: 'default',
taskId,
revision: 2,
mutationId: 'private-mutation',
name: `Task ${taskId}`,
description: 'private description',
kind: 'script',
spec: Object.freeze({
schema: 'qinglong/script@v1',
config: Object.freeze({ command: 'private command' }),
}),
labels: Object.freeze({ private: 'label' }),
enabled: true,
contentDigest: 'private-digest',
createdAtMs: 10,
updatedAtMs: 20,
...overrides,
});
}
test('defines an exact low-risk task.read Tool', () => {
assert.equal(BUILTIN_TASK_LIST_TOOL_DEFINITION.name, 'qinglong.task.list');
assert.equal(BUILTIN_TASK_LIST_TOOL_DEFINITION.version, '1.0.0');
assert.equal(BUILTIN_TASK_LIST_TOOL_DEFINITION.effect, 'read');
assert.equal(BUILTIN_TASK_LIST_TOOL_DEFINITION.risk, 'low');
assert.deepEqual(BUILTIN_TASK_LIST_TOOL_DEFINITION.requiredPermissions, [
'task.read',
]);
});
test('projects bounded current Tasks without private definition fields', async () => {
let captured;
const output = await executeBuiltInTaskListTool(
{
async listTaskDefinitions(query) {
captured = query;
return Object.freeze({
definitions: Object.freeze([definition('task-b')]),
truncated: true,
next: Object.freeze({ taskId: 'task-b' }),
});
},
},
'default',
{ after: { taskId: 'task-a' }, limit: 1 },
);
assert.deepEqual(captured, {
projectId: 'default',
limit: 1,
after: { taskId: 'task-a' },
});
assert.deepEqual(output, {
tasks: [
{
taskId: 'task-b',
revision: 2,
name: 'Task task-b',
kind: 'script',
specSchema: 'qinglong/script@v1',
enabled: true,
updatedAtMs: 20,
},
],
hasMore: true,
next: { taskId: 'task-b' },
});
const serialized = JSON.stringify(output);
for (const hidden of [
'private-mutation',
'private description',
'private command',
'private-digest',
'label',
]) {
assert.equal(serialized.includes(hidden), false);
}
});
test('defaults to 32 and returns no cursor for a complete page', async () => {
let captured;
const output = await executeBuiltInTaskListTool(
{
async listTaskDefinitions(query) {
captured = query;
return { definitions: [], truncated: false };
},
},
'default',
{},
);
assert.deepEqual(captured, { projectId: 'default', limit: 32 });
assert.deepEqual(output, { tasks: [], hasMore: false });
});
test('rejects invalid input before reading', async () => {
let reads = 0;
const source = {
async listTaskDefinitions() {
reads += 1;
return { definitions: [], truncated: false };
},
};
await assert.rejects(
executeBuiltInTaskListTool(source, 'default', { limit: 65 }),
InvalidBuiltInTaskListToolError,
);
await assert.rejects(
executeBuiltInTaskListTool(source, 'default', { after: { taskId: '' } }),
InvalidBuiltInTaskListToolError,
);
assert.equal(reads, 0);
});
test('fails closed on cross-Project, unordered, oversized or inconsistent pages', async () => {
for (const page of [
{
definitions: [definition('task-a', { projectId: 'other' })],
truncated: false,
},
{
definitions: [definition('task-b'), definition('task-a')],
truncated: false,
},
{
definitions: [definition('task-a'), definition('task-b')],
truncated: false,
},
{ definitions: [definition('task-a')], truncated: true },
{
definitions: [definition('task-a')],
truncated: true,
next: { taskId: 'other' },
},
]) {
await assert.rejects(
executeBuiltInTaskListTool(
{
async listTaskDefinitions() {
return page;
},
},
'default',
page.definitions.length === 2 && page.definitions[0].taskId === 'task-a'
? { limit: 1 }
: {},
),
BuiltInTaskListToolUnavailableError,
);
}
});