mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
191 lines
5.8 KiB
JavaScript
191 lines
5.8 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
PluginPackagePromptOutputKeyRotationConflictError,
|
|
PluginPackagePromptOutputKeyRotationCoordinator,
|
|
PluginPackagePromptOutputKeyRotationUnavailableError,
|
|
createPluginPackagePromptOutputKeyRotationCompletion,
|
|
createPluginPackagePromptOutputKeyRotationPreparation,
|
|
normalizePluginPackagePromptOutputKeyRotationCompletion,
|
|
normalizePluginPackagePromptOutputKeyRotationPreparation,
|
|
pluginPackagePromptOutputKeyRotationMaterialProof,
|
|
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyRotation.js');
|
|
|
|
function request() {
|
|
return {
|
|
rotationId: 'rotation-001',
|
|
requestId: 'request-001',
|
|
mutationId: 'mutation-001',
|
|
expectedSecretUid: 'secret-uid-001',
|
|
expectedActiveKeyId: 'prompt-key-one',
|
|
expectedCatalogDigest: '1'.repeat(64),
|
|
newKeyId: 'prompt-key-two',
|
|
};
|
|
}
|
|
|
|
function state(material) {
|
|
return {
|
|
generation: 2,
|
|
previousActiveKeyId: 'prompt-key-one',
|
|
activeKeyId: 'prompt-key-two',
|
|
catalogDigest: '2'.repeat(64),
|
|
materialProof: pluginPackagePromptOutputKeyRotationMaterialProof(
|
|
'prompt-key-two',
|
|
material,
|
|
),
|
|
};
|
|
}
|
|
|
|
function memoryRepository(options = {}) {
|
|
let record = null;
|
|
let clock = 1_000;
|
|
let failedAfterComplete = false;
|
|
return {
|
|
get record() {
|
|
return record;
|
|
},
|
|
async find(rotationId) {
|
|
return record?.preparation.rotationId === rotationId ? record : null;
|
|
},
|
|
async prepare(command) {
|
|
const candidate = createPluginPackagePromptOutputKeyRotationPreparation({
|
|
...command,
|
|
preparedAtMs: clock++,
|
|
});
|
|
if (record) {
|
|
return { status: 'existing', preparation: record.preparation };
|
|
}
|
|
record = { preparation: candidate, completion: null };
|
|
return { status: 'created', preparation: candidate };
|
|
},
|
|
async complete(command) {
|
|
if (record.completion) {
|
|
return { status: 'existing', completion: record.completion };
|
|
}
|
|
const completion = createPluginPackagePromptOutputKeyRotationCompletion({
|
|
...command,
|
|
completedAtMs: clock++,
|
|
});
|
|
record = { ...record, completion };
|
|
if (options.failOnceAfterComplete && !failedAfterComplete) {
|
|
failedAfterComplete = true;
|
|
throw new PluginPackagePromptOutputKeyRotationUnavailableError();
|
|
}
|
|
return { status: 'created', completion };
|
|
},
|
|
};
|
|
}
|
|
|
|
test('creates content-free rotation facts and exact replay skips material mutation', async () => {
|
|
const material = Buffer.alloc(32, 0x42);
|
|
const repository = memoryRepository();
|
|
let rotations = 0;
|
|
const coordinator = new PluginPackagePromptOutputKeyRotationCoordinator({
|
|
repository,
|
|
materials: {
|
|
async rotate() {
|
|
rotations += 1;
|
|
return state(material);
|
|
},
|
|
},
|
|
});
|
|
|
|
const completed = await coordinator.rotate({ request: request(), material });
|
|
assert.equal(completed.status, 'completed');
|
|
assert.equal(rotations, 1);
|
|
const replay = await coordinator.rotate({ request: request(), material });
|
|
assert.equal(replay.status, 'existing');
|
|
assert.equal(rotations, 1);
|
|
assert.deepEqual(replay.completion, completed.completion);
|
|
assert.deepEqual(
|
|
normalizePluginPackagePromptOutputKeyRotationPreparation(
|
|
completed.preparation,
|
|
),
|
|
completed.preparation,
|
|
);
|
|
assert.deepEqual(
|
|
normalizePluginPackagePromptOutputKeyRotationCompletion(
|
|
completed.completion,
|
|
),
|
|
completed.completion,
|
|
);
|
|
const durable = JSON.stringify(repository.record);
|
|
assert.equal(durable.includes(material.toString('base64url')), false);
|
|
assert.equal(durable.includes(material.toString('hex')), false);
|
|
});
|
|
|
|
test('resumes after preparation and converges after completion response loss', async () => {
|
|
const material = Buffer.alloc(32, 0x51);
|
|
const repository = memoryRepository({ failOnceAfterComplete: true });
|
|
await repository.prepare({
|
|
request: request(),
|
|
materialProof: pluginPackagePromptOutputKeyRotationMaterialProof(
|
|
request().newKeyId,
|
|
material,
|
|
),
|
|
});
|
|
let rotations = 0;
|
|
const coordinator = new PluginPackagePromptOutputKeyRotationCoordinator({
|
|
repository,
|
|
materials: {
|
|
async rotate() {
|
|
rotations += 1;
|
|
return state(material);
|
|
},
|
|
},
|
|
});
|
|
|
|
await assert.rejects(
|
|
coordinator.rotate({ request: request(), material }),
|
|
PluginPackagePromptOutputKeyRotationUnavailableError,
|
|
);
|
|
assert.equal(rotations, 1);
|
|
const replay = await coordinator.rotate({ request: request(), material });
|
|
assert.equal(replay.status, 'existing');
|
|
assert.equal(rotations, 1);
|
|
});
|
|
|
|
test('a prepared rotation rejects changed staged material and drifted successor', async () => {
|
|
const original = Buffer.alloc(32, 0x61);
|
|
const changed = Buffer.alloc(32, 0x62);
|
|
const repository = memoryRepository();
|
|
await repository.prepare({
|
|
request: request(),
|
|
materialProof: pluginPackagePromptOutputKeyRotationMaterialProof(
|
|
request().newKeyId,
|
|
original,
|
|
),
|
|
});
|
|
const coordinator = new PluginPackagePromptOutputKeyRotationCoordinator({
|
|
repository,
|
|
materials: {
|
|
async rotate() {
|
|
return state(original);
|
|
},
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
coordinator.rotate({ request: request(), material: changed }),
|
|
PluginPackagePromptOutputKeyRotationConflictError,
|
|
);
|
|
|
|
const fresh = memoryRepository();
|
|
const drifted = new PluginPackagePromptOutputKeyRotationCoordinator({
|
|
repository: fresh,
|
|
materials: {
|
|
async rotate() {
|
|
return {
|
|
...state(original),
|
|
catalogDigest: '3'.repeat(64),
|
|
activeKeyId: 'other-key',
|
|
};
|
|
},
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
drifted.rotate({ request: request(), material: original }),
|
|
PluginPackagePromptOutputKeyRotationConflictError,
|
|
);
|
|
});
|