Files
qinglong/test/back/legacyBootShadow.test.cjs
T

275 lines
7.9 KiB
JavaScript

require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { after, test } = require('node:test');
const { Sequelize } = require('sequelize');
const { Container } = require('typedi');
const dataDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-legacy-boot-shadow-'),
);
fs.mkdirSync(path.join(dataDirectory, 'db'), { recursive: true });
process.env.QL_DATA_DIR = dataDirectory;
const { CrontabModel, CrontabStatus } = require('../../back/data/cron');
const {
defineSchemaMigrationModel,
} = require('../../back/data/schemaMigration');
const CronService = require('../../back/services/cron').default;
const { runCrons } = require('../../back/schedule/api');
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
const {
runCancellationRequestMigration,
} = require('../../back/migrations/0004-run-cancellation-request');
const {
runAttemptDeadlineMigration,
} = require('../../back/migrations/0006-run-attempt-deadline');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
LegacyShadowRunObserver,
} = require('../../back/runtime/application/legacyShadowRunObserver');
const {
LegacyShadowRunWriter,
} = require('../../back/runtime/application/legacyShadowRunWriter');
const {
installLegacyExecutionObserver,
} = require('../../back/runtime/compatibility/legacyExecutionBridge');
const {
shadowOnlyRollout,
} = require('../../back/runtime/domain/runtimeRollout');
after(() => {
fs.rmSync(dataDirectory, { recursive: true, force: true });
});
function logger() {
return {
info() {},
warn() {},
error() {},
};
}
function successfulCommand() {
return `${JSON.stringify(process.execPath)} -e "process.exit(0)"`;
}
function cron(overrides = {}) {
return {
id: 71,
name: 'boot probe',
command: 'credential=must-not-persist',
schedule: '@boot',
status: CrontabStatus.queued,
isDisabled: 0,
extra_schedules: [],
...overrides,
};
}
async function withStubbedCronUpdate(work) {
const original = CrontabModel.update;
const updates = [];
CrontabModel.update = async (...args) => {
updates.push(args);
return [1];
};
try {
return await work(updates);
} finally {
CrontabModel.update = original;
}
}
test('bootTask selects only enabled @boot entries and fixes their origin before dispatch', async () => {
await withStubbedCronUpdate(async (updates) => {
const service = new CronService(logger());
const dispatched = [];
service.crontabs = async () => ({
data: [
cron({ id: 71 }),
cron({ id: 72, isDisabled: 1 }),
cron({ id: 73, schedule: '@once' }),
cron({ id: 74, schedule: '0 * * * *' }),
],
total: 4,
});
service.runSingle = (...args) => {
dispatched.push(args);
};
await service.bootTask();
assert.deepEqual(dispatched, [[71, 'boot']]);
assert.equal(updates.length, 1);
assert.deepEqual(updates[0][0], { status: CrontabStatus.queued });
assert.deepEqual(updates[0][1], { where: { id: [71] } });
});
});
test('persists one legacy-owned terminal Run for the real boot child path', async () => {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
const observations = [];
const failures = [];
let idSequence = 900;
try {
await runMigrations({
database,
migrationModel: defineSchemaMigrationModel(database),
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
],
logger: { info() {} },
});
const repository = new LegacySequelizeRunRepository(database);
const writer = new LegacyShadowRunWriter(repository, () => {
idSequence += 1;
return `019f7100-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
});
const delegate = new LegacyShadowRunObserver(
shadowOnlyRollout(['boot']),
writer,
{ failure: (failure) => failures.push(failure) },
);
const restore = installLegacyExecutionObserver(
{
begin(fact) {
const observation = delegate.begin(fact);
observations.push({ fact, observation });
return observation;
},
},
['boot'],
);
try {
await withStubbedCronUpdate(async () => {
const service = new CronService(logger());
service.getDb = async () => cron();
service.makeCommand = () => successfulCommand();
const result = await service.runSingle(71, 'boot');
assert.equal(result.code, 0);
assert.equal(observations.length, 1);
await observations[0].observation.settled();
});
} finally {
restore();
}
const runId = '019f7100-0000-7000-8000-000000000901';
const attemptId = '019f7100-0000-7000-8000-000000000902';
const run = await repository.findRunById(runId);
const attempt = await repository.findAttemptById(attemptId);
const events = await repository.listEvents(runId);
const fact = observations[0].fact;
assert.deepEqual(failures, []);
assert.equal(fact.origin, 'boot');
assert.equal(fact.triggerType, 'boot');
assert.equal(fact.triggeredBy, 'legacy:boot');
assert.equal(fact.legacyCronId, 71);
assert.match(fact.taskRevision, /^sha256:[0-9a-f]{64}$/u);
assert.equal(run.executionOrigin, 'boot');
assert.equal(run.executionOwner, 'legacy');
assert.equal(run.status, 'succeeded');
assert.equal(attempt.status, 'succeeded');
assert.equal(attempt.executorType, 'legacy_local');
assert.equal(attempt.pid > 0, true);
assert.equal(attempt.exitCode, 0);
assert.deepEqual(
events.map((event) => event.type),
[
'run.created',
'run.queued',
'run.dispatching',
'attempt.starting',
'attempt.running',
'run.running',
'attempt.succeeded',
'run.succeeded',
],
);
assert.equal(
JSON.stringify({ run, attempt, events }).includes('credential='),
false,
);
} finally {
await database.close();
}
});
test('@once remains a manual trigger instead of becoming an inferred execution origin', async () => {
const facts = [];
const restore = installLegacyExecutionObserver(
{
begin(fact) {
facts.push(fact);
return {
spawned() {},
running() {},
startFailed() {},
exited() {},
cancelled() {},
};
},
},
['manual', 'boot'],
);
try {
await withStubbedCronUpdate(async () => {
const service = new CronService(logger());
service.getDb = async () => cron({ id: 81, schedule: '@once' });
service.makeCommand = () => successfulCommand();
const result = await service.runSingle(81);
assert.equal(result.code, 0);
});
} finally {
restore();
}
assert.equal(facts.length, 1);
assert.equal(facts[0].origin, 'manual');
assert.equal(facts[0].triggerType, 'manual');
assert.equal(facts[0].triggeredBy, 'legacy:manual');
});
test('the legacy gRPC run endpoint delegates without inventing a grpc execution origin', async () => {
await withStubbedCronUpdate(async () => {
const service = new CronService(logger());
const dispatched = [];
service.runSingle = (...args) => {
dispatched.push(args);
};
Container.set(CronService, service);
try {
const response = await new Promise((resolve, reject) => {
runCrons({ request: { ids: [91] } }, (error, value) =>
error ? reject(error) : resolve(value),
);
});
assert.deepEqual(response, { code: 200 });
assert.deepEqual(dispatched, [[91]]);
assert.equal(dispatched[0].includes('grpc'), false);
} finally {
Container.remove(CronService);
}
});
});