mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:21:50 +08:00
255 lines
7.4 KiB
JavaScript
255 lines
7.4 KiB
JavaScript
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 {
|
|
taskExecutionRevisionMigration,
|
|
} = require('../../back/migrations/0012-task-execution-revisions');
|
|
const {
|
|
LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE,
|
|
localExecutionContextRecipeMigration,
|
|
} = require('../../back/migrations/0013-local-execution-context-recipes');
|
|
const { runMigrations } = require('../../back/migrations/runner');
|
|
const {
|
|
LegacySequelizeLocalExecutionContextRecipeRepository,
|
|
LocalExecutionContextRecipeCorruptError,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/localExecutionContextRecipeRepository');
|
|
const {
|
|
LegacySequelizeTaskExecutionRevisionRepository,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/taskExecutionRevisionRepository');
|
|
const {
|
|
LocalTaskExecutionRevisionPublisher,
|
|
} = require('../../back/runtime/application/localTaskExecutionRevisionPublisher');
|
|
const {
|
|
RecipeLocalExecutionContextMaterializer,
|
|
} = require('../../back/runtime/application/recipeLocalExecutionContextMaterializer');
|
|
const {
|
|
createLocalExecutionContextRecipe,
|
|
localExecutionContextRecipeDigest,
|
|
} = require('../../back/runtime/domain/localExecutionContextRecipe');
|
|
|
|
function contextRecipe() {
|
|
return createLocalExecutionContextRecipe([
|
|
{ name: 'MODE', kind: 'public', value: 'edge' },
|
|
{ name: 'TOKEN', kind: 'secret', secretRef: 'secret://token' },
|
|
]);
|
|
}
|
|
|
|
function revision(recipe = contextRecipe()) {
|
|
return {
|
|
projectId: 'default',
|
|
taskId: 'task-recipe',
|
|
taskRevision: 'revision-1',
|
|
executorType: 'local_process',
|
|
execution: {
|
|
command: { kind: 'argv', file: '/bin/true', args: [] },
|
|
environmentPolicy: 'isolated',
|
|
terminationGraceMs: 100,
|
|
},
|
|
contextRef: recipe.contextRef,
|
|
};
|
|
}
|
|
|
|
function candidate() {
|
|
return {
|
|
runId: 'run-recipe',
|
|
attemptId: 'attempt-recipe',
|
|
projectId: 'default',
|
|
taskId: 'task-recipe',
|
|
taskRevision: 'revision-1',
|
|
executorType: 'local_process',
|
|
priority: 0,
|
|
queuedAtMs: 1_760_000_000_000,
|
|
attemptCreatedAtMs: 1_760_000_000_000,
|
|
};
|
|
}
|
|
|
|
async function createRepositories(t) {
|
|
const database = new Sequelize({
|
|
dialect: 'sqlite',
|
|
storage: ':memory:',
|
|
logging: false,
|
|
});
|
|
t.after(() => database.close());
|
|
await runMigrations({
|
|
database,
|
|
migrationModel: defineSchemaMigrationModel(database),
|
|
migrations: [
|
|
taskExecutionRevisionMigration,
|
|
localExecutionContextRecipeMigration,
|
|
],
|
|
logger: { info() {} },
|
|
});
|
|
return {
|
|
database,
|
|
recipes: new LegacySequelizeLocalExecutionContextRecipeRepository(database),
|
|
revisions: new LegacySequelizeTaskExecutionRevisionRepository(database),
|
|
};
|
|
}
|
|
|
|
test('uses stable content-address and digest vectors without persisting Secret values', () => {
|
|
const recipe = contextRecipe();
|
|
assert.equal(
|
|
recipe.contextRef,
|
|
'localctx:sha256:d70176732d42e2701d95adce0371b765226a3d5085e17d14816615c933fc5c7f',
|
|
);
|
|
assert.equal(
|
|
localExecutionContextRecipeDigest(recipe),
|
|
'a1acbef2b7b882a0e6f204bd79f303a10fa6043753251ebb222adb31d42b2597',
|
|
);
|
|
assert.equal(
|
|
createLocalExecutionContextRecipe([...recipe.environment].reverse())
|
|
.contextRef,
|
|
recipe.contextRef,
|
|
);
|
|
assert.equal(JSON.stringify(recipe).includes('in-memory-secret'), false);
|
|
});
|
|
|
|
test('concurrent immutable recipe publication converges and resolves exactly', async (t) => {
|
|
const { recipes } = await createRepositories(t);
|
|
const recipe = contextRecipe();
|
|
const results = await Promise.all(
|
|
Array.from({ length: 12 }, (_, index) =>
|
|
recipes.insert(recipe, 100 + index),
|
|
),
|
|
);
|
|
assert.equal(results.filter((result) => result === 'inserted').length, 1);
|
|
assert.equal(results.filter((result) => result === 'idempotent').length, 11);
|
|
|
|
const stored = await recipes.resolve(recipe.contextRef);
|
|
assert.deepEqual(stored.environment, recipe.environment);
|
|
assert.ok(Object.isFrozen(stored));
|
|
assert.ok(Object.isFrozen(stored.environment));
|
|
assert.equal(await recipes.resolve(`${recipe.contextRef}-missing`), null);
|
|
await assert.rejects(
|
|
recipes.insert({ ...recipe, contextRef: 'localctx:sha256:wrong' }, 200),
|
|
/not content-addressed/,
|
|
);
|
|
});
|
|
|
|
test('fails closed on persisted recipe corruption and cluster dialect use', async (t) => {
|
|
const { database, recipes } = await createRepositories(t);
|
|
const recipe = contextRecipe();
|
|
await recipes.insert(recipe, 100);
|
|
await database
|
|
.getQueryInterface()
|
|
.bulkUpdate(
|
|
LOCAL_EXECUTION_CONTEXT_RECIPE_TABLE,
|
|
{ content_digest: '0'.repeat(64) },
|
|
{ context_ref: recipe.contextRef },
|
|
);
|
|
await assert.rejects(
|
|
recipes.resolve(recipe.contextRef),
|
|
LocalExecutionContextRecipeCorruptError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
new LegacySequelizeLocalExecutionContextRecipeRepository({
|
|
getDialect: () => 'postgres',
|
|
}),
|
|
/PostgreSQL adapter/,
|
|
);
|
|
});
|
|
|
|
test('publishes recipe before revision and materializes it with ephemeral Secrets', async (t) => {
|
|
const { recipes, revisions } = await createRepositories(t);
|
|
const recipe = contextRecipe();
|
|
const publisher = new LocalTaskExecutionRevisionPublisher(recipes, revisions);
|
|
assert.deepEqual(
|
|
await publisher.publish({
|
|
revision: revision(recipe),
|
|
contextRecipe: recipe,
|
|
createdAtMs: 100,
|
|
}),
|
|
{ contextRecipe: 'inserted', revision: 'inserted' },
|
|
);
|
|
assert.deepEqual(
|
|
await publisher.publish({
|
|
revision: revision(recipe),
|
|
contextRecipe: recipe,
|
|
createdAtMs: 101,
|
|
}),
|
|
{ contextRecipe: 'idempotent', revision: 'idempotent' },
|
|
);
|
|
assert.equal(
|
|
(await revisions.resolve(revision(recipe))).contextRef,
|
|
recipe.contextRef,
|
|
);
|
|
|
|
const materializer = new RecipeLocalExecutionContextMaterializer(
|
|
recipes,
|
|
{
|
|
async prepare() {
|
|
return {
|
|
logArtifactId: `local-${'c'.repeat(30)}`,
|
|
output: { async write() {} },
|
|
dispose() {},
|
|
};
|
|
},
|
|
},
|
|
{
|
|
async resolve() {
|
|
return ['in-memory-secret'];
|
|
},
|
|
},
|
|
);
|
|
const context = await materializer.prepare({
|
|
candidate: candidate(),
|
|
contextRef: recipe.contextRef,
|
|
});
|
|
assert.deepEqual(
|
|
{ ...context.context.environment },
|
|
{
|
|
MODE: 'edge',
|
|
TOKEN: 'in-memory-secret',
|
|
},
|
|
);
|
|
});
|
|
|
|
test('a missing or failed recipe publication can never create a dangling revision', async () => {
|
|
const calls = [];
|
|
const publisher = new LocalTaskExecutionRevisionPublisher(
|
|
{
|
|
async resolve() {
|
|
return null;
|
|
},
|
|
async insert() {
|
|
calls.push('recipe');
|
|
throw new Error('recipe write failed');
|
|
},
|
|
},
|
|
{
|
|
async resolve() {
|
|
return null;
|
|
},
|
|
async insert() {
|
|
calls.push('revision');
|
|
return 'inserted';
|
|
},
|
|
},
|
|
);
|
|
const recipe = contextRecipe();
|
|
await assert.rejects(
|
|
publisher.publish({
|
|
revision: revision(recipe),
|
|
contextRecipe: recipe,
|
|
createdAtMs: 100,
|
|
}),
|
|
/recipe write failed/,
|
|
);
|
|
assert.deepEqual(calls, ['recipe']);
|
|
await assert.rejects(
|
|
publisher.publish({
|
|
revision: { ...revision(recipe), contextRef: 'localctx:sha256:other' },
|
|
contextRecipe: recipe,
|
|
createdAtMs: 100,
|
|
}),
|
|
/does not match/,
|
|
);
|
|
assert.deepEqual(calls, ['recipe']);
|
|
});
|