feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,182 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PluginPackageAutomationPublicationConflictError,
createInitialPluginPackageAutomationPublication,
createNextPluginPackageAutomationPublication,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageAutomationPublication');
const {
pluginPackageTaskReconciliationFixture,
} = require('./pluginPackageTaskReconciliationRepositoryContract.cjs');
function automationOptions(name = 'daily') {
return {
workflows: [
{
schema: 'qinglong/plugin-package-workflow-resource@v1',
id: name,
name: `Workflow ${name}`,
enabled: true,
steps: [{ id: 'run', task: 'alpha', needs: [] }],
},
],
prompts: [
{
schema: 'qinglong/plugin-package-prompt-resource@v1',
id: `${name}-prompt`,
name: `Prompt ${name}`,
template: 'Hello {{name}}',
parameters: [{ name: 'name', required: true }],
},
],
};
}
function fixture(namespace, options = {}) {
return pluginPackageTaskReconciliationFixture(namespace, {
profile: options.profile,
previous: options.previous,
...automationOptions(options.name),
});
}
function registerPluginPackageAutomationPublicationRepositoryContract(options) {
test(`${options.name} publishes and replays one exact current head`, async (t) => {
const value = fixture(`${options.namespace}-initial`, {
profile: options.profile,
name: 'daily',
});
const harness = await options.createRepository(t, value);
t.after(() => harness.close?.());
await harness.materializedRepository.publish(value.revision);
const publication = createInitialPluginPackageAutomationPublication(
value.revision,
value.registry,
1_000,
);
const created = await harness.repository.publish(publication);
assert.equal(created.status, 'created');
assert.deepEqual(created.publication, publication);
assert.equal((await harness.repository.publish(publication)).status, 'existing');
assert.deepEqual(
await harness.repository.findByDigest(publication.publicationDigest),
publication,
);
assert.deepEqual(
await harness.repository.findCurrent(value.projectId, value.packageName),
publication,
);
});
test(`${options.name} advances generations with one CAS publication chain`, async (t) => {
const first = fixture(`${options.namespace}-upgrade`, {
profile: options.profile,
name: 'daily',
});
const harness = await options.createRepository(t, first);
t.after(() => harness.close?.());
await harness.materializedRepository.publish(first.revision);
const initial = createInitialPluginPackageAutomationPublication(
first.revision,
first.registry,
1_000,
);
await harness.repository.publish(initial);
const second = fixture(first.namespace, {
profile: options.profile,
previous: first,
name: 'hourly',
});
await harness.materializedRepository.publish(second.revision);
const next = createNextPluginPackageAutomationPublication(
second.revision,
second.registry,
initial,
1_100,
);
assert.equal((await harness.repository.publish(next)).status, 'created');
assert.deepEqual(
await harness.repository.findCurrent(first.projectId, first.packageName),
next,
);
assert.deepEqual(
await harness.repository.findByDigest(initial.publicationDigest),
initial,
);
const staleFork = createNextPluginPackageAutomationPublication(
second.revision,
second.registry,
initial,
1_101,
);
await assert.rejects(
harness.repository.publish(staleFork),
PluginPackageAutomationPublicationConflictError,
);
assert.deepEqual(
await harness.repository.findCurrent(first.projectId, first.packageName),
next,
);
});
test(`${options.name} persists absent tombstones across Package generations`, async (t) => {
const first = fixture(`${options.namespace}-tombstone`, {
profile: options.profile,
name: 'daily',
});
const harness = await options.createRepository(t, first);
t.after(() => harness.close?.());
await harness.materializedRepository.publish(first.revision);
const initial = createInitialPluginPackageAutomationPublication(
first.revision,
first.registry,
1_000,
);
await harness.repository.publish(initial);
const second = pluginPackageTaskReconciliationFixture(first.namespace, {
profile: options.profile,
previous: first,
});
await harness.materializedRepository.publish(second.revision);
const absent = createNextPluginPackageAutomationPublication(
second.revision,
second.registry,
initial,
1_100,
);
assert.equal(absent.state, 'absent');
assert.equal((await harness.repository.publish(absent)).status, 'created');
const third = fixture(first.namespace, {
profile: options.profile,
previous: second,
name: 'restored',
});
await harness.materializedRepository.publish(third.revision);
const restored = createNextPluginPackageAutomationPublication(
third.revision,
third.registry,
absent,
1_200,
);
assert.equal(restored.state, 'active');
assert.equal((await harness.repository.publish(restored)).status, 'created');
assert.deepEqual(
await harness.repository.findCurrent(first.projectId, first.packageName),
restored,
);
assert.deepEqual(
await harness.repository.findByDigest(absent.publicationDigest),
absent,
);
});
}
module.exports = {
pluginPackageAutomationPublicationFixture: fixture,
registerPluginPackageAutomationPublicationRepositoryContract,
};
@@ -0,0 +1,387 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
PluginPackageInstallMutationConflictError,
PluginPackageInstallTransitionConflictError,
createPluginPackageInstall,
createPluginPackageLock,
pluginPackageInstallActionDigest,
pluginPackageInstallCommit,
pluginPackageInstallCreate,
pluginPackageInstallPlanDigest,
transitionPluginPackageInstall,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/installation/pluginPackageInstall');
const {
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
planPluginPackageInstall,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackage');
const ARTIFACT_DIGEST = 'a'.repeat(64);
const CONTENT_DIGEST = 'b'.repeat(64);
function manifest(packageName) {
return {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: packageName,
displayName: packageName,
version: '1.2.0',
description: 'One bounded package',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '16Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: [],
},
contents: {
tasks: [],
workflows: [],
prompts: [],
tools: [],
},
},
};
}
function environment() {
return {
qinglongVersion: '3.0.0-alpha.0',
architecture: 'arm64',
deploymentProfile: 'edge',
runtimes: [],
availableMemoryBytes: 128 * 1024 * 1024,
availableDiskBytes: 256 * 1024 * 1024,
};
}
function fixture(namespace, overrides = {}) {
const packageName = overrides.packageName ?? `${namespace}-package`;
const packageManifest = manifest(packageName);
const installEnvironment = environment();
const plan = planPluginPackageInstall(packageManifest, installEnvironment);
const source = {
kind: 'offline',
locator: `offline:sha256:${ARTIFACT_DIGEST}`,
artifactDigest: ARTIFACT_DIGEST,
artifactBytes: 2048,
contentDigest: CONTENT_DIGEST,
};
const lockId = overrides.lockId ?? `lock-${namespace}`;
const actionInput = {
lockId,
projectId: 'default',
manifest: packageManifest,
plan,
environment: installEnvironment,
source,
architecture: 'arm64',
deploymentProfile: 'edge',
targetGeneration: 1,
};
const lock = createPluginPackageLock({
...actionInput,
approval: {
requestId: `approval-${lockId}`,
requestVersion: 1,
dispatchId: `dispatch-${lockId}`,
actionDigest: pluginPackageInstallActionDigest(actionInput),
previewDigest: pluginPackageInstallPlanDigest(plan),
approvedBy: { type: 'user', id: 'owner-001' },
approvedAtMs: 100,
expiresAtMs: 10_000,
fence: { projectVersion: 1, bindingVersion: 1 },
},
createdAtMs: 200,
});
const install = createPluginPackageInstall(lock, {
installationId: overrides.installationId ?? `install-${namespace}`,
mutationId: overrides.mutationId ?? `mutation-create-${namespace}`,
occurredAtMs: overrides.occurredAtMs ?? 201,
});
return { lock, install };
}
function stage(lock, install, overrides = {}) {
return transitionPluginPackageInstall(lock, install, {
type: 'stage_completed',
mutationId:
overrides.mutationId ?? `mutation-stage-${install.installationId}`,
occurredAtMs: overrides.occurredAtMs ?? install.updatedAtMs + 1,
stageRef: `stage:${lock.lockDigest}`,
artifactDigest: lock.source.artifactDigest,
manifestDigest: lock.manifestDigest,
contentDigest: lock.source.contentDigest,
evidenceDigest: 'e'.repeat(64),
});
}
function registerPluginPackageInstallRepositoryContract({
name,
createRepository,
}) {
let sequence = 0;
function nextNamespace(suffix) {
sequence += 1;
return `ql3-${process.pid.toString(36)}-${Date.now().toString(
36,
)}-${sequence.toString(36)}-${suffix}`;
}
async function setup(t, suffix) {
const namespace = nextNamespace(suffix);
const harness = await createRepository({ namespace });
if (harness.close) t.after(() => harness.close());
return { namespace, repository: harness.repository };
}
test(`${name}: creates, finds, and exactly replays one durable head`, async (t) => {
const { namespace, repository } = await setup(t, 'create');
const value = fixture(namespace);
const command = pluginPackageInstallCreate(value.lock, value.install, null);
const created = await repository.create(command);
assert.equal(created.status, 'created');
assert.deepEqual(
await repository.find('default', value.install.packageName),
value.install,
);
assert.deepEqual(
await repository.findLock(value.lock.lockDigest),
value.lock,
);
assert.equal(await repository.findLock('f'.repeat(64)), null);
const replay = await repository.create(command);
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.record, value.install);
});
test(`${name}: rejects mutation reuse with different locked facts`, async (t) => {
const { namespace, repository } = await setup(t, 'mutation');
const value = fixture(namespace);
await repository.create(
pluginPackageInstallCreate(value.lock, value.install, null),
);
const drift = fixture(namespace, {
lockId: `lock-${namespace}-drift`,
installationId: value.install.installationId,
mutationId: value.install.lastMutationId,
});
await assert.rejects(
repository.create(
pluginPackageInstallCreate(drift.lock, drift.install, null),
),
PluginPackageInstallMutationConflictError,
);
});
test(`${name}: commits exact CAS transitions and rejects stale state`, async (t) => {
const { namespace, repository } = await setup(t, 'cas');
const value = fixture(namespace);
await repository.create(
pluginPackageInstallCreate(value.lock, value.install, null),
);
const staged = stage(value.lock, value.install);
const command = pluginPackageInstallCommit(value.install, staged);
const committed = await repository.commit(command);
assert.equal(committed.status, 'committed');
assert.deepEqual(
await repository.find('default', value.install.packageName),
staged,
);
assert.equal((await repository.commit(command)).status, 'existing');
const competing = stage(value.lock, value.install, {
mutationId: `mutation-competing-${namespace}`,
});
await assert.rejects(
repository.commit(pluginPackageInstallCommit(value.install, competing)),
PluginPackageInstallTransitionConflictError,
);
});
test(`${name}: replays an old mutation to the current advanced record`, async (t) => {
const { namespace, repository } = await setup(t, 'advance');
const value = fixture(namespace);
const create = pluginPackageInstallCreate(value.lock, value.install, null);
await repository.create(create);
const staged = stage(value.lock, value.install);
await repository.commit(pluginPackageInstallCommit(value.install, staged));
const activating = transitionPluginPackageInstall(value.lock, staged, {
type: 'activation_started',
mutationId: `mutation-activate-${namespace}`,
occurredAtMs: 203,
});
await repository.commit(pluginPackageInstallCommit(staged, activating));
const replay = await repository.create(create);
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.record, activating);
});
test(`${name}: replaces only an exact terminal head`, async (t) => {
const { namespace, repository } = await setup(t, 'replace');
const first = fixture(namespace);
await repository.create(
pluginPackageInstallCreate(first.lock, first.install, null),
);
const failed = transitionPluginPackageInstall(first.lock, first.install, {
type: 'failed',
mutationId: `mutation-fail-${namespace}`,
occurredAtMs: 202,
reason: 'stage_failed',
});
await repository.commit(pluginPackageInstallCommit(first.install, failed));
const retry = fixture(namespace, {
lockId: `lock-${namespace}-retry`,
installationId: `install-${namespace}-retry`,
mutationId: `mutation-${namespace}-retry`,
occurredAtMs: 203,
});
await repository.create(
pluginPackageInstallCreate(retry.lock, retry.install, failed),
);
assert.deepEqual(
await repository.find('default', first.install.packageName),
retry.install,
);
const stale = fixture(namespace, {
lockId: `lock-${namespace}-stale`,
installationId: `install-${namespace}-stale`,
mutationId: `mutation-${namespace}-stale`,
occurredAtMs: 204,
});
await assert.rejects(
repository.create(
pluginPackageInstallCreate(stale.lock, stale.install, failed),
),
PluginPackageInstallTransitionConflictError,
);
});
test(`${name}: paginates only current recoverable heads with a stable cursor`, async (t) => {
const { namespace, repository } = await setup(t, 'recovery');
const alpha = fixture(namespace, {
packageName: `${namespace}-alpha`,
lockId: `lock-${namespace}-alpha`,
installationId: `install-${namespace}-alpha`,
mutationId: `mutation-${namespace}-alpha`,
});
const beta = fixture(namespace, {
packageName: `${namespace}-beta`,
lockId: `lock-${namespace}-beta`,
installationId: `install-${namespace}-beta`,
mutationId: `mutation-${namespace}-beta`,
});
await repository.create(
pluginPackageInstallCreate(alpha.lock, alpha.install, null),
);
await repository.create(
pluginPackageInstallCreate(beta.lock, beta.install, null),
);
const beforeNamespace = {
packageName: namespace,
installationId: `cursor-${namespace}`,
};
const first = await repository.listRecoveryPage({
limit: 1,
after: beforeNamespace,
});
assert.deepEqual(first.records, [alpha.install]);
assert.equal(first.truncated, true);
assert.deepEqual(first.next, {
packageName: alpha.install.packageName,
installationId: alpha.install.installationId,
});
const second = await repository.listRecoveryPage({
limit: 1,
after: first.next,
});
assert.deepEqual(second.records, [beta.install]);
const failed = transitionPluginPackageInstall(alpha.lock, alpha.install, {
type: 'failed',
mutationId: `mutation-${namespace}-alpha-fail`,
occurredAtMs: 202,
reason: 'source_unavailable',
});
await repository.commit(pluginPackageInstallCommit(alpha.install, failed));
const recovery = await repository.listRecoveryPage({
limit: 64,
after: beforeNamespace,
});
assert.deepEqual(
recovery.records.filter((record) =>
record.packageName.startsWith(namespace),
),
[beta.install],
);
});
test(`${name}: lists every current head for one project with a stable cursor`, async (t) => {
const { namespace, repository } = await setup(t, 'inventory');
const alpha = fixture(namespace, {
packageName: `${namespace}-alpha`,
lockId: `lock-${namespace}-inventory-alpha`,
installationId: `install-${namespace}-inventory-alpha`,
mutationId: `mutation-${namespace}-inventory-alpha`,
});
const beta = fixture(namespace, {
packageName: `${namespace}-beta`,
lockId: `lock-${namespace}-inventory-beta`,
installationId: `install-${namespace}-inventory-beta`,
mutationId: `mutation-${namespace}-inventory-beta`,
});
await repository.create(
pluginPackageInstallCreate(alpha.lock, alpha.install, null),
);
await repository.create(
pluginPackageInstallCreate(beta.lock, beta.install, null),
);
const failed = transitionPluginPackageInstall(alpha.lock, alpha.install, {
type: 'failed',
mutationId: `mutation-${namespace}-inventory-alpha-failed`,
occurredAtMs: 202,
reason: 'source_unavailable',
});
await repository.commit(pluginPackageInstallCommit(alpha.install, failed));
const first = await repository.listCurrentPage({
projectId: 'default',
limit: 1,
after: { packageName: namespace },
});
assert.deepEqual(first.items, [{ record: failed, quarantine: null }]);
assert.equal(first.truncated, true);
assert.deepEqual(first.next, { packageName: alpha.install.packageName });
const second = await repository.listCurrentPage({
projectId: 'default',
limit: 1,
after: first.next,
});
assert.deepEqual(second.items, [
{ record: beta.install, quarantine: null },
]);
});
}
module.exports = {
fixture,
registerPluginPackageInstallRepositoryContract,
};
@@ -0,0 +1,188 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const { test } = require('node:test');
const {
createPluginPackageResourceGenerationFromReferences,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageResourceGeneration');
const {
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
planPluginPackageInstall,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackage');
const {
createPluginPackageLock,
pluginPackageInstallActionDigest,
pluginPackageInstallPlanDigest,
serializePluginPackageManifest,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/installation/pluginPackageInstall');
const {
pluginPackageContentTreeDigest,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageBundle');
const {
InvalidPluginPackageResourceMaterializationError,
materializePluginPackageResources,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageResourceMaterialization');
const {
createBuiltInTaskSpecSemanticRegistry,
} = require('../../packages/ql3-runtime-core/dist/task-definition/taskSpecSemantic');
function materializedRevisionFixture(namespace, profile = 'edge') {
const projectId = `project-${namespace}`;
const packageName = `package-${namespace}`;
const resourcePath = 'prompts/report.json';
const resourceBytes = Buffer.from(
JSON.stringify({
schema: 'qinglong/plugin-package-prompt-resource@v1',
id: 'report',
name: 'Report',
template: 'Hello {{name}}\n',
parameters: [{ name: 'name', required: true }],
}),
);
const sourceDigest = createHash('sha256').update(resourceBytes).digest('hex');
const contentDigest = pluginPackageContentTreeDigest([
{ path: resourcePath, bytes: resourceBytes.byteLength, digest: sourceDigest },
]);
const manifest = {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: packageName,
displayName: packageName,
version: '1.0.0',
description: 'One immutable semantic revision fixture',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: [profile],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '8Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: [],
},
contents: {
tasks: [],
workflows: [],
prompts: [resourcePath],
tools: [],
},
},
};
const environment = {
qinglongVersion: '3.0.0-alpha.0',
architecture: 'arm64',
deploymentProfile: profile,
runtimes: [],
availableMemoryBytes: 128 * 1024 * 1024,
availableDiskBytes: 256 * 1024 * 1024,
};
const plan = planPluginPackageInstall(manifest, environment);
const actionInput = {
lockId: `lock-${namespace}`,
projectId,
manifest,
plan,
environment,
source: {
kind: 'offline',
locator: `offline:sha256:${'a'.repeat(64)}`,
artifactDigest: 'a'.repeat(64),
artifactBytes: 2048,
contentDigest,
},
architecture: 'arm64',
deploymentProfile: profile,
targetGeneration: 1,
};
const lock = createPluginPackageLock({
...actionInput,
approval: {
requestId: `approval-${namespace}`,
requestVersion: 1,
dispatchId: `dispatch-${namespace}`,
actionDigest: pluginPackageInstallActionDigest(actionInput),
previewDigest: pluginPackageInstallPlanDigest(plan),
approvedBy: { type: 'user', id: 'owner-001' },
approvedAtMs: 100,
expiresAtMs: 10_000,
fence: { projectVersion: 1, bindingVersion: 1 },
},
createdAtMs: 200,
});
const generation = createPluginPackageResourceGenerationFromReferences({
installationId: `install-${namespace}`,
projectId,
packageName,
lockDigest: lock.lockDigest,
generation: 1,
previousActiveLockDigest: null,
contentDigest,
resources: lock.resources,
});
const registry = createBuiltInTaskSpecSemanticRegistry();
const revision = materializePluginPackageResources({
generation,
lock,
manifestBytes: Buffer.from(serializePluginPackageManifest(manifest)),
resources: [
{
reference: generation.resources[0],
bytes: resourceBytes,
},
],
taskSpecSemanticRegistry: registry,
});
return { projectId, packageName, registry, revision };
}
function registerPluginPackageMaterializedRevisionRepositoryContract(options) {
test(`${options.name} publishes once and replays the exact revision`, async (t) => {
const fixture = materializedRevisionFixture(
options.namespace ?? 'materialized',
options.profile,
);
const harness = await options.createRepository(t, fixture);
t.after(() => harness.close?.());
const first = await harness.repository.publish(fixture.revision);
assert.equal(first.status, 'created');
assert.deepEqual(first.revision, fixture.revision);
const replay = await harness.repository.publish(fixture.revision);
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.revision, fixture.revision);
assert.deepEqual(
await harness.repository.find(
fixture.revision.generation.generationDigest,
),
fixture.revision,
);
});
test(`${options.name} returns absence and rejects invalid lookup identity`, async (t) => {
const fixture = materializedRevisionFixture(
`${options.namespace ?? 'materialized'}-absence`,
options.profile,
);
const harness = await options.createRepository(t, fixture);
t.after(() => harness.close?.());
assert.equal(await harness.repository.find('f'.repeat(64)), null);
await assert.rejects(
harness.repository.find('../generation'),
InvalidPluginPackageResourceMaterializationError,
);
});
}
module.exports = {
materializedRevisionFixture,
registerPluginPackageMaterializedRevisionRepositoryContract,
};
@@ -0,0 +1,479 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const { test } = require('node:test');
const {
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
planPluginPackageInstall,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackage');
const {
createPluginPackageLock,
createPluginPackageInstall,
pluginPackageActivationIntentDigest,
pluginPackageInstallActionDigest,
pluginPackageInstallCommit,
pluginPackageInstallCreate,
pluginPackageInstallPlanDigest,
serializePluginPackageManifest,
transitionPluginPackageInstall,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/installation/pluginPackageInstall');
const {
pluginPackageContentTreeDigest,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageBundle');
const {
createPluginPackagePublisherProvenance,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/publisher/pluginPackagePublisherProvenance');
const {
createPluginPackageResourceGenerationFromReferences,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageResourceGeneration');
const {
materializePluginPackageResources,
} = require('../../packages/ql3-runtime-core/dist/plugin-package/pluginPackageResourceMaterialization');
const {
createBuiltInTaskSpecSemanticRegistry,
} = require('../../packages/ql3-runtime-core/dist/task-definition/taskSpecSemantic');
function environment(profile) {
return {
qinglongVersion: '3.0.0-alpha.0',
architecture: 'arm64',
deploymentProfile: profile,
runtimes: [],
availableMemoryBytes: 128 * 1024 * 1024,
availableDiskBytes: 256 * 1024 * 1024,
};
}
function taskValue(id, argument = id) {
const command =
typeof argument === 'string'
? {
kind: 'argv',
file: '/usr/bin/printf',
args: [argument],
}
: argument;
return {
schema: 'qinglong/plugin-package-task-resource@v1',
id,
name: `Task ${id}`,
labels: { 'plugin.qinglong.io/source': 'contract' },
enabled: true,
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command,
},
},
};
}
function activeRecord(lock, namespace, previousHead = null) {
const queued = createPluginPackageInstall(lock, {
installationId: `install-${namespace}-${lock.targetGeneration}`,
mutationId: `create-${namespace}-${lock.targetGeneration}`,
occurredAtMs: 1_000 + lock.targetGeneration * 10,
});
const staged = transitionPluginPackageInstall(lock, queued, {
type: 'stage_completed',
mutationId: `stage-${namespace}-${lock.targetGeneration}`,
occurredAtMs: queued.updatedAtMs + 1,
stageRef: `stage:${lock.lockDigest}`,
artifactDigest: lock.source.artifactDigest,
manifestDigest: lock.manifestDigest,
contentDigest: lock.source.contentDigest,
evidenceDigest: createHash('sha256')
.update(`evidence-${namespace}-${lock.targetGeneration}`)
.digest('hex'),
});
const activating = transitionPluginPackageInstall(lock, staged, {
type: 'activation_started',
mutationId: `activate-${namespace}-${lock.targetGeneration}`,
occurredAtMs: staged.updatedAtMs + 1,
});
const active = transitionPluginPackageInstall(lock, activating, {
type: 'activation_committed',
mutationId: `commit-${namespace}-${lock.targetGeneration}`,
occurredAtMs: activating.updatedAtMs + 1,
activationRef: `activation:${lock.lockDigest}`,
intentDigest: pluginPackageActivationIntentDigest(lock, activating),
generation: lock.targetGeneration,
contentDigest: lock.source.contentDigest,
});
return {
queued,
staged,
activating,
active,
create: pluginPackageInstallCreate(lock, queued, previousHead),
commits: [
pluginPackageInstallCommit(queued, staged),
pluginPackageInstallCommit(staged, activating),
pluginPackageInstallCommit(activating, active),
],
};
}
function fixture(namespace, options = {}) {
const profile = options.profile ?? 'edge';
const previous = options.previous ?? null;
const generation = previous ? previous.generation + 1 : 1;
const packageName = `package-${namespace}`;
const projectId = `project-${namespace}`;
const tasks = options.tasks ?? [
['alpha', 'alpha'],
['beta', 'beta'],
];
const workflows = options.workflows ?? [];
const prompts = options.prompts ?? [];
const tools = options.tools ?? [];
const taskResources = tasks.map(([id, argument]) => {
const path = `tasks/${id}.json`;
const bytes = Buffer.from(JSON.stringify(taskValue(id, argument)));
return {
reference: { kind: 'task', path },
bytes,
descriptor: {
path,
bytes: bytes.byteLength,
digest: createHash('sha256').update(bytes).digest('hex'),
},
};
});
const toolResources = tools.map((definition, index) => {
const path = `tools/tool-${index}.json`;
const bytes = Buffer.from(
JSON.stringify({
schema: 'qinglong/plugin-package-tool-resource@v1',
definition,
}),
);
return {
reference: { kind: 'tool', path },
bytes,
descriptor: {
path,
bytes: bytes.byteLength,
digest: createHash('sha256').update(bytes).digest('hex'),
},
};
});
const workflowResources = workflows.map((value) => {
const path = `workflows/${value.id}.json`;
const bytes = Buffer.from(JSON.stringify(value));
return {
reference: { kind: 'workflow', path },
bytes,
descriptor: {
path,
bytes: bytes.byteLength,
digest: createHash('sha256').update(bytes).digest('hex'),
},
};
});
const promptResources = prompts.map((value) => {
const path = `prompts/${value.id}.json`;
const bytes = Buffer.from(JSON.stringify(value));
return {
reference: { kind: 'prompt', path },
bytes,
descriptor: {
path,
bytes: bytes.byteLength,
digest: createHash('sha256').update(bytes).digest('hex'),
},
};
});
const resources = [
...taskResources,
...workflowResources,
...promptResources,
...toolResources,
].sort((left, right) =>
left.reference.path.localeCompare(right.reference.path),
);
const contentDigest = pluginPackageContentTreeDigest(
resources.map(({ descriptor }) => descriptor),
);
const manifest = {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: packageName,
displayName: packageName,
version: `${generation}.0.0`,
description: 'Task reconciliation contract package',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: [profile],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '8Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: [
...new Set([
'system.command',
...tools.flatMap(
({ requiredPermissions = [] }) => requiredPermissions,
),
]),
].sort(),
},
contents: {
tasks: taskResources.map(({ reference }) => reference.path),
workflows: workflowResources.map(({ reference }) => reference.path),
prompts: promptResources.map(({ reference }) => reference.path),
tools: toolResources.map(({ reference }) => reference.path),
},
},
};
const installEnvironment = environment(profile);
const plan = planPluginPackageInstall(
manifest,
installEnvironment,
previous?.manifest,
);
const action = {
lockId: `lock-${namespace}-${generation}`,
projectId,
manifest,
plan,
environment: installEnvironment,
...(previous ? { previousManifest: previous.manifest } : {}),
source: {
kind: 'offline',
locator: `offline:sha256:${createHash('sha256')
.update(`artifact-${namespace}-${generation}`)
.digest('hex')}`,
artifactDigest: createHash('sha256')
.update(`artifact-${namespace}-${generation}`)
.digest('hex'),
artifactBytes: 2048,
contentDigest,
},
architecture: 'arm64',
deploymentProfile: profile,
targetGeneration: generation,
...(previous ? { previousLockDigest: previous.lock.lockDigest } : {}),
};
const lock = createPluginPackageLock({
...action,
approval: {
requestId: `approval-${namespace}-${generation}`,
requestVersion: 1,
dispatchId: `dispatch-${namespace}-${generation}`,
actionDigest: pluginPackageInstallActionDigest(action),
previewDigest: pluginPackageInstallPlanDigest(plan),
approvedBy: { type: 'user', id: 'owner-001' },
approvedAtMs: 100,
expiresAtMs: 100_000,
fence: { projectVersion: 1, bindingVersion: 1 },
},
createdAtMs: 200 + generation,
});
const generationRecord = createPluginPackageResourceGenerationFromReferences({
installationId: `install-${namespace}-${generation}`,
projectId,
packageName,
lockDigest: lock.lockDigest,
generation,
previousActiveLockDigest: previous?.lock.lockDigest ?? null,
contentDigest,
resources: lock.resources,
});
const registry =
previous?.registry ?? createBuiltInTaskSpecSemanticRegistry();
const revision = materializePluginPackageResources({
generation: generationRecord,
lock,
manifestBytes: Buffer.from(serializePluginPackageManifest(manifest)),
resources: resources.map(({ reference, bytes }) => ({ reference, bytes })),
taskSpecSemanticRegistry: registry,
});
const install = activeRecord(
lock,
namespace,
previous?.install.active ?? null,
);
return {
namespace,
profile,
projectId,
packageName,
generation,
manifest,
lock,
revision,
registry,
install,
manifestBytes: Buffer.from(serializePluginPackageManifest(manifest)),
resourceEntries: resources.map(({ reference, bytes }) => ({
reference,
bytes,
})),
};
}
async function activateInstall(repository, value) {
await repository.create(value.install.create);
for (const command of value.install.commits) {
await repository.commit(command);
}
}
function publisherProvenanceInstallRepository(repository, provenance) {
return Object.freeze({
find: (...args) => repository.find(...args),
findLock: (...args) => repository.findLock(...args),
create: (...args) => repository.create(...args),
listRecoveryPage: (...args) => repository.listRecoveryPage(...args),
async commit(command) {
if (command.record.state !== 'staged') {
return repository.commit(command);
}
const lock = await repository.findLock(command.record.lockDigest);
assert.ok(lock);
assert.ok(command.record.stageReceipt);
return provenance.commitStage(
command,
createPluginPackagePublisherProvenance({
projectId: command.record.projectId,
packageName: command.record.packageName,
installationId: command.record.installationId,
lockDigest: command.record.lockDigest,
artifactDigest: command.record.stageReceipt.artifactDigest,
manifestDigest: command.record.stageReceipt.manifestDigest,
contentDigest: command.record.stageReceipt.contentDigest,
stageEvidenceDigest: command.record.stageReceipt.evidenceDigest,
signature: {
publisher: 'packages.contract.qinglong.dev',
keyId: 'contract-key-1',
signatureDigest: createHash('sha256')
.update(`signature:${command.record.installationId}`)
.digest('hex'),
keyNotBeforeMs: 0,
keyNotAfterMs: 100_000,
verifiedAtMs: command.record.updatedAtMs,
},
}),
'cluster',
);
},
});
}
function registerPluginPackageTaskReconciliationRepositoryContract(options) {
test(`${options.name} reconciles one complete generation and exact replay`, async (t) => {
const first = fixture(`${options.namespace}-create`, {
profile: options.profile,
});
const harness = await options.createRepository(t, first);
t.after(() => harness.close?.());
await activateInstall(harness.installRepository, first);
await harness.materializedRepository.publish(first.revision);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [
{ projectId: first.projectId, packageName: first.packageName },
],
truncated: false,
});
const source = {
async findActiveResourceGeneration() {
return first.revision.generation;
},
};
const created = await harness.repository.reconcile(first.revision, source);
assert.equal(created.status, 'created');
assert.deepEqual(
created.receipt.items.map(({ disposition }) => disposition),
['created', 'created'],
);
assert.equal(
(await harness.repository.reconcile(first.revision, source)).status,
'existing',
);
assert.deepEqual(
await harness.repository.find(first.revision.generation.generationDigest),
created.receipt,
);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [],
truncated: false,
});
await options.assertGenericWriteRejected?.(harness, first);
});
test(`${options.name} retains, disables and creates as one next generation`, async (t) => {
const first = fixture(`${options.namespace}-upgrade`, {
profile: options.profile,
});
const harness = await options.createRepository(t, first);
t.after(() => harness.close?.());
await activateInstall(harness.installRepository, first);
await harness.materializedRepository.publish(first.revision);
await harness.repository.reconcile(first.revision, {
async findActiveResourceGeneration() {
return first.revision.generation;
},
});
const second = fixture(first.namespace, {
profile: options.profile,
previous: first,
tasks: [
['alpha', 'alpha'],
['gamma', 'gamma'],
],
});
await activateInstall(harness.installRepository, second);
await harness.materializedRepository.publish(second.revision);
const reconciled = await harness.repository.reconcile(second.revision, {
async findActiveResourceGeneration() {
return second.revision.generation;
},
});
assert.deepEqual(
reconciled.receipt.items.map(({ taskId, revision, disposition }) => ({
taskId,
revision,
disposition,
})),
[
{
taskId: `pkg:${first.packageName}:alpha`,
revision: 1,
disposition: 'retained',
},
{
taskId: `pkg:${first.packageName}:beta`,
revision: 2,
disposition: 'disabled',
},
{
taskId: `pkg:${first.packageName}:gamma`,
revision: 1,
disposition: 'created',
},
],
);
await options.assertDurableUpgrade?.(harness, second);
});
}
module.exports = {
activateInstall,
pluginPackageTaskReconciliationFixture: fixture,
publisherProvenanceInstallRepository,
registerPluginPackageTaskReconciliationRepositoryContract,
};
@@ -0,0 +1,226 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createProjectToolDefinitionSnapshot,
projectToolDefinitionSnapshotContribution,
InvalidProjectToolDefinitionSnapshotError,
ProjectToolDefinitionSnapshotConflictError,
} = require('../../packages/ql3-runtime-core/dist/tool-execution/tool-registry/projectToolDefinitionSnapshot');
const {
createBuiltInTaskSpecSemanticRegistry,
} = require('../../packages/ql3-runtime-core/dist/task-definition/taskSpecSemantic');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('./pluginPackageTaskReconciliationRepositoryContract.cjs');
function snapshotFor(value) {
return createProjectToolDefinitionSnapshot({
projectId: value.projectId,
contributions: [
projectToolDefinitionSnapshotContribution(value.revision, value.registry),
],
});
}
async function pendingProjectIds(repository) {
const projects = [];
let after;
for (let pageNumber = 0; pageNumber < 8; pageNumber += 1) {
const page = await repository.listPendingProjectPage({
limit: 64,
...(after ? { after } : {}),
});
projects.push(...page.projectIds);
if (!page.truncated) return projects;
after = page.next;
}
throw new Error('snapshot pending Project contract did not converge');
}
function registerProjectToolDefinitionSnapshotRepositoryContract(options) {
test(`${options.name}: publishes and exactly replays an empty active vector`, async (t) => {
const projectId = `${options.namespace}-empty`;
const registry = createBuiltInTaskSpecSemanticRegistry();
const harness = await options.createRepository(t, { projectId, registry });
t.after(() => harness.close?.());
const snapshot = createProjectToolDefinitionSnapshot({
projectId,
contributions: [],
});
assert.deepEqual(
await harness.repository.listActiveSourcePage({
projectId,
limit: 1,
}),
{ sources: [], truncated: false },
);
assert.equal(
(await pendingProjectIds(harness.repository)).includes(projectId),
true,
);
assert.equal(await harness.repository.findCurrent(projectId), null);
const created = await harness.repository.publish(snapshot);
assert.equal(created.status, 'created');
assert.deepEqual(created.record.snapshot, snapshot);
assert.ok(Number.isSafeInteger(created.record.committedAtMs));
const replay = await harness.repository.publish(snapshot);
assert.equal(replay.status, 'existing');
assert.deepEqual(replay.record, created.record);
assert.deepEqual(
await harness.repository.findCurrent(projectId),
created.record,
);
assert.equal(
(await pendingProjectIds(harness.repository)).includes(projectId),
false,
);
});
test(`${options.name}: binds one active Package to its immutable revision`, async (t) => {
const value = pluginPackageTaskReconciliationFixture(
`${options.namespace}-active`,
{ profile: options.profile },
);
const harness = await options.createRepository(t, value);
t.after(() => harness.close?.());
await activateInstall(harness.installRepository, value);
await harness.materializedRepository.publish(value.revision);
const snapshot = snapshotFor(value);
const sourcePage = await harness.repository.listActiveSourcePage({
projectId: value.projectId,
limit: 1,
});
assert.deepEqual(sourcePage, {
sources: snapshot.sources,
truncated: false,
});
assert.equal(
(await pendingProjectIds(harness.repository)).includes(value.projectId),
true,
);
const created = await harness.repository.publish(snapshot);
assert.equal(created.status, 'created');
assert.deepEqual(
await harness.repository.findCurrent(value.projectId),
created.record,
);
assert.deepEqual(created.record.snapshot.sources, [
{
installationId: value.install.active.installationId,
packageName: value.packageName,
generation: value.generation,
generationDigest: value.revision.generation.generationDigest,
lockDigest: value.lock.lockDigest,
revisionDigest: value.revision.revisionDigest,
},
]);
assert.equal(
(await pendingProjectIds(harness.repository)).includes(value.projectId),
false,
);
await options.assertDurableSource?.(harness, value, created.record);
});
test(`${options.name}: keeps the old snapshot during staging and fails closed after activation`, async (t) => {
const first = pluginPackageTaskReconciliationFixture(
`${options.namespace}-upgrade`,
{ profile: options.profile },
);
const harness = await options.createRepository(t, first);
t.after(() => harness.close?.());
await activateInstall(harness.installRepository, first);
await harness.materializedRepository.publish(first.revision);
const firstSnapshot = snapshotFor(first);
const firstRecord = (await harness.repository.publish(firstSnapshot))
.record;
const second = pluginPackageTaskReconciliationFixture(first.namespace, {
profile: options.profile,
previous: first,
tasks: [['alpha', 'next']],
});
await harness.materializedRepository.publish(second.revision);
await harness.installRepository.create(second.install.create);
assert.deepEqual(
await harness.repository.findCurrent(first.projectId),
firstRecord,
);
await harness.installRepository.commit(second.install.commits[0]);
await harness.installRepository.commit(second.install.commits[1]);
assert.deepEqual(
await harness.repository.findCurrent(first.projectId),
firstRecord,
);
assert.deepEqual(
(
await harness.repository.listActiveSourcePage({
projectId: first.projectId,
limit: 1,
})
).sources,
firstRecord.snapshot.sources,
);
await harness.installRepository.commit(second.install.commits[2]);
assert.equal(await harness.repository.findCurrent(first.projectId), null);
assert.deepEqual(
(
await harness.repository.listActiveSourcePage({
projectId: first.projectId,
limit: 1,
})
).sources,
secondSnapshotSources(second),
);
assert.equal(
(await pendingProjectIds(harness.repository)).includes(first.projectId),
true,
);
await assert.rejects(
harness.repository.publish(firstSnapshot),
ProjectToolDefinitionSnapshotConflictError,
);
const secondSnapshot = snapshotFor(second);
const secondRecord = (await harness.repository.publish(secondSnapshot))
.record;
assert.deepEqual(
await harness.repository.findCurrent(first.projectId),
secondRecord,
);
assert.notEqual(
firstRecord.snapshot.activeVectorDigest,
secondRecord.snapshot.activeVectorDigest,
);
assert.equal(
(await pendingProjectIds(harness.repository)).includes(first.projectId),
false,
);
});
test(`${options.name}: rejects invalid Project identity`, async (t) => {
const projectId = `${options.namespace}-invalid`;
const registry = createBuiltInTaskSpecSemanticRegistry();
const harness = await options.createRepository(t, { projectId, registry });
t.after(() => harness.close?.());
await assert.rejects(
Promise.resolve().then(() =>
harness.repository.findCurrent('project\0invalid'),
),
InvalidProjectToolDefinitionSnapshotError,
);
});
}
function secondSnapshotSources(value) {
return snapshotFor(value).sources;
}
module.exports = {
registerProjectToolDefinitionSnapshotRepositoryContract,
projectToolDefinitionSnapshotForFixture: snapshotFor,
};
+392
View File
@@ -0,0 +1,392 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
DuplicateIdempotencyKeyError,
DuplicateRunAttemptError,
DuplicateRunEventError,
RunEventPayloadTooLargeError,
} = require('../../back/runtime/domain/repositoryErrors');
const {
MAX_CANCELLATION_RECOVERY_PAGE_SIZE,
MAX_RUN_EVENT_PAGE_SIZE,
MAX_RUN_EVENT_PAYLOAD_BYTES,
} = require('../../back/runtime/ports/runRepository');
const legacyRepositoryContract = Object.freeze({
DuplicateIdempotencyKeyError,
DuplicateRunAttemptError,
DuplicateRunEventError,
RunEventPayloadTooLargeError,
MAX_CANCELLATION_RECOVERY_PAGE_SIZE,
MAX_RUN_EVENT_PAGE_SIZE,
MAX_RUN_EVENT_PAYLOAD_BYTES,
});
function registerRunRepositoryContract({
name,
createRepository,
defaultExecutionOwner = 'legacy',
contract = legacyRepositoryContract,
}) {
const {
DuplicateIdempotencyKeyError,
DuplicateRunAttemptError,
DuplicateRunEventError,
RunEventPayloadTooLargeError,
MAX_CANCELLATION_RECOVERY_PAGE_SIZE,
MAX_RUN_EVENT_PAGE_SIZE,
MAX_RUN_EVENT_PAYLOAD_BYTES,
} = contract;
let idSequence = 100;
function nextId() {
idSequence += 1;
return `019f70b0-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
function createRun(overrides = {}) {
return {
id: nextId(),
projectId: 'default',
taskId: 'legacy-cron:1',
taskRevision: 'revision-1',
taskName: 'test cron',
legacyCronId: 1,
triggerType: 'manual',
executionOrigin: 'manual',
executionOwner: defaultExecutionOwner,
triggeredBy: 'user:1',
status: 'created',
version: 0,
eventSequence: 0,
priority: 0,
createdAtMs: 1_750_000_000_000,
...overrides,
};
}
function createAttempt(runId, overrides = {}) {
return {
id: nextId(),
runId,
attempt: 1,
status: 'claimed',
executorType: 'legacy_local',
callbackSequence: 0,
createdAtMs: 1_750_000_000_001,
...overrides,
};
}
function createEvent(runId, overrides = {}) {
return {
id: nextId(),
runId,
sequence: 1,
type: 'run.created',
dedupeKey: 'run.created',
actorType: 'compatibility',
payload: { source: 'repository-contract-test' },
createdAtMs: 1_750_000_000_002,
...overrides,
};
}
function createRetryPolicy(runId, overrides = {}) {
return {
runId,
maxAttempts: 3,
retryOnLost: true,
safety: 'idempotent',
backoffBaseMs: 1_000,
backoffMaxMs: 30_000,
version: 0,
createdAtMs: 1_750_000_000_002,
updatedAtMs: 1_750_000_000_002,
...overrides,
};
}
async function setup(t) {
const harness = await createRepository();
if (harness.close) {
t.after(() => harness.close());
}
return harness.repository;
}
test(`${name}: persists a Run aggregate atomically and returns plain records`, async (t) => {
const repository = await setup(t);
const run = createRun({ idempotencyKey: 'manual-request-1' });
const attempt = createAttempt(run.id, {
deadlineAtMs: 1_750_000_030_000,
});
const event = createEvent(run.id, { attemptId: attempt.id });
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
await transaction.insertAttempt(attempt);
await transaction.appendEvent(event);
assert.deepEqual(await transaction.findRunById(run.id), run);
assert.deepEqual(await transaction.findAttemptById(attempt.id), attempt);
});
assert.deepEqual(await repository.findRunById(run.id), run);
assert.deepEqual(await repository.findAttemptById(attempt.id), attempt);
assert.deepEqual(await repository.listEvents(run.id), [event]);
});
test(`${name}: rolls back every aggregate write when a transaction fails`, async (t) => {
const repository = await setup(t);
const run = createRun();
const attempt = createAttempt(run.id);
await assert.rejects(
repository.transaction(async (transaction) => {
await transaction.insertRun(run);
await transaction.insertAttempt(attempt);
throw new Error('force rollback');
}),
/force rollback/,
);
assert.equal(await repository.findRunById(run.id), null);
assert.equal(await repository.findAttemptById(attempt.id), null);
});
test(`${name}: enforces Run and Attempt compare-and-set predicates`, async (t) => {
const repository = await setup(t);
const run = createRun();
const attempt = createAttempt(run.id);
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
await transaction.insertAttempt(attempt);
});
const updatedRun = {
...run,
status: 'queued',
version: 1,
queuedAtMs: 1_750_000_000_003,
};
const updatedAttempt = {
...attempt,
status: 'starting',
callbackSequence: 1,
startedAtMs: 1_750_000_000_004,
};
await repository.transaction(async (transaction) => {
assert.equal(await transaction.compareAndSetRun(updatedRun, 0), true);
assert.equal(await transaction.compareAndSetRun(updatedRun, 0), false);
assert.equal(
await transaction.compareAndSetAttempt(updatedAttempt, {
status: 'claimed',
callbackSequence: 0,
}),
true,
);
assert.equal(
await transaction.compareAndSetAttempt(updatedAttempt, {
status: 'claimed',
callbackSequence: 0,
}),
false,
);
});
assert.deepEqual(await repository.findRunById(run.id), updatedRun);
assert.deepEqual(
await repository.findAttemptById(attempt.id),
updatedAttempt,
);
});
test(`${name}: maps stable uniqueness violations to domain errors`, async (t) => {
const repository = await setup(t);
const run = createRun({ idempotencyKey: 'manual-request-2' });
const attempt = createAttempt(run.id);
const event = createEvent(run.id);
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
await transaction.insertAttempt(attempt);
await transaction.appendEvent(event);
});
await assert.rejects(
repository.transaction((transaction) =>
transaction.insertRun(
createRun({ idempotencyKey: 'manual-request-2' }),
),
),
DuplicateIdempotencyKeyError,
);
await assert.rejects(
repository.transaction((transaction) =>
transaction.insertAttempt(
createAttempt(run.id, { attempt: attempt.attempt }),
),
),
DuplicateRunAttemptError,
);
await assert.rejects(
repository.transaction((transaction) =>
transaction.appendEvent(
createEvent(run.id, {
sequence: 2,
dedupeKey: event.dedupeKey,
}),
),
),
DuplicateRunEventError,
);
});
test(`${name}: persists and compare-and-sets the admitted retry policy`, async (t) => {
const repository = await setup(t);
const run = createRun();
const policy = createRetryPolicy(run.id);
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
await transaction.insertRetryPolicy(policy);
assert.deepEqual(
await transaction.findRetryPolicyByRunId(run.id),
policy,
);
});
const updated = {
...policy,
nextAttemptAtMs: 1_750_000_010_000,
version: 1,
updatedAtMs: 1_750_000_000_003,
};
await repository.transaction(async (transaction) => {
assert.equal(
await transaction.compareAndSetRetryPolicy(updated, 0),
true,
);
assert.equal(
await transaction.compareAndSetRetryPolicy(updated, 0),
false,
);
});
assert.deepEqual(await repository.findRetryPolicyByRunId(run.id), updated);
await assert.rejects(
repository.transaction((transaction) =>
transaction.compareAndSetRetryPolicy(
{ ...updated, version: 3, updatedAtMs: 1_750_000_000_004 },
1,
),
),
(error) => error.code === 'RUN_REPOSITORY_CONSTRAINT',
);
});
test(`${name}: bounds event pages and rejects oversized payloads`, async (t) => {
const repository = await setup(t);
const run = createRun();
await repository.transaction(async (transaction) => {
await transaction.insertRun(run);
await transaction.appendEvent(createEvent(run.id));
await transaction.appendEvent(
createEvent(run.id, {
sequence: 2,
dedupeKey: 'run.queued',
type: 'run.queued',
}),
);
await transaction.appendEvent(
createEvent(run.id, {
sequence: 3,
dedupeKey: 'attempt.claimed',
type: 'attempt.claimed',
}),
);
});
const page = await repository.listEvents(run.id, {
afterSequence: 1,
limit: 2,
});
assert.deepEqual(
page.map((event) => event.sequence),
[2, 3],
);
await assert.rejects(
repository.listEvents(run.id, { limit: MAX_RUN_EVENT_PAGE_SIZE + 1 }),
RangeError,
);
const oversized = createEvent(run.id, {
sequence: 4,
dedupeKey: 'oversized',
payload: { value: 'x'.repeat(MAX_RUN_EVENT_PAYLOAD_BYTES) },
});
await assert.rejects(
repository.transaction((transaction) =>
transaction.appendEvent(oversized),
),
RunEventPayloadTooLargeError,
);
assert.deepEqual(
(await repository.listEvents(run.id)).map((event) => event.sequence),
[1, 2, 3],
);
});
test(`${name}: returns bounded cancellation requests in recovery order`, async (t) => {
const repository = await setup(t);
const earlier = createRun({
status: 'running',
cancelRequestedAtMs: 1_750_000_000_010,
cancelReason: 'user',
});
const later = createRun({
status: 'dispatching',
cancelRequestedAtMs: 1_750_000_000_020,
cancelReason: 'shutdown',
});
const terminal = createRun({
status: 'cancelled',
cancelRequestedAtMs: 1_750_000_000_005,
cancelReason: 'policy',
finishedAtMs: 1_750_000_000_006,
});
const untouched = createRun({ status: 'running' });
await repository.transaction(async (transaction) => {
for (const run of [later, terminal, untouched, earlier]) {
await transaction.insertRun(run);
}
});
assert.deepEqual(
(await repository.listCancellationRequested()).map((run) => run.id),
[earlier.id, later.id],
);
assert.deepEqual(
(
await repository.listCancellationRequested({
beforeMs: 1_750_000_000_015,
limit: 1,
})
).map((run) => run.id),
[earlier.id],
);
await assert.rejects(
repository.listCancellationRequested({ beforeMs: -1 }),
RangeError,
);
await assert.rejects(
repository.listCancellationRequested({
limit: MAX_CANCELLATION_RECOVERY_PAGE_SIZE + 1,
}),
RangeError,
);
});
}
module.exports = { registerRunRepositoryContract };