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,251 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
RecipeLocalExecutionContextMaterializer,
|
||||
} = require('../../back/runtime/application/recipeLocalExecutionContextMaterializer');
|
||||
const {
|
||||
normalizeLocalExecutionContextRecipe,
|
||||
} = require('../../back/runtime/domain/localExecutionContextRecipe');
|
||||
|
||||
function candidate() {
|
||||
return {
|
||||
runId: 'run-context',
|
||||
attemptId: 'attempt-context',
|
||||
projectId: 'default',
|
||||
taskId: 'task-context',
|
||||
taskRevision: 'revision-1',
|
||||
executorType: 'local_process',
|
||||
priority: 0,
|
||||
queuedAtMs: 1_760_000_000_000,
|
||||
attemptCreatedAtMs: 1_760_000_000_000,
|
||||
};
|
||||
}
|
||||
|
||||
function recipe(environment = []) {
|
||||
return {
|
||||
contextRef: 'context://default/task-context/revision-1',
|
||||
environment,
|
||||
};
|
||||
}
|
||||
|
||||
test('materializes public and ephemeral Secret values into one bounded context', async () => {
|
||||
const secretRequests = [];
|
||||
const artifactRequests = [];
|
||||
let disposed = 0;
|
||||
const output = { async write() {} };
|
||||
const materializer = new RecipeLocalExecutionContextMaterializer(
|
||||
{
|
||||
async resolve(contextRef) {
|
||||
assert.equal(contextRef, recipe().contextRef);
|
||||
return recipe([
|
||||
{ name: 'MODE', kind: 'public', value: 'edge' },
|
||||
{ name: 'TOKEN', kind: 'secret', secretRef: 'secret://token' },
|
||||
{ name: 'TOKEN_COPY', kind: 'secret', secretRef: 'secret://token' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
async prepare(request) {
|
||||
artifactRequests.push(request);
|
||||
return {
|
||||
logArtifactId: `local-${'a'.repeat(30)}`,
|
||||
output,
|
||||
async dispose() {
|
||||
disposed += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
async resolve(request) {
|
||||
secretRequests.push(request);
|
||||
assert.ok(Object.isFrozen(request));
|
||||
assert.ok(Object.isFrozen(request.candidate));
|
||||
assert.ok(Object.isFrozen(request.secretRefs));
|
||||
return ['in-memory-secret'];
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const materialized = await materializer.prepare({
|
||||
candidate: candidate(),
|
||||
contextRef: recipe().contextRef,
|
||||
});
|
||||
assert.deepEqual(secretRequests[0].secretRefs, ['secret://token']);
|
||||
assert.equal(artifactRequests.length, 1);
|
||||
assert.equal(materialized.logArtifactId, `local-${'a'.repeat(30)}`);
|
||||
assert.equal(materialized.context.output, output);
|
||||
assert.equal(Object.getPrototypeOf(materialized.context.environment), null);
|
||||
assert.deepEqual(
|
||||
{ ...materialized.context.environment },
|
||||
{
|
||||
MODE: 'edge',
|
||||
TOKEN: 'in-memory-secret',
|
||||
TOKEN_COPY: 'in-memory-secret',
|
||||
},
|
||||
);
|
||||
assert.ok(Object.isFrozen(materialized.context.environment));
|
||||
await materialized.dispose();
|
||||
assert.equal(disposed, 1);
|
||||
});
|
||||
|
||||
test('returns unavailable before allocating output when recipe or Secrets are missing', async () => {
|
||||
let artifactCalls = 0;
|
||||
const artifacts = {
|
||||
async prepare() {
|
||||
artifactCalls += 1;
|
||||
},
|
||||
};
|
||||
const missingRecipe = new RecipeLocalExecutionContextMaterializer(
|
||||
{
|
||||
async resolve() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
artifacts,
|
||||
);
|
||||
assert.equal(
|
||||
await missingRecipe.prepare({
|
||||
candidate: candidate(),
|
||||
contextRef: recipe().contextRef,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
|
||||
const missingSecret = new RecipeLocalExecutionContextMaterializer(
|
||||
{
|
||||
async resolve() {
|
||||
return recipe([
|
||||
{ name: 'TOKEN', kind: 'secret', secretRef: 'secret://missing' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
artifacts,
|
||||
{
|
||||
async resolve() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
await missingSecret.prepare({
|
||||
candidate: candidate(),
|
||||
contextRef: recipe().contextRef,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
assert.equal(artifactCalls, 0);
|
||||
});
|
||||
|
||||
test('fails closed on recipe drift, duplicate names, and invalid Secret results', async () => {
|
||||
const artifacts = {
|
||||
async prepare() {
|
||||
throw new Error('artifact allocation must not run');
|
||||
},
|
||||
};
|
||||
const drift = new RecipeLocalExecutionContextMaterializer(
|
||||
{
|
||||
async resolve() {
|
||||
return { ...recipe(), contextRef: 'context://other' };
|
||||
},
|
||||
},
|
||||
artifacts,
|
||||
);
|
||||
await assert.rejects(
|
||||
drift.prepare({ candidate: candidate(), contextRef: recipe().contextRef }),
|
||||
/does not match/,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeLocalExecutionContextRecipe(
|
||||
recipe([
|
||||
{ name: 'DUPLICATE', kind: 'public', value: 'a' },
|
||||
{ name: 'DUPLICATE', kind: 'public', value: 'b' },
|
||||
]),
|
||||
),
|
||||
/duplicated/,
|
||||
);
|
||||
|
||||
const invalidSecret = new RecipeLocalExecutionContextMaterializer(
|
||||
{
|
||||
async resolve() {
|
||||
return recipe([
|
||||
{ name: 'TOKEN', kind: 'secret', secretRef: 'secret://token' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
artifacts,
|
||||
{
|
||||
async resolve() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
);
|
||||
await assert.rejects(
|
||||
invalidSecret.prepare({
|
||||
candidate: candidate(),
|
||||
contextRef: recipe().contextRef,
|
||||
}),
|
||||
/invalid result/,
|
||||
);
|
||||
});
|
||||
|
||||
test('disposes an allocated Artifact when its capability is invalid', async () => {
|
||||
let disposed = 0;
|
||||
const materializer = new RecipeLocalExecutionContextMaterializer(
|
||||
{
|
||||
async resolve() {
|
||||
return recipe();
|
||||
},
|
||||
},
|
||||
{
|
||||
async prepare() {
|
||||
return {
|
||||
logArtifactId: '../escape',
|
||||
output: { async write() {} },
|
||||
async dispose() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
disposed += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
await assert.rejects(
|
||||
materializer.prepare({
|
||||
candidate: candidate(),
|
||||
contextRef: recipe().contextRef,
|
||||
}),
|
||||
/artifact id is invalid/,
|
||||
);
|
||||
assert.equal(disposed, 1);
|
||||
});
|
||||
|
||||
test('rejects an invalid request before consulting recipes or Secret providers', async () => {
|
||||
let recipeCalls = 0;
|
||||
const materializer = new RecipeLocalExecutionContextMaterializer(
|
||||
{
|
||||
async resolve() {
|
||||
recipeCalls += 1;
|
||||
return recipe();
|
||||
},
|
||||
},
|
||||
{ async prepare() {} },
|
||||
);
|
||||
await assert.rejects(
|
||||
materializer.prepare({
|
||||
candidate: { ...candidate(), attemptId: '../invalid\0' },
|
||||
contextRef: recipe().contextRef,
|
||||
}),
|
||||
);
|
||||
await assert.rejects(
|
||||
materializer.prepare({
|
||||
candidate: candidate(),
|
||||
contextRef: '../invalid\0',
|
||||
}),
|
||||
);
|
||||
assert.equal(recipeCalls, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user