mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
222 lines
6.1 KiB
JavaScript
222 lines
6.1 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
DuplicateIdempotencyKeyError,
|
|
MAX_RUN_EVENT_PAYLOAD_BYTES,
|
|
RunEventPayloadTooLargeError,
|
|
RunRepositoryBusyError,
|
|
RunRepositoryConstraintError,
|
|
} = require('@qinglong/runtime-core');
|
|
const { PostgresRunRepository, PostgresRunTransaction } = require('../dist');
|
|
|
|
const RUN = Object.freeze({
|
|
id: '019f70b0-0000-7000-8000-000000000001',
|
|
projectId: 'default',
|
|
taskId: 'task-1',
|
|
taskRevision: 'revision-1',
|
|
taskName: 'test',
|
|
triggerType: 'manual',
|
|
executionOrigin: 'manual',
|
|
executionOwner: 'runtime',
|
|
triggeredBy: 'user:1',
|
|
status: 'created',
|
|
version: 0,
|
|
eventSequence: 0,
|
|
priority: 0,
|
|
idempotencyKey: 'request-1',
|
|
createdAtMs: 1_750_000_000_000,
|
|
});
|
|
|
|
const EVENT = Object.freeze({
|
|
id: '019f70b0-0000-7000-8000-000000000003',
|
|
runId: RUN.id,
|
|
sequence: 1,
|
|
type: 'run.created',
|
|
dedupeKey: 'run.created',
|
|
actorType: 'system',
|
|
payload: Object.freeze({ source: 'postgres-test' }),
|
|
createdAtMs: 1_750_000_000_002,
|
|
});
|
|
|
|
function driverError(code, constraint) {
|
|
return Object.assign(new Error('driver failure'), { code, constraint });
|
|
}
|
|
|
|
function clientHarness(handler = async () => ({ rows: [], rowCount: 0 })) {
|
|
const queries = [];
|
|
let released = 0;
|
|
const client = {
|
|
async query(text, values) {
|
|
queries.push({ text, values });
|
|
return handler(text, values, queries.length);
|
|
},
|
|
release() {
|
|
released += 1;
|
|
},
|
|
};
|
|
return {
|
|
queries,
|
|
released: () => released,
|
|
pool: {
|
|
query: (...args) => client.query(...args),
|
|
async connect() {
|
|
return client;
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function databaseRunRow(overrides = {}) {
|
|
return {
|
|
...RUN,
|
|
createdAtMs: String(RUN.createdAtMs),
|
|
taskSnapshotRef: null,
|
|
legacyCronId: null,
|
|
parentRunId: null,
|
|
retryOfRunId: null,
|
|
triggerId: null,
|
|
requestId: null,
|
|
scheduledForMs: null,
|
|
queuedAtMs: null,
|
|
startedAtMs: null,
|
|
finishedAtMs: null,
|
|
cancelRequestedAtMs: null,
|
|
cancelReason: null,
|
|
inputRef: null,
|
|
outputRef: null,
|
|
errorCode: null,
|
|
errorSummary: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('orders one bounded PostgreSQL transaction and releases its client', async () => {
|
|
const harness = clientHarness();
|
|
const result = await new PostgresRunRepository(harness.pool).transaction(
|
|
async () => 'committed',
|
|
);
|
|
assert.equal(result, 'committed');
|
|
assert.deepEqual(
|
|
harness.queries.map(({ text }) => text),
|
|
[
|
|
'BEGIN',
|
|
'SET TRANSACTION ISOLATION LEVEL READ COMMITTED',
|
|
"SELECT set_config('statement_timeout', $1, true)",
|
|
"SELECT set_config('lock_timeout', $1, true)",
|
|
"SELECT set_config('idle_in_transaction_session_timeout', $1, true)",
|
|
'COMMIT',
|
|
],
|
|
);
|
|
assert.equal(harness.released(), 1);
|
|
});
|
|
|
|
test('rolls back a serialization failure and exposes a stable domain error', async () => {
|
|
const harness = clientHarness(async (text) => {
|
|
if (text === 'COMMIT') throw driverError('40001');
|
|
return { rows: [], rowCount: 0 };
|
|
});
|
|
await assert.rejects(
|
|
new PostgresRunRepository(harness.pool).transaction(async () => 1),
|
|
RunRepositoryBusyError,
|
|
);
|
|
assert.equal(harness.queries.at(-1).text, 'ROLLBACK');
|
|
assert.equal(harness.released(), 1);
|
|
});
|
|
|
|
test('normalizes bigint rows and treats missing rows as null', async () => {
|
|
const row = databaseRunRow({ scheduledForMs: '1750000000100' });
|
|
const harness = clientHarness(async (text, values) => {
|
|
if (!text.includes('FROM "ql3"."runs"')) {
|
|
throw new Error(`unexpected query: ${text}`);
|
|
}
|
|
return values[0] === RUN.id
|
|
? { rows: [row], rowCount: 1 }
|
|
: { rows: [], rowCount: 0 };
|
|
});
|
|
const repository = new PostgresRunRepository(harness.pool);
|
|
assert.deepEqual(await repository.findRunById(RUN.id), {
|
|
...RUN,
|
|
scheduledForMs: 1_750_000_000_100,
|
|
});
|
|
assert.equal(await repository.findRunById('missing'), null);
|
|
});
|
|
|
|
test('lists one Project with the indexed descending Run keyset', async () => {
|
|
const harness = clientHarness(async () => ({
|
|
rows: [
|
|
databaseRunRow({ id: 'run-c', createdAtMs: '20' }),
|
|
databaseRunRow({ id: 'run-b', createdAtMs: '20' }),
|
|
],
|
|
rowCount: 2,
|
|
}));
|
|
const repository = new PostgresRunRepository(harness.pool);
|
|
const rows = await repository.listRunsByProject({
|
|
projectId: 'default',
|
|
limit: 2,
|
|
after: { createdAtMs: 30, runId: 'run-z' },
|
|
});
|
|
assert.deepEqual(
|
|
rows.map(({ id }) => id),
|
|
['run-c', 'run-b'],
|
|
);
|
|
assert.deepEqual(harness.queries[0].values, ['default', 'run-z', 30, 2]);
|
|
assert.match(
|
|
harness.queries[0].text,
|
|
/ORDER BY "created_at_ms" DESC, "id" DESC\s+LIMIT \$4/u,
|
|
);
|
|
await assert.rejects(
|
|
repository.listRunsByProject({ projectId: 'default', limit: 66 }),
|
|
TypeError,
|
|
);
|
|
});
|
|
|
|
test('rejects duplicate identity rows and corrupt enum data', async () => {
|
|
const duplicate = clientHarness(async () => ({
|
|
rows: [databaseRunRow(), databaseRunRow()],
|
|
rowCount: 2,
|
|
}));
|
|
await assert.rejects(
|
|
new PostgresRunRepository(duplicate.pool).findRunById(RUN.id),
|
|
RunRepositoryConstraintError,
|
|
);
|
|
|
|
const corrupt = clientHarness(async () => ({
|
|
rows: [databaseRunRow({ status: 'invented' })],
|
|
rowCount: 1,
|
|
}));
|
|
await assert.rejects(
|
|
new PostgresRunRepository(corrupt.pool).findRunById(RUN.id),
|
|
RunRepositoryConstraintError,
|
|
);
|
|
});
|
|
|
|
test('maps PostgreSQL constraint names without leaking driver errors', async () => {
|
|
const transaction = new PostgresRunTransaction({
|
|
async query() {
|
|
throw driverError('23505', 'ql3_runs_project_idempotency_uidx');
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
transaction.insertRun(RUN),
|
|
DuplicateIdempotencyKeyError,
|
|
);
|
|
});
|
|
|
|
test('rejects oversized event payloads before issuing SQL', async () => {
|
|
let calls = 0;
|
|
const transaction = new PostgresRunTransaction({
|
|
async query() {
|
|
calls += 1;
|
|
return { rows: [], rowCount: 0 };
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
transaction.appendEvent({
|
|
...EVENT,
|
|
payload: { value: 'x'.repeat(MAX_RUN_EVENT_PAYLOAD_BYTES) },
|
|
}),
|
|
RunEventPayloadTooLargeError,
|
|
);
|
|
assert.equal(calls, 0);
|
|
});
|