mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
feat(ql3): compare bounded task run outcomes
This commit is contained in:
@@ -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",
|
||||
|
||||
+84
@@ -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;
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user