Files
qinglong/packages/ql3-cluster-admin/test/promptOutputKeyRotationProcess.test.cjs
T

184 lines
5.8 KiB
JavaScript

const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
ClusterPromptOutputKeyRotationProcessConfigError,
runClusterPromptOutputKeyRotationProcess,
} = require('../dist/prompt-output/key-management/promptOutputKeyRotationProcess.js');
const {
pluginPackagePromptOutputKeyRotationMaterialProof,
} = require('@qinglong/ai/plugin-package-prompt-output-key-rotation');
function request() {
return {
rotationId: 'rotation-process-1',
requestId: 'rotation-process-request-1',
mutationId: 'rotation-process-mutation-1',
expectedSecretUid: 'rotation-secret-uid-1',
expectedActiveKeyId: 'key-before',
expectedCatalogDigest: '1'.repeat(64),
newKeyId: 'key-after',
};
}
function fakeDatabase(material) {
let preparation;
let completion;
let closed = 0;
const client = {
async query(sql, parameters = []) {
if (sql.includes('current_user AS "currentUser"')) {
return {
rows: [
{
currentUser: 'ql3_ai_maintenance',
maintenanceAuthority: true,
schemaAuthority: true,
artifactDeleteOnly: true,
tombstoneAppendOnly: true,
keyRetirementAppendOnly: true,
keyRotationAppendOnly: true,
terminalEvidenceReadOnly: true,
},
],
};
}
if (sql.includes('key_rotation_preparations') && sql.includes('SELECT')) {
return {
rows: preparation
? [
{
rotationId: preparation.rotationId,
requestId: preparation.requestId,
mutationId: preparation.mutationId,
expectedSecretUid: preparation.expectedSecretUid,
expectedActiveKeyId: preparation.expectedActiveKeyId,
expectedCatalogDigest: preparation.expectedCatalogDigest,
newKeyId: preparation.newKeyId,
materialProof: preparation.materialProof,
preparedAtMs: String(preparation.preparedAtMs),
preparationDigest: preparation.preparationDigest,
preparationJson: preparation,
},
]
: [],
};
}
if (sql.includes('key_rotation_completions') && sql.includes('SELECT')) {
return {
rows: completion
? [
{
rotationId: completion.rotationId,
requestId: completion.requestId,
mutationId: completion.mutationId,
preparationDigest: completion.preparationDigest,
generation: String(completion.generation),
previousActiveKeyId: completion.previousActiveKeyId,
activeKeyId: completion.activeKeyId,
catalogDigest: completion.catalogDigest,
materialProof: completion.materialProof,
completedAtMs: String(completion.completedAtMs),
completionDigest: completion.completionDigest,
completionJson: completion,
},
]
: [],
};
}
if (
sql.includes('INSERT INTO') &&
sql.includes('key_rotation_preparations')
) {
preparation = JSON.parse(parameters[10]);
}
if (
sql.includes('INSERT INTO') &&
sql.includes('key_rotation_completions')
) {
completion = JSON.parse(parameters[11]);
}
return { rows: [], rowCount: 1 };
},
release() {},
};
const pool = {
async connect() {
return client;
},
async query(sql, parameters) {
return client.query(sql, parameters);
},
};
return {
database: {
pool,
async close() {
closed += 1;
},
},
get closed() {
return closed;
},
get completion() {
return completion;
},
materials: {
async rotate(command) {
return {
generation: 2,
previousActiveKeyId: command.expectedActiveKeyId,
activeKeyId: command.newKeyId,
catalogDigest: '2'.repeat(64),
materialProof: pluginPackagePromptOutputKeyRotationMaterialProof(
command.newKeyId,
material,
),
};
},
},
};
}
test('Cluster rotation process commits content-free prepare and completion', async () => {
const material = Buffer.alloc(32, 0x71);
const fixture = fakeDatabase(material);
const result = await runClusterPromptOutputKeyRotationProcess({
database: { connection: { connectionString: 'postgres://unused' } },
request: request(),
material,
materials: fixture.materials,
openDatabase: async () => fixture.database,
});
assert.equal(result.status, 'completed');
assert.equal(result.generation, 2);
assert.equal(result.activeKeyId, 'key-after');
assert.equal(result.readiness.keyRotationAppendOnly, true);
assert.equal(fixture.closed, 1);
const durable = JSON.stringify(fixture.completion);
assert.equal(durable.includes(material.toString('base64url')), false);
assert.equal(durable.includes(material.toString('hex')), false);
});
test('Cluster rotation process rejects invalid material before opening PostgreSQL', async () => {
let opened = false;
await assert.rejects(
runClusterPromptOutputKeyRotationProcess({
database: { connection: { connectionString: 'postgres://unused' } },
request: request(),
material: Buffer.alloc(31),
materials: {
async rotate() {
throw new Error('unreachable');
},
},
openDatabase: async () => {
opened = true;
throw new Error('must not open');
},
}),
ClusterPromptOutputKeyRotationProcessConfigError,
);
assert.equal(opened, false);
});