mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const { Sequelize } = require('sequelize');
|
||||
const {
|
||||
defineSchemaMigrationModel,
|
||||
} = require('../../back/data/schemaMigration');
|
||||
const {
|
||||
TASK_EXECUTION_REVISION_TABLE,
|
||||
taskExecutionRevisionMigration,
|
||||
} = require('../../back/migrations/0012-task-execution-revisions');
|
||||
const { runMigrations } = require('../../back/migrations/runner');
|
||||
const {
|
||||
LegacySequelizeTaskExecutionRevisionRepository,
|
||||
TaskExecutionRevisionConflictError,
|
||||
} = require('../../back/runtime/adapters/legacy-sequelize/taskExecutionRevisionRepository');
|
||||
const {
|
||||
TaskExecutionRevisionCorruptError,
|
||||
taskExecutionRevisionDigest,
|
||||
} = require('../../back/runtime/domain/taskExecutionRevisionRecord');
|
||||
|
||||
function revision(overrides = {}) {
|
||||
return {
|
||||
projectId: 'project-a',
|
||||
taskId: 'task-a',
|
||||
taskRevision: 'sha256:revision-a',
|
||||
executorType: 'local_process',
|
||||
execution: {
|
||||
command: {
|
||||
kind: 'argv',
|
||||
file: '/usr/bin/env',
|
||||
args: ['node', 'script.js'],
|
||||
},
|
||||
workingDirectory: '/work',
|
||||
environmentPolicy: 'isolated',
|
||||
timeoutMs: 60_000,
|
||||
terminationGraceMs: 5_000,
|
||||
resourcePolicy: {
|
||||
memoryBytes: { value: 128 * 1024 * 1024, enforcement: 'required' },
|
||||
networkIsolation: 'best_effort',
|
||||
},
|
||||
},
|
||||
contextRef: 'context://project-a/task-a/revision-a',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function createRepository(t) {
|
||||
const database = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: ':memory:',
|
||||
logging: false,
|
||||
});
|
||||
t.after(() => database.close());
|
||||
await runMigrations({
|
||||
database,
|
||||
migrationModel: defineSchemaMigrationModel(database),
|
||||
migrations: [taskExecutionRevisionMigration],
|
||||
logger: { info() {} },
|
||||
});
|
||||
return {
|
||||
database,
|
||||
repository: new LegacySequelizeTaskExecutionRevisionRepository(database),
|
||||
};
|
||||
}
|
||||
|
||||
test('uses a stable canonical digest vector shared by future adapters', () => {
|
||||
assert.equal(
|
||||
taskExecutionRevisionDigest(revision()),
|
||||
'a8df00094b9b37c24f707367801c915c188bb8bf224f4f5ab72ba8753e6e2cf5',
|
||||
);
|
||||
assert.equal(
|
||||
taskExecutionRevisionDigest({
|
||||
contextRef: revision().contextRef,
|
||||
execution: {
|
||||
networkIgnored: true,
|
||||
...revision().execution,
|
||||
},
|
||||
executorType: revision().executorType,
|
||||
taskRevision: revision().taskRevision,
|
||||
taskId: revision().taskId,
|
||||
projectId: revision().projectId,
|
||||
ignored: true,
|
||||
}),
|
||||
'a8df00094b9b37c24f707367801c915c188bb8bf224f4f5ab72ba8753e6e2cf5',
|
||||
);
|
||||
});
|
||||
|
||||
test('stores an immutable canonical revision and resolves only the exact identity', async (t) => {
|
||||
const { repository } = await createRepository(t);
|
||||
const input = revision();
|
||||
|
||||
assert.equal(await repository.insert(input, 1_750_000_000_000), 'inserted');
|
||||
input.execution.command.args[0] = 'mutated-after-insert';
|
||||
input.execution.resourcePolicy.memoryBytes.value = 1;
|
||||
|
||||
const stored = await repository.resolve({
|
||||
projectId: 'project-a',
|
||||
taskId: 'task-a',
|
||||
taskRevision: 'sha256:revision-a',
|
||||
});
|
||||
assert.ok(stored);
|
||||
assert.deepEqual(stored.execution.command.args, ['node', 'script.js']);
|
||||
assert.equal(stored.execution.resourcePolicy.memoryBytes.value, 134_217_728);
|
||||
assert.equal(stored.createdAtMs, 1_750_000_000_000);
|
||||
assert.match(stored.contentDigest, /^[0-9a-f]{64}$/);
|
||||
assert.equal(stored.contentDigest, taskExecutionRevisionDigest(stored));
|
||||
assert.ok(Object.isFrozen(stored));
|
||||
assert.ok(Object.isFrozen(stored.execution));
|
||||
assert.ok(Object.isFrozen(stored.execution.command));
|
||||
assert.ok(Object.isFrozen(stored.execution.command.args));
|
||||
assert.ok(Object.isFrozen(stored.execution.resourcePolicy));
|
||||
assert.ok(Object.isFrozen(stored.execution.resourcePolicy.memoryBytes));
|
||||
|
||||
assert.equal(
|
||||
await repository.resolve({
|
||||
projectId: 'project-a',
|
||||
taskId: 'task-a',
|
||||
taskRevision: 'sha256:revision-b',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('concurrent replay converges without replacing the first immutable row', async (t) => {
|
||||
const { repository } = await createRepository(t);
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: 12 }, (_, index) =>
|
||||
repository.insert(revision(), 1_750_000_000_000 + index),
|
||||
),
|
||||
);
|
||||
|
||||
assert.equal(results.filter((result) => result === 'inserted').length, 1);
|
||||
assert.equal(results.filter((result) => result === 'idempotent').length, 11);
|
||||
const stored = await repository.resolve(revision());
|
||||
assert.ok(stored.createdAtMs >= 1_750_000_000_000);
|
||||
assert.ok(stored.createdAtMs <= 1_750_000_000_011);
|
||||
});
|
||||
|
||||
test('same identity with different normalized content is an immutable conflict', async (t) => {
|
||||
const { repository } = await createRepository(t);
|
||||
assert.equal(await repository.insert(revision(), 100), 'inserted');
|
||||
assert.equal(
|
||||
await repository.insert(
|
||||
{
|
||||
...revision(),
|
||||
unknownTopLevel: 'stripped',
|
||||
execution: {
|
||||
...revision().execution,
|
||||
unknownExecutionField: 'stripped',
|
||||
},
|
||||
},
|
||||
101,
|
||||
),
|
||||
'idempotent',
|
||||
);
|
||||
await assert.rejects(
|
||||
repository.insert(
|
||||
revision({
|
||||
execution: {
|
||||
...revision().execution,
|
||||
command: { kind: 'shell', command: 'echo changed' },
|
||||
},
|
||||
}),
|
||||
102,
|
||||
),
|
||||
TaskExecutionRevisionConflictError,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails closed when stored content or digest is corrupt', async (t) => {
|
||||
const { database, repository } = await createRepository(t);
|
||||
await repository.insert(revision(), 100);
|
||||
await database.getQueryInterface().bulkUpdate(
|
||||
TASK_EXECUTION_REVISION_TABLE,
|
||||
{ content_digest: '0'.repeat(64) },
|
||||
{
|
||||
project_id: 'project-a',
|
||||
task_id: 'task-a',
|
||||
task_revision: 'sha256:revision-a',
|
||||
},
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
repository.resolve(revision()),
|
||||
TaskExecutionRevisionCorruptError,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects invalid timestamps and prevents the SQLite adapter from serving cluster-control', async (t) => {
|
||||
const { repository } = await createRepository(t);
|
||||
await assert.rejects(repository.insert(revision(), -1), /createdAtMs/);
|
||||
assert.throws(
|
||||
() =>
|
||||
new LegacySequelizeTaskExecutionRevisionRepository({
|
||||
getDialect: () => 'postgres',
|
||||
}),
|
||||
/PostgreSQL adapter/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user