feat(ql3): bridge legacy panel to bounded run logs

This commit is contained in:
whyour
2026-09-04 01:55:44 +08:00
parent fa730da091
commit 09ef174522
11 changed files with 328 additions and 53 deletions
@@ -28,6 +28,7 @@ export interface BoundedRunListItem {
readonly id: string;
readonly taskId: string;
readonly taskRevision: string;
readonly triggerId?: string;
readonly status: RunStatus;
readonly version: number;
readonly eventSequence: number;
@@ -73,7 +74,11 @@ function boundedText(value: unknown, maximum: number): value is string {
);
}
function integer(value: unknown, minimum: number, maximum: number): value is number {
function integer(
value: unknown,
minimum: number,
maximum: number,
): value is number {
return (
Number.isSafeInteger(value) &&
Number(value) >= minimum &&
@@ -89,7 +94,11 @@ function cursor(value: unknown): value is Readonly<ProjectRunListCursor> {
Reflect.ownKeys(value).length === 2 &&
Object.hasOwn(value, 'createdAtMs') &&
Object.hasOwn(value, 'runId') &&
integer((value as ProjectRunListCursor).createdAtMs, 0, Number.MAX_SAFE_INTEGER) &&
integer(
(value as ProjectRunListCursor).createdAtMs,
0,
Number.MAX_SAFE_INTEGER,
) &&
boundedText((value as ProjectRunListCursor).runId, 128)
);
}
@@ -153,6 +162,7 @@ function projectRun(
(boundary !== undefined && !isBefore(rowCursor, boundary)) ||
!boundedText(value.taskId, 255) ||
!boundedText(value.taskRevision, 255) ||
(value.triggerId !== undefined && !boundedText(value.triggerId, 128)) ||
!RUN_STATUSES.includes(value.status) ||
!EXECUTION_ORIGINS.includes(value.executionOrigin) ||
(value.executionOwner !== 'legacy' && value.executionOwner !== 'runtime') ||
@@ -169,6 +179,7 @@ function projectRun(
id: value.id,
taskId: value.taskId,
taskRevision: value.taskRevision,
...(value.triggerId === undefined ? {} : { triggerId: value.triggerId }),
status: value.status,
version: value.version,
eventSequence: value.eventSequence,
@@ -177,8 +188,12 @@ function projectRun(
executionOwner: value.executionOwner,
createdAtMs: value.createdAtMs,
...(value.queuedAtMs === undefined ? {} : { queuedAtMs: value.queuedAtMs }),
...(value.startedAtMs === undefined ? {} : { startedAtMs: value.startedAtMs }),
...(value.finishedAtMs === undefined ? {} : { finishedAtMs: value.finishedAtMs }),
...(value.startedAtMs === undefined
? {}
: { startedAtMs: value.startedAtMs }),
...(value.finishedAtMs === undefined
? {}
: { finishedAtMs: value.finishedAtMs }),
});
}
@@ -37,7 +37,11 @@ test('projects one descending bounded page and a stable keyset cursor', async ()
{
async listRunsByProject(query) {
calls.push(query);
return [run('run-c', 30), run('run-b', 20), run('run-a', 10)];
return [
run('run-c', 30, { triggerId: 'cron:task-run-c' }),
run('run-b', 20),
run('run-a', 10),
];
},
},
'prj_default',
@@ -50,6 +54,7 @@ test('projects one descending bounded page and a stable keyset cursor', async ()
id: 'run-c',
taskId: 'task-run-c',
taskRevision: 'revision-1',
triggerId: 'cron:task-run-c',
status: 'succeeded',
version: 2,
eventSequence: 3,
@@ -109,7 +114,11 @@ test('uses default and maximum bounds and passes an exact cursor', async () => {
});
test('fails closed on malformed inputs, rows, ordering and repository failures', async () => {
const reader = { async listRunsByProject() { return []; } };
const reader = {
async listRunsByProject() {
return [];
},
};
for (const input of [
null,
{ limit: 0 },
@@ -131,7 +140,11 @@ test('fails closed on malformed inputs, rows, ordering and repository failures',
]) {
await assert.rejects(
executeBoundedRunListProjection(
{ async listRunsByProject() { return rows; } },
{
async listRunsByProject() {
return rows;
},
},
'prj_default',
{},
),
@@ -140,7 +153,11 @@ test('fails closed on malformed inputs, rows, ordering and repository failures',
}
await assert.rejects(
executeBoundedRunListProjection(
{ async listRunsByProject() { throw new Error('offline'); } },
{
async listRunsByProject() {
throw new Error('offline');
},
},
'prj_default',
{},
),