mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
307 lines
8.9 KiB
JavaScript
307 lines
8.9 KiB
JavaScript
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,
|
|
});
|
|
});
|