mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
820 lines
23 KiB
JavaScript
820 lines
23 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
createInitialPluginPackageAutomationPublication,
|
|
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
|
|
const {
|
|
createPluginPackageWorkflowAdmissionBundle,
|
|
} = require('@qinglong/runtime-core/plugin-package-workflow-execution-plan');
|
|
const {
|
|
pluginPackageTaskReconciliationFixture,
|
|
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
|
|
const {
|
|
createClusterPluginPackageWorkflowAdministrationCapability,
|
|
} = require('@qinglong/cluster-control/workflow-administration');
|
|
const {
|
|
createClusterControlPluginPackageWorkflowRoutes,
|
|
} = require('@qinglong/cluster-control/workflow-routes');
|
|
|
|
const IDS = Object.freeze({
|
|
planId: '123e4567-e89b-42d3-a456-426614174000',
|
|
runId: '123e4567-e89b-42d3-a456-426614174001',
|
|
collect: '123e4567-e89b-42d3-a456-426614174002',
|
|
summarize: '123e4567-e89b-42d3-a456-426614174003',
|
|
});
|
|
|
|
function productFixture() {
|
|
const value = pluginPackageTaskReconciliationFixture('cluster-product', {
|
|
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 principal(now = 3_000) {
|
|
return Object.freeze({
|
|
subject: { type: 'api_app', id: 'workflow-operator' },
|
|
authenticationId: 'api_credential:workflow-product:1',
|
|
authenticatedAtMs: now - 1,
|
|
expiresAtMs: now + 60_000,
|
|
assurance: 'service',
|
|
});
|
|
}
|
|
|
|
test('derives the cluster Workflow plan from current server publications and exactly replays it', async () => {
|
|
const value = productFixture();
|
|
let storedPlan = null;
|
|
let cancellationCommand;
|
|
let inspectionCommand;
|
|
let runListCommand;
|
|
let stepListCommand;
|
|
let runEventListCommand;
|
|
const admissions = [];
|
|
const capability = createClusterPluginPackageWorkflowAdministrationCapability(
|
|
{
|
|
async findCurrent(projectId, packageName) {
|
|
assert.equal(projectId, value.projectId);
|
|
assert.equal(packageName, value.packageName);
|
|
return value.publication;
|
|
},
|
|
},
|
|
{
|
|
async find(generationDigest) {
|
|
assert.equal(
|
|
generationDigest,
|
|
value.revision.generation.generationDigest,
|
|
);
|
|
return value.revision;
|
|
},
|
|
},
|
|
{
|
|
async findPlanByPlanId(planId) {
|
|
assert.equal(planId, IDS.planId);
|
|
return storedPlan;
|
|
},
|
|
async admitAuthorized(admission) {
|
|
admissions.push(admission);
|
|
const bundle = createPluginPackageWorkflowAdmissionBundle(
|
|
admission.plan,
|
|
);
|
|
const status = storedPlan ? 'existing' : 'created';
|
|
storedPlan = admission.plan;
|
|
return { status, receipt: bundle.receipt };
|
|
},
|
|
},
|
|
{
|
|
async inspectRunAuthorized(command) {
|
|
inspectionCommand = command;
|
|
return {
|
|
schema: 'qinglong/plugin-package-workflow-run-inspection@v1',
|
|
found: false,
|
|
projectId: command.projectId,
|
|
packageName: command.packageName,
|
|
workflowId: command.workflowId,
|
|
runId: command.runId,
|
|
run: null,
|
|
stepCount: null,
|
|
stepStatusCounts: null,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
async listRunsAuthorized(command) {
|
|
runListCommand = command;
|
|
return {
|
|
schema: 'qinglong/plugin-package-workflow-run-list@v1',
|
|
projectId: command.projectId,
|
|
packageName: command.packageName,
|
|
workflowId: command.workflowId,
|
|
after: command.after,
|
|
runs: [],
|
|
truncated: false,
|
|
next: null,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
async listStepRunsAuthorized(command) {
|
|
stepListCommand = command;
|
|
return {
|
|
schema: 'qinglong/plugin-package-workflow-step-run-list@v1',
|
|
found: false,
|
|
projectId: command.projectId,
|
|
packageName: command.packageName,
|
|
workflowId: command.workflowId,
|
|
runId: command.runId,
|
|
stepRuns: [],
|
|
truncated: false,
|
|
next: null,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
async listRunEventsAuthorized(command) {
|
|
runEventListCommand = command;
|
|
return {
|
|
schema: 'qinglong/plugin-package-workflow-run-event-list@v1',
|
|
found: false,
|
|
projectId: command.projectId,
|
|
packageName: command.packageName,
|
|
workflowId: command.workflowId,
|
|
runId: command.runId,
|
|
afterSequence: command.afterSequence,
|
|
headSequence: null,
|
|
events: [],
|
|
truncated: false,
|
|
nextAfterSequence: null,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
async requestUserCancellation(command) {
|
|
cancellationCommand = command;
|
|
return {
|
|
status: 'accepted',
|
|
projectId: command.projectId,
|
|
runId: command.runId,
|
|
runStatus: 'running',
|
|
runVersion: 4,
|
|
eventSequence: 4,
|
|
cancelRequestedAtMs: 3_100,
|
|
cancelReason: 'user',
|
|
};
|
|
},
|
|
},
|
|
value.registry,
|
|
);
|
|
|
|
const listed = await capability.inspect(value.projectId, value.packageName);
|
|
assert.equal(listed.found, true);
|
|
assert.deepEqual(listed.workflows[0].steps, [
|
|
{ id: 'collect', task: 'alpha', needs: [] },
|
|
{ id: 'summarize', task: 'beta', needs: ['collect'] },
|
|
]);
|
|
|
|
const command = {
|
|
projectId: value.projectId,
|
|
packageName: value.packageName,
|
|
workflowId: 'daily',
|
|
planId: IDS.planId,
|
|
runId: IDS.runId,
|
|
stepRunIds: { collect: IDS.collect, summarize: IDS.summarize },
|
|
principal: principal(),
|
|
policyFence: { projectVersion: 3, bindingVersion: 7 },
|
|
plannedAtMs: 3_000,
|
|
};
|
|
const created = await capability.start(command);
|
|
const replay = await capability.start({ ...command, plannedAtMs: 9_000 });
|
|
assert.equal(created.status, 'created');
|
|
assert.equal(replay.status, 'existing');
|
|
assert.equal(replay.plan.planDigest, created.plan.planDigest);
|
|
assert.equal(
|
|
created.plan.target.publicationDigest,
|
|
value.publication.publicationDigest,
|
|
);
|
|
assert.equal(admissions[0].audit.eventId, IDS.planId);
|
|
assert.equal(admissions[0].audit.requestId, IDS.planId);
|
|
assert.equal(
|
|
admissions[0].audit.authenticationId,
|
|
principal().authenticationId,
|
|
);
|
|
assert.equal(admissions[1].audit.occurredAtMs, created.plan.plannedAtMs);
|
|
|
|
assert.equal(
|
|
(
|
|
await capability.cancel({
|
|
projectId: value.projectId,
|
|
packageName: value.packageName,
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
mutationId: 'workflow-cancel-mutation-1',
|
|
eventId: '018f0000-0000-7000-8000-000000000091',
|
|
principal: principal(),
|
|
policyFence: { projectVersion: 3, bindingVersion: 7 },
|
|
})
|
|
).status,
|
|
'accepted',
|
|
);
|
|
assert.deepEqual(cancellationCommand.workflowTarget, {
|
|
packageName: value.packageName,
|
|
workflowId: 'daily',
|
|
});
|
|
const inspected = await capability.inspectRun({
|
|
projectId: value.projectId,
|
|
packageName: value.packageName,
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
requestId: 'workflow-run-read-request-1',
|
|
auditEventId: '123e4567-e89b-42d3-a456-426614174093',
|
|
principal: principal(),
|
|
policyFence: { projectVersion: 3, bindingVersion: 7 },
|
|
observedAtMs: 3_200,
|
|
});
|
|
assert.equal(inspected.found, false);
|
|
assert.equal(inspectionCommand.audit.operationId, 'workflow.run.read');
|
|
assert.equal(
|
|
inspectionCommand.audit.requestId,
|
|
'workflow-run-read-request-1',
|
|
);
|
|
assert.deepEqual(inspectionCommand.fence, {
|
|
projectVersion: 3,
|
|
bindingVersion: 7,
|
|
});
|
|
const runPage = await capability.listRuns({
|
|
projectId: value.projectId,
|
|
packageName: value.packageName,
|
|
workflowId: 'daily',
|
|
limit: 16,
|
|
after: { admittedAtMs: 3_100, runId: IDS.runId },
|
|
requestId: 'workflow-run-list-request-1',
|
|
auditEventId: '123e4567-e89b-42d3-a456-426614174096',
|
|
principal: principal(),
|
|
policyFence: { projectVersion: 3, bindingVersion: 7 },
|
|
observedAtMs: 3_250,
|
|
});
|
|
assert.deepEqual(runPage.runs, []);
|
|
assert.equal(runListCommand.audit.operationId, 'workflow.run.list');
|
|
assert.equal(runListCommand.limit, 16);
|
|
assert.deepEqual(runListCommand.after, {
|
|
admittedAtMs: 3_100,
|
|
runId: IDS.runId,
|
|
});
|
|
const stepPage = await capability.listStepRuns({
|
|
projectId: value.projectId,
|
|
packageName: value.packageName,
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
limit: 16,
|
|
after: { stepKey: 'collect', id: IDS.collect },
|
|
requestId: 'workflow-step-list-request-1',
|
|
auditEventId: '123e4567-e89b-42d3-a456-426614174094',
|
|
principal: principal(),
|
|
policyFence: { projectVersion: 3, bindingVersion: 7 },
|
|
observedAtMs: 3_300,
|
|
});
|
|
assert.equal(stepPage.found, false);
|
|
assert.equal(stepListCommand.audit.operationId, 'workflow.step.list');
|
|
assert.equal(stepListCommand.limit, 16);
|
|
assert.deepEqual(stepListCommand.after, {
|
|
stepKey: 'collect',
|
|
id: IDS.collect,
|
|
});
|
|
const eventPage = await capability.listRunEvents({
|
|
projectId: value.projectId,
|
|
packageName: value.packageName,
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
limit: 16,
|
|
afterSequence: 2,
|
|
requestId: 'workflow-event-list-request-1',
|
|
auditEventId: '123e4567-e89b-42d3-a456-426614174095',
|
|
principal: principal(),
|
|
policyFence: { projectVersion: 3, bindingVersion: 7 },
|
|
observedAtMs: 3_400,
|
|
});
|
|
assert.equal(eventPage.found, false);
|
|
assert.equal(runEventListCommand.audit.operationId, 'workflow.event.list');
|
|
assert.equal(runEventListCommand.limit, 16);
|
|
assert.equal(runEventListCommand.afterSequence, 2);
|
|
});
|
|
|
|
test('publishes strict authenticated list and content-free start routes', async () => {
|
|
let startCommand;
|
|
let cancellationError;
|
|
const routes = createClusterControlPluginPackageWorkflowRoutes(
|
|
{
|
|
async inspect() {
|
|
return { found: false, publicationState: null, workflows: [] };
|
|
},
|
|
async inspectRun(command) {
|
|
return {
|
|
schema: 'qinglong/plugin-package-workflow-run-inspection@v1',
|
|
found: true,
|
|
projectId: command.projectId,
|
|
packageName: command.packageName,
|
|
workflowId: command.workflowId,
|
|
runId: command.runId,
|
|
run: {
|
|
status: 'running',
|
|
version: 4,
|
|
eventSequence: 4,
|
|
createdAtMs: 3_000,
|
|
queuedAtMs: 3_010,
|
|
startedAtMs: 3_020,
|
|
finishedAtMs: null,
|
|
cancelRequestedAtMs: null,
|
|
cancelReason: null,
|
|
},
|
|
stepCount: 1,
|
|
stepStatusCounts: {
|
|
pending: 0,
|
|
ready: 0,
|
|
waiting_approval: 0,
|
|
running: 1,
|
|
lost: 0,
|
|
succeeded: 0,
|
|
failed: 0,
|
|
skipped: 0,
|
|
cancelled: 0,
|
|
timed_out: 0,
|
|
},
|
|
};
|
|
},
|
|
async listRuns(command) {
|
|
return {
|
|
schema: 'qinglong/plugin-package-workflow-run-list@v1',
|
|
projectId: command.projectId,
|
|
packageName: command.packageName,
|
|
workflowId: command.workflowId,
|
|
after: command.after,
|
|
runs: [
|
|
{
|
|
runId: IDS.runId,
|
|
status: 'running',
|
|
version: 4,
|
|
eventSequence: 4,
|
|
stepCount: 2,
|
|
admittedAtMs: 3_000,
|
|
queuedAtMs: 3_010,
|
|
startedAtMs: 3_020,
|
|
finishedAtMs: null,
|
|
cancelRequestedAtMs: null,
|
|
cancelReason: null,
|
|
},
|
|
],
|
|
truncated: true,
|
|
next: { admittedAtMs: 3_000, runId: IDS.runId },
|
|
};
|
|
},
|
|
async listStepRuns(command) {
|
|
return {
|
|
schema: 'qinglong/plugin-package-workflow-step-run-list@v1',
|
|
found: true,
|
|
projectId: command.projectId,
|
|
packageName: command.packageName,
|
|
workflowId: command.workflowId,
|
|
runId: command.runId,
|
|
stepRuns: [
|
|
{
|
|
id: IDS.collect,
|
|
parentStepRunId: null,
|
|
stepKey: 'collect',
|
|
kind: 'task',
|
|
required: true,
|
|
status: 'running',
|
|
version: 2,
|
|
attemptCount: 1,
|
|
readyAtMs: 3_010,
|
|
startedAtMs: 3_020,
|
|
finishedAtMs: null,
|
|
resultCode: null,
|
|
createdAtMs: 3_000,
|
|
updatedAtMs: 3_020,
|
|
},
|
|
],
|
|
truncated: true,
|
|
next: { stepKey: 'collect', id: IDS.collect },
|
|
};
|
|
},
|
|
async listRunEvents(command) {
|
|
return {
|
|
schema: 'qinglong/plugin-package-workflow-run-event-list@v1',
|
|
found: true,
|
|
projectId: command.projectId,
|
|
packageName: command.packageName,
|
|
workflowId: command.workflowId,
|
|
runId: command.runId,
|
|
afterSequence: command.afterSequence,
|
|
headSequence: 4,
|
|
events: [
|
|
{
|
|
id: '123e4567-e89b-42d3-a456-426614174004',
|
|
sequence: 3,
|
|
type: 'workflow.task_attempt.running',
|
|
stepRunId: IDS.collect,
|
|
createdAtMs: 3_020,
|
|
},
|
|
],
|
|
truncated: true,
|
|
nextAfterSequence: 3,
|
|
};
|
|
},
|
|
async start(command) {
|
|
startCommand = command;
|
|
return {
|
|
status: 'created',
|
|
plan: { planId: command.planId, runId: command.runId },
|
|
receipt: { receiptDigest: 'a'.repeat(64) },
|
|
};
|
|
},
|
|
async cancel(command) {
|
|
if (cancellationError) {
|
|
throw cancellationError;
|
|
}
|
|
return {
|
|
status: 'accepted',
|
|
projectId: command.projectId,
|
|
runId: command.runId,
|
|
runStatus: 'running',
|
|
runVersion: 4,
|
|
eventSequence: 4,
|
|
cancelRequestedAtMs: 3_100,
|
|
cancelReason: 'user',
|
|
};
|
|
},
|
|
},
|
|
() => 3_000,
|
|
() => '123e4567-e89b-42d3-a456-426614174092',
|
|
);
|
|
const start = routes.find(
|
|
({ operationId }) => operationId === 'workflow.start',
|
|
);
|
|
assert.ok(start);
|
|
const authorized = {
|
|
request: {
|
|
requestId: 'transport-request-1',
|
|
body: {
|
|
schema: 'qinglong/cluster-plugin-package-workflow-start-request@v1',
|
|
planId: IDS.planId,
|
|
runId: IDS.runId,
|
|
stepRunIds: { collect: IDS.collect },
|
|
},
|
|
signal: new AbortController().signal,
|
|
},
|
|
principal: principal(),
|
|
operationId: 'workflow.start',
|
|
permission: 'run.start',
|
|
projectId: 'project-1',
|
|
policyFence: { projectVersion: 3, bindingVersion: 7 },
|
|
};
|
|
const result = await start.handle(authorized, {
|
|
packageName: 'example',
|
|
workflowId: 'daily',
|
|
});
|
|
assert.equal(result.statusCode, 201);
|
|
assert.deepEqual(Object.keys(result.body).sort(), [
|
|
'planId',
|
|
'receiptDigest',
|
|
'replayed',
|
|
'runId',
|
|
'schema',
|
|
'status',
|
|
]);
|
|
assert.equal(startCommand.principal.subject.id, 'workflow-operator');
|
|
assert.equal(startCommand.plannedAtMs, 3_000);
|
|
|
|
const inspectRun = routes.find(
|
|
({ operationId }) => operationId === 'workflow.run.read',
|
|
);
|
|
assert.ok(inspectRun);
|
|
assert.equal(inspectRun.permission, 'run.read');
|
|
const inspected = await inspectRun.handle(
|
|
{
|
|
...authorized,
|
|
operationId: 'workflow.run.read',
|
|
permission: 'run.read',
|
|
request: { ...authorized.request, body: null },
|
|
},
|
|
{
|
|
packageName: 'example',
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
},
|
|
);
|
|
assert.equal(inspected.statusCode, 200);
|
|
assert.equal(
|
|
inspected.body.schema,
|
|
'qinglong/plugin-package-workflow-run-inspection@v1',
|
|
);
|
|
assert.deepEqual(Object.keys(inspected.body.run).sort(), [
|
|
'cancelReason',
|
|
'cancelRequestedAtMs',
|
|
'createdAtMs',
|
|
'eventSequence',
|
|
'finishedAtMs',
|
|
'queuedAtMs',
|
|
'startedAtMs',
|
|
'status',
|
|
'version',
|
|
]);
|
|
|
|
const listRuns = routes.find(
|
|
({ operationId }) => operationId === 'workflow.run.list',
|
|
);
|
|
assert.ok(listRuns);
|
|
assert.equal(listRuns.permission, 'run.read');
|
|
assert.deepEqual(listRuns.allowedQuery, [
|
|
'after_admitted_at_ms',
|
|
'after_run_id',
|
|
'limit',
|
|
]);
|
|
const listedRuns = await listRuns.handle(
|
|
{
|
|
...authorized,
|
|
operationId: 'workflow.run.list',
|
|
permission: 'run.read',
|
|
request: {
|
|
...authorized.request,
|
|
body: null,
|
|
query: {
|
|
limit: ['1'],
|
|
after_admitted_at_ms: ['3100'],
|
|
after_run_id: [IDS.runId],
|
|
},
|
|
},
|
|
},
|
|
{ packageName: 'example', workflowId: 'daily' },
|
|
);
|
|
assert.equal(listedRuns.statusCode, 200);
|
|
assert.equal(
|
|
listedRuns.body.schema,
|
|
'qinglong/plugin-package-workflow-run-list@v1',
|
|
);
|
|
assert.deepEqual(Object.keys(listedRuns.body.runs[0]).sort(), [
|
|
'admittedAtMs',
|
|
'cancelReason',
|
|
'cancelRequestedAtMs',
|
|
'eventSequence',
|
|
'finishedAtMs',
|
|
'queuedAtMs',
|
|
'runId',
|
|
'startedAtMs',
|
|
'status',
|
|
'stepCount',
|
|
'version',
|
|
]);
|
|
const invalidRunCursor = await listRuns.handle(
|
|
{
|
|
...authorized,
|
|
request: {
|
|
...authorized.request,
|
|
body: null,
|
|
query: { after_admitted_at_ms: ['3100'] },
|
|
},
|
|
},
|
|
{ packageName: 'example', workflowId: 'daily' },
|
|
);
|
|
assert.equal(invalidRunCursor.statusCode, 400);
|
|
|
|
const listSteps = routes.find(
|
|
({ operationId }) => operationId === 'workflow.step.list',
|
|
);
|
|
assert.ok(listSteps);
|
|
assert.equal(listSteps.permission, 'run.read');
|
|
assert.deepEqual(listSteps.allowedQuery, [
|
|
'after_step_key',
|
|
'after_step_run_id',
|
|
'limit',
|
|
]);
|
|
const listedSteps = await listSteps.handle(
|
|
{
|
|
...authorized,
|
|
operationId: 'workflow.step.list',
|
|
permission: 'run.read',
|
|
request: {
|
|
...authorized.request,
|
|
body: null,
|
|
query: {
|
|
limit: ['1'],
|
|
after_step_key: ['collect'],
|
|
after_step_run_id: [IDS.collect],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
packageName: 'example',
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
},
|
|
);
|
|
assert.equal(listedSteps.statusCode, 200);
|
|
assert.equal(
|
|
listedSteps.body.schema,
|
|
'qinglong/plugin-package-workflow-step-run-list@v1',
|
|
);
|
|
assert.deepEqual(Object.keys(listedSteps.body.stepRuns[0]).sort(), [
|
|
'attemptCount',
|
|
'createdAtMs',
|
|
'finishedAtMs',
|
|
'id',
|
|
'kind',
|
|
'parentStepRunId',
|
|
'readyAtMs',
|
|
'required',
|
|
'resultCode',
|
|
'startedAtMs',
|
|
'status',
|
|
'stepKey',
|
|
'updatedAtMs',
|
|
'version',
|
|
]);
|
|
const invalidStepCursor = await listSteps.handle(
|
|
{
|
|
...authorized,
|
|
request: {
|
|
...authorized.request,
|
|
body: null,
|
|
query: { after_step_key: ['collect'] },
|
|
},
|
|
},
|
|
{
|
|
packageName: 'example',
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
},
|
|
);
|
|
assert.equal(invalidStepCursor.statusCode, 400);
|
|
|
|
const listEvents = routes.find(
|
|
({ operationId }) => operationId === 'workflow.event.list',
|
|
);
|
|
assert.ok(listEvents);
|
|
assert.equal(listEvents.permission, 'run.read');
|
|
assert.deepEqual(listEvents.allowedQuery, ['after_sequence', 'limit']);
|
|
const listedEvents = await listEvents.handle(
|
|
{
|
|
...authorized,
|
|
operationId: 'workflow.event.list',
|
|
permission: 'run.read',
|
|
request: {
|
|
...authorized.request,
|
|
body: null,
|
|
query: { limit: ['1'], after_sequence: ['2'] },
|
|
},
|
|
},
|
|
{
|
|
packageName: 'example',
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
},
|
|
);
|
|
assert.equal(listedEvents.statusCode, 200);
|
|
assert.equal(
|
|
listedEvents.body.schema,
|
|
'qinglong/plugin-package-workflow-run-event-list@v1',
|
|
);
|
|
assert.deepEqual(Object.keys(listedEvents.body.events[0]).sort(), [
|
|
'createdAtMs',
|
|
'id',
|
|
'sequence',
|
|
'stepRunId',
|
|
'type',
|
|
]);
|
|
const invalidEventCursor = await listEvents.handle(
|
|
{
|
|
...authorized,
|
|
request: {
|
|
...authorized.request,
|
|
body: null,
|
|
query: { after_sequence: ['02'] },
|
|
},
|
|
},
|
|
{
|
|
packageName: 'example',
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
},
|
|
);
|
|
assert.equal(invalidEventCursor.statusCode, 400);
|
|
|
|
const invalid = await start.handle(
|
|
{
|
|
...authorized,
|
|
request: {
|
|
...authorized.request,
|
|
body: { ...authorized.request.body, extra: true },
|
|
},
|
|
},
|
|
{ packageName: 'example', workflowId: 'daily' },
|
|
);
|
|
assert.equal(invalid.statusCode, 400);
|
|
|
|
const cancellation = routes.find(
|
|
({ operationId }) => operationId === 'workflow.cancel',
|
|
);
|
|
assert.ok(cancellation);
|
|
const cancelled = await cancellation.handle(
|
|
{
|
|
...authorized,
|
|
operationId: 'workflow.cancel',
|
|
permission: 'run.stop',
|
|
request: {
|
|
...authorized.request,
|
|
body: {
|
|
schema: 'qinglong/run-cancellation@v1',
|
|
mutationId: 'workflow-cancel-mutation-1',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
packageName: 'example',
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
},
|
|
);
|
|
assert.equal(cancelled.statusCode, 202);
|
|
assert.equal(cancelled.body.status, 'accepted');
|
|
assert.deepEqual(
|
|
Object.keys(cancelled.body).sort(),
|
|
[
|
|
'cancelReason',
|
|
'cancelRequestedAtMs',
|
|
'eventSequence',
|
|
'projectId',
|
|
'runId',
|
|
'runStatus',
|
|
'runVersion',
|
|
'schema',
|
|
'status',
|
|
].sort(),
|
|
);
|
|
const invalidCancellation = await cancellation.handle(
|
|
{
|
|
...authorized,
|
|
operationId: 'workflow.cancel',
|
|
permission: 'run.stop',
|
|
request: {
|
|
...authorized.request,
|
|
body: {
|
|
schema: 'qinglong/run-cancellation@v1',
|
|
mutationId: 'workflow-cancel-mutation-1',
|
|
reason: 'shutdown',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
packageName: 'example',
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
},
|
|
);
|
|
assert.equal(invalidCancellation.statusCode, 400);
|
|
|
|
cancellationError = Object.assign(new Error('private adapter detail'), {
|
|
code: 'CLUSTER_RUN_CANCELLATION_FENCE_REJECTED',
|
|
reason: 'private_adapter_detail',
|
|
});
|
|
const closedCancellationFailure = await cancellation.handle(
|
|
{
|
|
...authorized,
|
|
operationId: 'workflow.cancel',
|
|
permission: 'run.stop',
|
|
request: {
|
|
...authorized.request,
|
|
body: {
|
|
schema: 'qinglong/run-cancellation@v1',
|
|
mutationId: 'workflow-cancel-mutation-2',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
packageName: 'example',
|
|
workflowId: 'daily',
|
|
runId: IDS.runId,
|
|
},
|
|
);
|
|
assert.equal(closedCancellationFailure.statusCode, 409);
|
|
assert.deepEqual(closedCancellationFailure.body, {
|
|
code: 'workflow_cancellation_fence_rejected',
|
|
reason: 'state_mismatch',
|
|
});
|
|
});
|