Files
qinglong/packages/ql3-runtime-core/test/boundedTaskListProjection.test.cjs

191 lines
5.4 KiB
JavaScript

const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const { test } = require('node:test');
const {
BoundedTaskListProjectionUnavailableError,
DEFAULT_BOUNDED_TASK_LIST_LIMIT,
InvalidBoundedTaskListProjectionError,
MAX_BOUNDED_TASK_LIST_LIMIT,
executeBoundedTaskListProjection,
} = require('../dist/task-definition/projection/boundedTaskListProjection.js');
function task(taskId, overrides = {}) {
return Object.freeze({
projectId: 'prj_default',
taskId,
revision: 2,
name: `Task ${taskId}`,
description: 'must-not-cross-projection',
kind: 'command',
spec: Object.freeze({
schema: 'qinglong/command@v1',
config: Object.freeze({ command: ['private'] }),
}),
labels: Object.freeze({ private: 'value' }),
enabled: true,
mutationId: 'mutation-private',
contentDigest: 'digest-private',
createdAtMs: 10,
updatedAtMs: 20,
...overrides,
});
}
test('projects one bounded ascending current-head page and stable cursor', async () => {
const calls = [];
const result = await executeBoundedTaskListProjection(
{
async listTaskDefinitions(query) {
calls.push(query);
return {
definitions: [
task('task-a'),
task('task-b', { enabled: false }),
],
truncated: true,
next: { taskId: 'task-b' },
};
},
},
'prj_default',
{ limit: 2 },
);
assert.deepEqual(calls, [{ projectId: 'prj_default', limit: 2 }]);
assert.deepEqual(result, {
tasks: [
{
taskId: 'task-a',
revision: 2,
name: 'Task task-a',
kind: 'command',
specSchema: 'qinglong/command@v1',
enabled: true,
updatedAtMs: 20,
},
{
taskId: 'task-b',
revision: 2,
name: 'Task task-b',
kind: 'command',
specSchema: 'qinglong/command@v1',
enabled: false,
updatedAtMs: 20,
},
],
hasMore: true,
next: { taskId: 'task-b' },
});
assert.equal(JSON.stringify(result).includes('private'), false);
});
test('uses default and maximum bounds and passes an exact cursor', async () => {
const seen = [];
const source = {
async listTaskDefinitions(query) {
seen.push(query);
return { definitions: [], truncated: false };
},
};
assert.deepEqual(
await executeBoundedTaskListProjection(source, 'prj_default', {}),
{ tasks: [], hasMore: false },
);
assert.deepEqual(
await executeBoundedTaskListProjection(source, 'prj_default', {
limit: MAX_BOUNDED_TASK_LIST_LIMIT,
after: { taskId: 'task-a' },
}),
{ tasks: [], hasMore: false },
);
assert.deepEqual(seen, [
{ projectId: 'prj_default', limit: DEFAULT_BOUNDED_TASK_LIST_LIMIT },
{
projectId: 'prj_default',
limit: MAX_BOUNDED_TASK_LIST_LIMIT,
after: { taskId: 'task-a' },
},
]);
});
test('fails closed on malformed input before storage', async () => {
let calls = 0;
const source = {
async listTaskDefinitions() {
calls += 1;
return { definitions: [], truncated: false };
},
};
for (const input of [
null,
{ limit: 0 },
{ limit: 65 },
{ extra: true },
{ after: null },
{ after: { taskId: '' } },
{ after: { taskId: 'task-a', extra: true } },
]) {
await assert.rejects(
executeBoundedTaskListProjection(source, 'prj_default', input),
InvalidBoundedTaskListProjectionError,
);
}
assert.equal(calls, 0);
});
test('fails closed on cross-Project, ordering, shape and continuation drift', async () => {
for (const page of [
{ definitions: [task('task-a', { projectId: 'prj_other' })], truncated: false },
{ definitions: [task('task-b'), task('task-a')], truncated: false },
{ definitions: [task('task-a'), task('task-a')], truncated: false },
{ definitions: [task('task-a', { kind: 'invented' })], truncated: false },
{ definitions: [task('task-a')], truncated: true },
{
definitions: [task('task-a')],
truncated: true,
next: { taskId: 'task-b' },
},
{ definitions: [], truncated: true, next: { taskId: 'task-a' } },
{ definitions: [task('task-a'), task('task-b')], truncated: false },
]) {
await assert.rejects(
executeBoundedTaskListProjection(
{ async listTaskDefinitions() { return page; } },
'prj_default',
page.definitions.length > 1 ? { limit: 1 } : {},
),
BoundedTaskListProjectionUnavailableError,
);
}
await assert.rejects(
executeBoundedTaskListProjection(
{ async listTaskDefinitions() { throw new Error('offline'); } },
'prj_default',
{},
),
BoundedTaskListProjectionUnavailableError,
);
});
test('leaf import does not load Tool Registry or SemVer', () => {
const entry = path.resolve(
__dirname,
'../dist/task-definition/projection/boundedTaskListProjection.js',
);
const result = spawnSync(
process.execPath,
[
'-e',
`require(${JSON.stringify(entry)});
const loaded = Object.keys(require.cache);
if (loaded.some((value) => /node_modules[\\/]semver(?:[\\/]|$)/.test(value))) process.exit(2);
if (loaded.some((value) => /tool-execution[\\/]tool-registry/.test(value))) process.exit(3);
process.stdout.write(String(loaded.length));`,
],
{ encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
assert.ok(Number(result.stdout) <= 4);
});