feat(ql3): compare bounded task run outcomes

This commit is contained in:
whyour
2026-08-14 17:18:25 +08:00
parent a55613afaa
commit 828370c60b
22 changed files with 1802 additions and 22 deletions
@@ -75,6 +75,11 @@
"require": "./dist/run-management/runManualRetryRepository.js",
"default": "./dist/run-management/runManualRetryRepository.js"
},
"./task-run-outcome-window": {
"types": "./dist/run/outcome-comparison/taskRunOutcomeWindowReader.d.ts",
"require": "./dist/run/outcome-comparison/taskRunOutcomeWindowReader.js",
"default": "./dist/run/outcome-comparison/taskRunOutcomeWindowReader.js"
},
"./run-manager": {
"types": "./dist/entrypoints/runManager.d.ts",
"require": "./dist/entrypoints/runManager.js",
@@ -0,0 +1,84 @@
import { RUN_STATUSES, type RunStatus } from '@qinglong/runtime-core/run';
import {
RunRepositoryConstraintError,
RunRepositoryOperationError,
} from '@qinglong/runtime-core/run-repository';
import {
normalizeTaskRunOutcomeWindowQuery,
normalizeTaskRunOutcomeWindowRecord,
type TaskRunOutcomeWindowQuery,
type TaskRunOutcomeWindowReader,
type TaskRunOutcomeWindowRecord,
} from '@qinglong/runtime-core/task-run-outcome-window';
import type { PostgresQueryable } from '@qinglong/runtime-core';
type QueryRow = Readonly<Record<string, unknown>>;
function requiredString(row: QueryRow, property: string): string {
const value = row[property];
if (typeof value !== 'string' || value.length === 0) {
throw new RunRepositoryConstraintError(
`PostgreSQL Task Run outcome row has an invalid ${property}`,
);
}
return value;
}
function requiredInteger(row: QueryRow, property: string): number {
const value = row[property];
if (typeof value === 'number' && Number.isSafeInteger(value)) return value;
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
throw new RunRepositoryConstraintError(
`PostgreSQL Task Run outcome row has an invalid ${property}`,
);
}
function record(row: QueryRow): Readonly<TaskRunOutcomeWindowRecord> {
const status = requiredString(row, 'status');
if (!RUN_STATUSES.includes(status as RunStatus)) {
throw new RunRepositoryConstraintError(
'PostgreSQL Task Run outcome row has an invalid status',
);
}
return normalizeTaskRunOutcomeWindowRecord({
id: requiredString(row, 'id'),
projectId: requiredString(row, 'projectId'),
taskId: requiredString(row, 'taskId'),
status: status as RunStatus,
createdAtMs: requiredInteger(row, 'createdAtMs'),
});
}
export class PostgresTaskRunOutcomeWindowReader
implements TaskRunOutcomeWindowReader
{
constructor(private readonly queryable: PostgresQueryable) {}
async listRecentRunsByTask(
value: Readonly<TaskRunOutcomeWindowQuery>,
): Promise<readonly Readonly<TaskRunOutcomeWindowRecord>[]> {
const query = normalizeTaskRunOutcomeWindowQuery(value);
try {
const result = await this.queryable.query<QueryRow>(
`SELECT
"id" AS "id",
"project_id" AS "projectId",
"task_id" AS "taskId",
"status" AS "status",
"created_at_ms" AS "createdAtMs"
FROM "ql3"."runs"
WHERE "project_id" = $1 AND "task_id" = $2
ORDER BY "created_at_ms" DESC, "id" DESC
LIMIT $3`,
[query.projectId, query.taskId, query.limit],
);
return Object.freeze(result.rows.map(record));
} catch (error) {
if (error instanceof RunRepositoryConstraintError) throw error;
throw new RunRepositoryOperationError(error);
}
}
}
@@ -0,0 +1,114 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
RunRepositoryConstraintError,
RunRepositoryOperationError,
} = require('@qinglong/runtime-core/run-repository');
const {
PostgresTaskRunOutcomeWindowReader,
} = require('@qinglong/cluster-postgres/task-run-outcome-window');
function harness(rows) {
const queries = [];
return {
queries,
queryable: {
async query(text, values) {
queries.push({ text, values });
return { rows, rowCount: rows.length };
},
},
};
}
test('reads one bounded PostgreSQL Project Task window with bigint normalization', async () => {
assert.equal(
require('@qinglong/cluster-postgres').PostgresTaskRunOutcomeWindowReader,
undefined,
);
const value = harness([
{
id: 'run-failed',
projectId: 'project-a',
taskId: 'task-a',
status: 'failed',
createdAtMs: '2000',
},
{
id: 'run-succeeded',
projectId: 'project-a',
taskId: 'task-a',
status: 'succeeded',
createdAtMs: '1000',
},
]);
const rows = await new PostgresTaskRunOutcomeWindowReader(
value.queryable,
).listRecentRunsByTask({
projectId: 'project-a',
taskId: 'task-a',
limit: 65,
});
assert.deepEqual(rows, [
{
id: 'run-failed',
projectId: 'project-a',
taskId: 'task-a',
status: 'failed',
createdAtMs: 2000,
},
{
id: 'run-succeeded',
projectId: 'project-a',
taskId: 'task-a',
status: 'succeeded',
createdAtMs: 1000,
},
]);
assert.deepEqual(value.queries[0].values, ['project-a', 'task-a', 65]);
assert.match(
value.queries[0].text,
/WHERE "project_id" = \$1 AND "task_id" = \$2[\s\S]*ORDER BY "created_at_ms" DESC, "id" DESC[\s\S]*LIMIT \$3/u,
);
});
test('rejects invalid windows and maps driver failures without leaking details', async () => {
const invalid = harness([
{
id: 'run-invalid',
projectId: 'project-a',
taskId: 'task-a',
status: 'invented',
createdAtMs: '1000',
},
]);
await assert.rejects(
new PostgresTaskRunOutcomeWindowReader(
invalid.queryable,
).listRecentRunsByTask({
projectId: 'project-a',
taskId: 'task-a',
limit: 65,
}),
RunRepositoryConstraintError,
);
await assert.rejects(
new PostgresTaskRunOutcomeWindowReader({
async query() {
throw new Error('postgresql://private-host/secret');
},
}).listRecentRunsByTask({
projectId: 'project-a',
taskId: 'task-a',
limit: 65,
}),
(error) => {
assert.ok(error instanceof RunRepositoryOperationError);
assert.equal(error.message, 'Run repository operation failed');
return true;
},
);
});