mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createPluginPackagePromptOutputArtifact,
|
||||
pluginPackagePromptOutputArtifactReference,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputArtifact.js');
|
||||
const {
|
||||
PostgresPluginPackagePromptOutputGarbageCollector,
|
||||
PostgresPluginPackagePromptOutputRetentionRepository,
|
||||
assertPostgresPluginPackagePromptOutputMaintenanceReady,
|
||||
} = require('../dist/prompt-output/storage/postgresPluginPackagePromptOutputRetentionRepository.js');
|
||||
const {
|
||||
PostgresModelInvocationRepository,
|
||||
} = require('../dist/model-invocation/postgresModelInvocationRepository.js');
|
||||
|
||||
function artifact() {
|
||||
return createPluginPackagePromptOutputArtifact(
|
||||
{
|
||||
projectId: 'project-a',
|
||||
runId: 'run-a',
|
||||
stepRunId: 'step-a',
|
||||
invocationId: 'invocation-a',
|
||||
requestedBy: { type: 'user', id: 'user-a' },
|
||||
result: {
|
||||
provider: 'openai-compatible',
|
||||
model: 'model-a',
|
||||
text: 'private PostgreSQL GC output',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 2, outputTokens: 3, totalTokens: 5 },
|
||||
},
|
||||
retentionPolicy: {
|
||||
revision: 'cluster-v1',
|
||||
retentionMs: 3_600_000,
|
||||
},
|
||||
keyId: 'cluster-key-1',
|
||||
key: Buffer.alloc(32, 7),
|
||||
sealedAtMs: 1_000,
|
||||
},
|
||||
() => Buffer.alloc(12, 9),
|
||||
);
|
||||
}
|
||||
|
||||
function artifactRow(value) {
|
||||
return {
|
||||
artifactId: value.artifactId,
|
||||
projectId: value.projectId,
|
||||
runId: value.runId,
|
||||
stepRunId: value.stepRunId,
|
||||
invocationId: value.invocationId,
|
||||
requestedByType: value.requestedBy.type,
|
||||
requestedById: value.requestedBy.id,
|
||||
provider: value.provider,
|
||||
model: value.model,
|
||||
contentDigest: value.contentDigest,
|
||||
outputBytes: value.outputBytes,
|
||||
retentionPolicyRevision: value.retentionPolicy.revision,
|
||||
retentionMs: String(value.retentionPolicy.retentionMs),
|
||||
retentionPolicyDigest: value.retentionPolicyDigest,
|
||||
retentionEligibleAtMs: String(value.retentionEligibleAtMs),
|
||||
keyId: value.keyId,
|
||||
algorithm: value.algorithm,
|
||||
plaintextBytes: value.plaintextBytes,
|
||||
sealedAtMs: String(value.sealedAtMs),
|
||||
artifactDigest: value.artifactDigest,
|
||||
artifactJson: value,
|
||||
};
|
||||
}
|
||||
|
||||
function tombstoneRow(value) {
|
||||
return {
|
||||
artifactId: value.reference.artifactId,
|
||||
projectId: value.reference.projectId,
|
||||
runId: value.reference.runId,
|
||||
stepRunId: value.reference.stepRunId,
|
||||
invocationId: value.reference.invocationId,
|
||||
artifactDigest: value.reference.artifactDigest,
|
||||
retentionPolicyDigest: value.reference.retentionPolicyDigest,
|
||||
retentionEligibleAtMs: String(value.reference.retentionEligibleAtMs),
|
||||
keyId: value.reference.keyId,
|
||||
tombstonedAtMs: String(value.tombstonedAtMs),
|
||||
tombstoneDigest: value.tombstoneDigest,
|
||||
tombstoneJson: value,
|
||||
};
|
||||
}
|
||||
|
||||
function storagePool(initialArtifact, options = {}) {
|
||||
let storedArtifact = initialArtifact;
|
||||
let storedTombstone = null;
|
||||
const queries = [];
|
||||
const query = async (sql, parameters = []) => {
|
||||
queries.push({ sql, parameters });
|
||||
if (sql.includes('clock_timestamp()')) {
|
||||
return { rows: [{ observedAtMs: '4000000' }], rowCount: 1 };
|
||||
}
|
||||
if (
|
||||
sql.startsWith('SELECT artifact_id AS "artifactId"') &&
|
||||
sql.includes('retention_eligible_at_ms <=')
|
||||
) {
|
||||
return {
|
||||
rows: storedArtifact ? [{ artifactId: storedArtifact.artifactId }] : [],
|
||||
};
|
||||
}
|
||||
if (
|
||||
sql.includes('model_invocation_prompt_output_artifacts') &&
|
||||
sql.includes('artifact_json AS "artifactJson"')
|
||||
) {
|
||||
return {
|
||||
rows:
|
||||
storedArtifact && storedArtifact.artifactId === parameters[0]
|
||||
? [artifactRow(storedArtifact)]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
if (sql.includes('AS "completionOutcome"')) {
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
runStatus: options.runStatus ?? 'succeeded',
|
||||
stepStatus: 'succeeded',
|
||||
outputRef: storedArtifact?.artifactId,
|
||||
completionOutcome: 'succeeded',
|
||||
finalizationStatus: 'succeeded',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (sql.includes('model_invocation_prompt_output_artifact_tombstones')) {
|
||||
if (sql.startsWith('SELECT artifact_id')) {
|
||||
return {
|
||||
rows:
|
||||
storedTombstone &&
|
||||
storedTombstone.reference.artifactId === parameters[0]
|
||||
? [tombstoneRow(storedTombstone)]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
if (sql.startsWith('INSERT INTO')) {
|
||||
storedTombstone = JSON.parse(parameters[11]);
|
||||
return { rows: [], rowCount: 1 };
|
||||
}
|
||||
}
|
||||
if (sql.startsWith('DELETE FROM')) {
|
||||
if (
|
||||
storedArtifact?.artifactId === parameters[0] &&
|
||||
storedArtifact.artifactDigest === parameters[1]
|
||||
) {
|
||||
storedArtifact = null;
|
||||
return { rows: [], rowCount: 1 };
|
||||
}
|
||||
return { rows: [], rowCount: 0 };
|
||||
}
|
||||
return { rows: [], rowCount: 0 };
|
||||
};
|
||||
const client = { query, release() {} };
|
||||
return {
|
||||
queries,
|
||||
get artifact() {
|
||||
return storedArtifact;
|
||||
},
|
||||
get tombstone() {
|
||||
return storedTombstone;
|
||||
},
|
||||
query,
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('PostgreSQL GC uses database time and atomically replaces ciphertext with a content-free tombstone', async () => {
|
||||
const output = artifact();
|
||||
const pool = storagePool(output);
|
||||
const collector = new PostgresPluginPackagePromptOutputGarbageCollector({
|
||||
pool,
|
||||
policies: {
|
||||
async resolve() {
|
||||
return output.retentionPolicy;
|
||||
},
|
||||
},
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
assert.deepEqual(await collector.collect(), {
|
||||
scanned: 1,
|
||||
tombstoned: 1,
|
||||
skipped: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
assert.equal(pool.artifact, null);
|
||||
assert.deepEqual(
|
||||
pool.tombstone.reference,
|
||||
pluginPackagePromptOutputArtifactReference(output),
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(pool.tombstone).includes(output.ciphertext),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(pool.tombstone).includes('private PostgreSQL GC output'),
|
||||
false,
|
||||
);
|
||||
const insertIndex = pool.queries.findIndex(({ sql }) =>
|
||||
sql.startsWith('INSERT INTO'),
|
||||
);
|
||||
const deleteIndex = pool.queries.findIndex(({ sql }) =>
|
||||
sql.startsWith('DELETE FROM'),
|
||||
);
|
||||
assert.ok(insertIndex >= 0 && insertIndex < deleteIndex);
|
||||
assert.ok(
|
||||
pool.queries.some(({ sql }) =>
|
||||
sql.startsWith('BEGIN ISOLATION LEVEL SERIALIZABLE'),
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
pool.queries.some(({ sql }) => sql.includes('pg_advisory_xact_lock')),
|
||||
);
|
||||
|
||||
const retention = new PostgresPluginPackagePromptOutputRetentionRepository(
|
||||
pool,
|
||||
);
|
||||
const state = await retention.inspect({
|
||||
reference: pluginPackagePromptOutputArtifactReference(output),
|
||||
observedAtMs: 4_000_001,
|
||||
});
|
||||
assert.equal(state.state, 'tombstoned');
|
||||
assert.equal(state.tombstoneDigest, pool.tombstone.tombstoneDigest);
|
||||
|
||||
const replay = new PostgresModelInvocationRepository(pool);
|
||||
assert.deepEqual(
|
||||
await replay.findPromptOutputArtifactTombstone(output.artifactId),
|
||||
pool.tombstone,
|
||||
);
|
||||
});
|
||||
|
||||
test('PostgreSQL GC skips non-terminal or policy-drifted artifacts without deleting ciphertext', async () => {
|
||||
const output = artifact();
|
||||
const nonTerminal = storagePool(output, { runStatus: 'running' });
|
||||
const terminalCollector =
|
||||
new PostgresPluginPackagePromptOutputGarbageCollector({
|
||||
pool: nonTerminal,
|
||||
policies: {
|
||||
async resolve() {
|
||||
return output.retentionPolicy;
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await terminalCollector.collect(), {
|
||||
scanned: 1,
|
||||
tombstoned: 0,
|
||||
skipped: 1,
|
||||
hasMore: false,
|
||||
});
|
||||
assert.deepEqual(nonTerminal.artifact, output);
|
||||
assert.equal(nonTerminal.tombstone, null);
|
||||
|
||||
const drifted = storagePool(output);
|
||||
const policyCollector = new PostgresPluginPackagePromptOutputGarbageCollector(
|
||||
{
|
||||
pool: drifted,
|
||||
policies: {
|
||||
async resolve() {
|
||||
return { revision: 'cluster-v2', retentionMs: 3_600_000 };
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.deepEqual(await policyCollector.collect(), {
|
||||
scanned: 1,
|
||||
tombstoned: 0,
|
||||
skipped: 1,
|
||||
hasMore: false,
|
||||
});
|
||||
assert.deepEqual(drifted.artifact, output);
|
||||
});
|
||||
|
||||
test('PostgreSQL GC maintenance readiness requires the exact delete-only authority', async () => {
|
||||
const report = await assertPostgresPluginPackagePromptOutputMaintenanceReady({
|
||||
async query(sql) {
|
||||
assert.match(sql, /ql3_ai_maintenance/);
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
currentUser: 'ql3_ai_maintenance',
|
||||
maintenanceAuthority: true,
|
||||
schemaAuthority: true,
|
||||
artifactDeleteOnly: true,
|
||||
tombstoneAppendOnly: true,
|
||||
keyRetirementAppendOnly: true,
|
||||
keyRotationAppendOnly: true,
|
||||
terminalEvidenceReadOnly: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.deepEqual(report, {
|
||||
currentUser: 'ql3_ai_maintenance',
|
||||
maintenanceAuthority: true,
|
||||
artifactDeleteOnly: true,
|
||||
tombstoneAppendOnly: true,
|
||||
keyRetirementAppendOnly: true,
|
||||
keyRotationAppendOnly: true,
|
||||
terminalEvidenceReadOnly: true,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user