mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
312 lines
9.3 KiB
JavaScript
312 lines
9.3 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
createInitialPluginPackageAutomationPublication,
|
|
createPluginPackageAutomationLifecyclePublication,
|
|
} = require('../dist/plugin-package/pluginPackageAutomationPublication');
|
|
const {
|
|
createPluginPackageWorkflowAdmissionBundle,
|
|
createPluginPackageWorkflowExecutionPlan,
|
|
InvalidPluginPackageWorkflowAdmissionReceiptError,
|
|
InvalidPluginPackageWorkflowExecutionPlanError,
|
|
normalizePluginPackageWorkflowAdmissionReceipt,
|
|
normalizePluginPackageWorkflowExecutionPlan,
|
|
PluginPackageWorkflowExecutionPlanConflictError,
|
|
pluginPackageWorkflowAdmissionReceiptDigest,
|
|
pluginPackageWorkflowExecutionPlanDigest,
|
|
} = require('@qinglong/runtime-core/plugin-package-workflow-execution-plan');
|
|
const {
|
|
pluginPackageTaskReconciliationFixture,
|
|
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
|
|
|
|
function fixture(namespace = 'workflow-execution-plan') {
|
|
const value = pluginPackageTaskReconciliationFixture(namespace, {
|
|
workflows: [
|
|
{
|
|
schema: 'qinglong/plugin-package-workflow-resource@v1',
|
|
id: 'daily',
|
|
name: 'Daily workflow',
|
|
enabled: true,
|
|
steps: [
|
|
{
|
|
id: 'collect',
|
|
task: 'alpha',
|
|
needs: [],
|
|
},
|
|
{
|
|
id: 'summarize',
|
|
task: 'beta',
|
|
needs: ['collect'],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
return {
|
|
...value,
|
|
publication: createInitialPluginPackageAutomationPublication(
|
|
value.revision,
|
|
value.registry,
|
|
2_000,
|
|
),
|
|
};
|
|
}
|
|
|
|
function planInput(value, overrides = {}) {
|
|
return {
|
|
planId: 'workflow-plan-001',
|
|
runId: 'workflow-run-001',
|
|
workflowId: 'daily',
|
|
stepRunIds: {
|
|
collect: 'step-run-collect-001',
|
|
summarize: 'step-run-summarize-001',
|
|
},
|
|
publication: value.publication,
|
|
revision: value.revision,
|
|
taskSpecSemanticRegistry: value.registry,
|
|
plannedAtMs: 3_000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('binds one active publication and exact materialized Tasks into a canonical DAG plan', () => {
|
|
const value = fixture();
|
|
const plan = createPluginPackageWorkflowExecutionPlan(planInput(value));
|
|
|
|
assert.equal(
|
|
plan.target.publicationDigest,
|
|
value.publication.publicationDigest,
|
|
);
|
|
assert.equal(
|
|
plan.target.materializedRevisionDigest,
|
|
value.revision.revisionDigest,
|
|
);
|
|
assert.equal(plan.target.workflowId, 'daily');
|
|
assert.match(plan.target.workflowDefinitionDigest, /^[0-9a-f]{64}$/);
|
|
assert.deepEqual(
|
|
plan.steps.map(
|
|
({ stepKey, taskId, needs, initialStatus, taskDefinitionRef }) => ({
|
|
stepKey,
|
|
taskId,
|
|
needs,
|
|
initialStatus,
|
|
taskDefinitionRef,
|
|
}),
|
|
),
|
|
[
|
|
{
|
|
stepKey: 'collect',
|
|
taskId: 'alpha',
|
|
needs: [],
|
|
initialStatus: 'ready',
|
|
taskDefinitionRef: `plugin-package:${value.revision.revisionDigest}:task:alpha`,
|
|
},
|
|
{
|
|
stepKey: 'summarize',
|
|
taskId: 'beta',
|
|
needs: ['collect'],
|
|
initialStatus: 'pending',
|
|
taskDefinitionRef: `plugin-package:${value.revision.revisionDigest}:task:beta`,
|
|
},
|
|
],
|
|
);
|
|
assert.equal(
|
|
plan.steps.every(({ required }) => required),
|
|
true,
|
|
);
|
|
assert.equal(plan.planDigest, pluginPackageWorkflowExecutionPlanDigest(plan));
|
|
assert.deepEqual(normalizePluginPackageWorkflowExecutionPlan(plan), plan);
|
|
assert.deepEqual(
|
|
normalizePluginPackageWorkflowExecutionPlan(
|
|
JSON.parse(JSON.stringify(plan)),
|
|
),
|
|
plan,
|
|
);
|
|
assert.equal(
|
|
createPluginPackageWorkflowExecutionPlan(planInput(value)).planDigest,
|
|
plan.planDigest,
|
|
);
|
|
});
|
|
|
|
test('rejects withdrawn, drifted and incomplete Workflow execution inputs', () => {
|
|
const value = fixture('workflow-execution-plan-conflict');
|
|
const withdrawn = createPluginPackageAutomationLifecyclePublication({
|
|
previous: value.publication,
|
|
state: 'withdrawn',
|
|
lifecycleEventDigest: 'a'.repeat(64),
|
|
publishedAtMs: 2_001,
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
createPluginPackageWorkflowExecutionPlan(
|
|
planInput(value, { publication: withdrawn }),
|
|
),
|
|
PluginPackageWorkflowExecutionPlanConflictError,
|
|
);
|
|
|
|
const replacement = fixture('workflow-execution-plan-replacement');
|
|
assert.throws(
|
|
() =>
|
|
createPluginPackageWorkflowExecutionPlan(
|
|
planInput(value, { revision: replacement.revision }),
|
|
),
|
|
PluginPackageWorkflowExecutionPlanConflictError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createPluginPackageWorkflowExecutionPlan(
|
|
planInput(value, {
|
|
stepRunIds: { collect: 'step-run-collect-001' },
|
|
}),
|
|
),
|
|
InvalidPluginPackageWorkflowExecutionPlanError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
createPluginPackageWorkflowExecutionPlan(
|
|
planInput(value, {
|
|
stepRunIds: {
|
|
collect: 'step-run-shared',
|
|
summarize: 'step-run-shared',
|
|
},
|
|
}),
|
|
),
|
|
InvalidPluginPackageWorkflowExecutionPlanError,
|
|
);
|
|
});
|
|
|
|
test('fails closed when a durable plan digest or generation-bound Task reference drifts', () => {
|
|
const value = fixture('workflow-execution-plan-normalization');
|
|
const plan = createPluginPackageWorkflowExecutionPlan(planInput(value));
|
|
assert.throws(
|
|
() =>
|
|
normalizePluginPackageWorkflowExecutionPlan({
|
|
...plan,
|
|
planDigest: 'f'.repeat(64),
|
|
}),
|
|
InvalidPluginPackageWorkflowExecutionPlanError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizePluginPackageWorkflowExecutionPlan({
|
|
...plan,
|
|
steps: plan.steps.map((step) =>
|
|
step.stepKey === 'collect'
|
|
? {
|
|
...step,
|
|
taskDefinitionRef:
|
|
'plugin-package:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:task:alpha',
|
|
}
|
|
: step,
|
|
),
|
|
}),
|
|
InvalidPluginPackageWorkflowExecutionPlanError,
|
|
);
|
|
});
|
|
|
|
test('derives a deterministic atomic admission bundle and durable receipt', () => {
|
|
const value = fixture('workflow-admission-bundle');
|
|
const plan = createPluginPackageWorkflowExecutionPlan(planInput(value));
|
|
const bundle = createPluginPackageWorkflowAdmissionBundle(plan);
|
|
|
|
assert.deepEqual(bundle.plan, plan);
|
|
assert.equal(bundle.run.id, plan.runId);
|
|
assert.equal(bundle.run.status, 'running');
|
|
assert.equal(bundle.run.version, 3);
|
|
assert.equal(bundle.run.eventSequence, 3);
|
|
assert.equal(bundle.admissionEvent.sequence, 1);
|
|
assert.equal(bundle.admissionEvent.type, 'workflow.admitted');
|
|
assert.equal(bundle.admissionEvent.id.length <= 36, true);
|
|
assert.deepEqual(
|
|
bundle.stepMutations.map((mutation) => ({
|
|
stepKey: mutation.stepRun.stepKey,
|
|
status: mutation.stepRun.status,
|
|
expectedRunVersion: mutation.expectedRunVersion,
|
|
expectedRunEventSequence: mutation.expectedRunEventSequence,
|
|
eventSequence: mutation.event.sequence,
|
|
})),
|
|
[
|
|
{
|
|
stepKey: 'collect',
|
|
status: 'ready',
|
|
expectedRunVersion: 1,
|
|
expectedRunEventSequence: 1,
|
|
eventSequence: 2,
|
|
},
|
|
{
|
|
stepKey: 'summarize',
|
|
status: 'pending',
|
|
expectedRunVersion: 2,
|
|
expectedRunEventSequence: 2,
|
|
eventSequence: 3,
|
|
},
|
|
],
|
|
);
|
|
assert.equal(bundle.receipt.planDigest, plan.planDigest);
|
|
assert.equal(bundle.receipt.finalRunVersion, 3);
|
|
assert.equal(bundle.receipt.finalRunEventSequence, 3);
|
|
assert.equal(
|
|
bundle.receipt.receiptDigest,
|
|
pluginPackageWorkflowAdmissionReceiptDigest(bundle.receipt),
|
|
);
|
|
assert.equal(
|
|
bundle.stepMutations.every(
|
|
({ event }) => event.id.length <= 36 && event.dedupeKey.length <= 36,
|
|
),
|
|
true,
|
|
);
|
|
assert.deepEqual(
|
|
normalizePluginPackageWorkflowAdmissionReceipt(
|
|
JSON.parse(JSON.stringify(bundle.receipt)),
|
|
),
|
|
bundle.receipt,
|
|
);
|
|
assert.deepEqual(createPluginPackageWorkflowAdmissionBundle(plan), bundle);
|
|
});
|
|
|
|
test('rejects tampered Workflow admission receipts and counters', () => {
|
|
const value = fixture('workflow-admission-receipt-invalid');
|
|
const bundle = createPluginPackageWorkflowAdmissionBundle(
|
|
createPluginPackageWorkflowExecutionPlan(planInput(value)),
|
|
);
|
|
|
|
assert.throws(
|
|
() =>
|
|
normalizePluginPackageWorkflowAdmissionReceipt({
|
|
...bundle.receipt,
|
|
receiptDigest: 'f'.repeat(64),
|
|
}),
|
|
InvalidPluginPackageWorkflowAdmissionReceiptError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizePluginPackageWorkflowAdmissionReceipt({
|
|
...bundle.receipt,
|
|
finalRunVersion: 2,
|
|
}),
|
|
InvalidPluginPackageWorkflowAdmissionReceiptError,
|
|
);
|
|
});
|
|
|
|
test('keeps Run and RunEvent identities portable across SQLite and PostgreSQL', () => {
|
|
const value = fixture('workflow-portable-identity');
|
|
assert.throws(
|
|
() =>
|
|
createPluginPackageWorkflowExecutionPlan(
|
|
planInput(value, { runId: `r${'a'.repeat(36)}` }),
|
|
),
|
|
InvalidPluginPackageWorkflowExecutionPlanError,
|
|
);
|
|
});
|
|
|
|
test('publishes the pure planner only through its explicit runtime-core subpath', () => {
|
|
const subpath = require('@qinglong/runtime-core/plugin-package-workflow-execution-plan');
|
|
const root = require('../dist');
|
|
assert.equal(
|
|
subpath.createPluginPackageWorkflowExecutionPlan,
|
|
createPluginPackageWorkflowExecutionPlan,
|
|
);
|
|
assert.equal(root.createPluginPackageWorkflowExecutionPlan, undefined);
|
|
});
|