mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 10:32:40 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const {
|
||||
createStepRunRecord,
|
||||
transitionStepRunMutation,
|
||||
} = require('@qinglong/runtime-core/step-run');
|
||||
const {
|
||||
createModelInvocationCompletionCommand,
|
||||
createModelInvocationMutationIdentity,
|
||||
createModelInvocationStartCommand,
|
||||
} = require('../../dist/model-invocation/modelInvocation.js');
|
||||
const {
|
||||
LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
|
||||
migrateLocalModelInvocationFeature,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
const {
|
||||
LocalModelInvocationFeatureActivationRepository,
|
||||
createLocalModelInvocationFeatureTransitionCommand,
|
||||
} = require('@qinglong/ai/local-feature-activation');
|
||||
const {
|
||||
LocalModelInvocationRepository,
|
||||
} = require('../../dist/model-invocation/localModelInvocationRepository.js');
|
||||
|
||||
const NOW = 3_000_000;
|
||||
|
||||
const CRASH_POINTS = Object.freeze({
|
||||
start_before_begin: Object.freeze({
|
||||
operation: 'start',
|
||||
timing: 'beforeExec',
|
||||
sql: 'BEGIN IMMEDIATE',
|
||||
durable: false,
|
||||
}),
|
||||
start_after_mutation: Object.freeze({
|
||||
operation: 'start',
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "StepRunMutations"',
|
||||
durable: false,
|
||||
}),
|
||||
start_after_fact: Object.freeze({
|
||||
operation: 'start',
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "ModelInvocationStarts"',
|
||||
durable: false,
|
||||
}),
|
||||
start_after_commit: Object.freeze({
|
||||
operation: 'start',
|
||||
timing: 'afterExec',
|
||||
sql: 'COMMIT',
|
||||
durable: true,
|
||||
}),
|
||||
completion_after_mutation: Object.freeze({
|
||||
operation: 'completion',
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "StepRunMutations"',
|
||||
durable: false,
|
||||
}),
|
||||
completion_after_fact: Object.freeze({
|
||||
operation: 'completion',
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "ModelInvocationCompletions"',
|
||||
durable: false,
|
||||
}),
|
||||
completion_after_commit: Object.freeze({
|
||||
operation: 'completion',
|
||||
timing: 'afterExec',
|
||||
sql: 'COMMIT',
|
||||
durable: true,
|
||||
}),
|
||||
});
|
||||
|
||||
function openClient(databasePath, profile) {
|
||||
const client = new DatabaseSync(databasePath);
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
client.exec('PRAGMA busy_timeout = 5000');
|
||||
client.exec(`PRAGMA journal_mode = ${profile === 'edge' ? 'DELETE' : 'WAL'}`);
|
||||
client.exec('PRAGMA synchronous = FULL');
|
||||
return client;
|
||||
}
|
||||
|
||||
function createMainContract(client) {
|
||||
client.exec(`
|
||||
CREATE TABLE "QingLong3SchemaMigrations" (
|
||||
migration_id TEXT PRIMARY KEY,
|
||||
stream_id TEXT NOT NULL,
|
||||
dialect TEXT NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE "Runs" (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
event_sequence INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE "RunEvents" (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
sequence INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
dedupe_key TEXT NOT NULL,
|
||||
actor_type TEXT NOT NULL,
|
||||
actor_id TEXT,
|
||||
attempt_id TEXT,
|
||||
step_run_id TEXT,
|
||||
payload TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
UNIQUE (run_id, sequence),
|
||||
UNIQUE (run_id, dedupe_key)
|
||||
);
|
||||
CREATE TABLE "StepRuns" (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
attempt_count INTEGER NOT NULL,
|
||||
output_ref TEXT,
|
||||
approval_request_id TEXT,
|
||||
ready_at_ms INTEGER,
|
||||
started_at_ms INTEGER,
|
||||
finished_at_ms INTEGER,
|
||||
result_code TEXT,
|
||||
error_summary TEXT,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
last_mutation_id TEXT NOT NULL,
|
||||
step_run_digest TEXT NOT NULL,
|
||||
step_run_json TEXT NOT NULL,
|
||||
UNIQUE (run_id, id)
|
||||
);
|
||||
CREATE TABLE "StepRunMutations" (
|
||||
mutation_id TEXT PRIMARY KEY,
|
||||
mutation_digest TEXT NOT NULL,
|
||||
run_id TEXT NOT NULL,
|
||||
step_run_id TEXT NOT NULL,
|
||||
step_run_digest TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL,
|
||||
event_sequence INTEGER NOT NULL,
|
||||
run_version INTEGER NOT NULL,
|
||||
step_run_json TEXT NOT NULL,
|
||||
committed_at_ms INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
function audit(phase, overrides = {}) {
|
||||
return {
|
||||
phase,
|
||||
projectId: 'crash-project',
|
||||
runId: 'crash-run',
|
||||
stepRunId: 'crash-step',
|
||||
traceId: 'crash-trace',
|
||||
requestId: 'crash-request',
|
||||
provider: 'remote',
|
||||
model: 'crash-model',
|
||||
policyRevision: 'policy-1',
|
||||
requestDigest: `sha256:${'b'.repeat(64)}`,
|
||||
deadlineAtMs: NOW + 10_000,
|
||||
inputBytes: 128,
|
||||
maxOutputTokens: 64,
|
||||
outputBytes: 0,
|
||||
usage: null,
|
||||
errorCode: null,
|
||||
occurredAtMs: NOW,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function startCommand(ready) {
|
||||
const identity = createModelInvocationMutationIdentity(
|
||||
'crash-request',
|
||||
'start',
|
||||
);
|
||||
return createModelInvocationStartCommand(
|
||||
audit('admitted'),
|
||||
transitionStepRunMutation(
|
||||
ready,
|
||||
{
|
||||
expectedVersion: ready.version,
|
||||
expectedDigest: ready.stepRunDigest,
|
||||
mutationId: identity.mutationId,
|
||||
to: 'running',
|
||||
atMs: NOW,
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 1,
|
||||
expectedRunEventSequence: 1,
|
||||
eventId: identity.eventId,
|
||||
dedupeKey: identity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function completionCommand(start) {
|
||||
const identity = createModelInvocationMutationIdentity(
|
||||
'crash-request',
|
||||
'completion',
|
||||
);
|
||||
return createModelInvocationCompletionCommand(
|
||||
start.start,
|
||||
audit('completed', {
|
||||
occurredAtMs: NOW + 25,
|
||||
outputBytes: 12,
|
||||
usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
|
||||
}),
|
||||
transitionStepRunMutation(
|
||||
start.stepRunMutation.stepRun,
|
||||
{
|
||||
expectedVersion: start.start.startedStepRunVersion,
|
||||
expectedDigest: start.start.startedStepRunDigest,
|
||||
mutationId: identity.mutationId,
|
||||
to: 'succeeded',
|
||||
atMs: NOW + 25,
|
||||
outputRef: 'model-invocation:crash-request',
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 2,
|
||||
expectedRunEventSequence: 2,
|
||||
eventId: identity.eventId,
|
||||
dedupeKey: identity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function setupScenario({ databasePath, statePath, profile, operation }) {
|
||||
const client = openClient(databasePath, profile);
|
||||
try {
|
||||
createMainContract(client);
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
new LocalModelInvocationFeatureActivationRepository(client).transition(
|
||||
createLocalModelInvocationFeatureTransitionCommand({
|
||||
featureId: 'model-invocation',
|
||||
expectedGeneration: 0,
|
||||
expectedState: null,
|
||||
state: 'active',
|
||||
mutationId: 'model-invocation-crash-feature-activation',
|
||||
requestId: 'model-invocation-crash-feature-request',
|
||||
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
|
||||
safety: {
|
||||
mode: 'fresh_database',
|
||||
backupEvidenceDigest: null,
|
||||
},
|
||||
principal: {
|
||||
subject: { type: 'user', id: 'test-owner' },
|
||||
authenticationId: 'local_ai_feature:crash-proof',
|
||||
authenticatedAtMs: 1,
|
||||
expiresAtMs: 301_000,
|
||||
assurance: 'local_console',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const ready = createStepRunRecord({
|
||||
id: 'crash-step',
|
||||
runId: 'crash-run',
|
||||
stepKey: 'summarize',
|
||||
kind: 'model',
|
||||
definitionRef: 'prompt:crash@1',
|
||||
definitionDigest: 'a'.repeat(64),
|
||||
required: true,
|
||||
initialStatus: 'ready',
|
||||
inputRef: 'artifact:crash-input',
|
||||
mutationId: 'create-crash-step',
|
||||
createdAtMs: NOW - 1,
|
||||
});
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "Runs"
|
||||
(id, project_id, status, version, event_sequence)
|
||||
VALUES ('crash-run', 'crash-project', 'running', 1, 1)`,
|
||||
)
|
||||
.run();
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "StepRuns" (
|
||||
id, run_id, kind, status, version, attempt_count, output_ref,
|
||||
approval_request_id, ready_at_ms, started_at_ms, finished_at_ms,
|
||||
result_code, error_summary, updated_at_ms, last_mutation_id,
|
||||
step_run_digest, step_run_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
ready.id,
|
||||
ready.runId,
|
||||
ready.kind,
|
||||
ready.status,
|
||||
ready.version,
|
||||
ready.attemptCount,
|
||||
ready.outputRef,
|
||||
ready.approvalRequestId,
|
||||
ready.readyAtMs,
|
||||
ready.startedAtMs,
|
||||
ready.finishedAtMs,
|
||||
ready.resultCode,
|
||||
ready.errorSummary,
|
||||
ready.updatedAtMs,
|
||||
ready.lastMutationId,
|
||||
ready.stepRunDigest,
|
||||
JSON.stringify(ready),
|
||||
);
|
||||
const start = startCommand(ready);
|
||||
const completion = completionCommand(start);
|
||||
if (operation === 'completion') {
|
||||
assert.equal(
|
||||
(await new LocalModelInvocationRepository(client).admit(start)).status,
|
||||
'created',
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
statePath,
|
||||
JSON.stringify({ profile, operation, start, completion }),
|
||||
{ encoding: 'utf8', flag: 'wx', mode: 0o600 },
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
function writeCrashMarker(markerPath, pointName) {
|
||||
const file = fs.openSync(markerPath, 'wx', 0o600);
|
||||
try {
|
||||
fs.writeSync(
|
||||
file,
|
||||
JSON.stringify({
|
||||
schema: 'qinglong/sqlite-model-invocation-crash-marker@v1',
|
||||
point: pointName,
|
||||
pid: process.pid,
|
||||
}),
|
||||
);
|
||||
fs.fsyncSync(file);
|
||||
} finally {
|
||||
fs.closeSync(file);
|
||||
}
|
||||
}
|
||||
|
||||
function crashClient(client, pointName, markerPath) {
|
||||
const point = CRASH_POINTS[pointName];
|
||||
if (!point) throw new Error(`unknown crash point ${pointName}`);
|
||||
let triggered = false;
|
||||
const crash = () => {
|
||||
if (triggered) return;
|
||||
triggered = true;
|
||||
writeCrashMarker(markerPath, pointName);
|
||||
process.kill(process.pid, 'SIGKILL');
|
||||
throw new Error(`SIGKILL did not terminate ${pointName}`);
|
||||
};
|
||||
const matches = (timing, sql) =>
|
||||
!triggered && point.timing === timing && sql.trim().includes(point.sql);
|
||||
return new Proxy(client, {
|
||||
get(target, property) {
|
||||
if (property === 'exec') {
|
||||
return (sql) => {
|
||||
if (matches('beforeExec', sql)) crash();
|
||||
const result = target.exec(sql);
|
||||
if (matches('afterExec', sql)) crash();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
if (property === 'prepare') {
|
||||
return (sql) => {
|
||||
const statement = target.prepare(sql);
|
||||
return new Proxy(statement, {
|
||||
get(statementTarget, statementProperty) {
|
||||
const value = Reflect.get(
|
||||
statementTarget,
|
||||
statementProperty,
|
||||
statementTarget,
|
||||
);
|
||||
if (statementProperty === 'run') {
|
||||
return (...values) => {
|
||||
const result = value.apply(statementTarget, values);
|
||||
if (matches('afterRun', sql)) crash();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
return typeof value === 'function'
|
||||
? value.bind(statementTarget)
|
||||
: value;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
const value = Reflect.get(target, property, target);
|
||||
return typeof value === 'function' ? value.bind(target) : value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function runCrashScenario({
|
||||
databasePath,
|
||||
statePath,
|
||||
markerPath,
|
||||
pointName,
|
||||
}) {
|
||||
const point = CRASH_POINTS[pointName];
|
||||
if (!point) throw new Error(`unknown crash point ${pointName}`);
|
||||
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
||||
if (state.operation !== point.operation) {
|
||||
throw new Error(
|
||||
`crash point ${pointName} does not match ${state.operation}`,
|
||||
);
|
||||
}
|
||||
const client = openClient(databasePath, state.profile);
|
||||
const repository = new LocalModelInvocationRepository(
|
||||
crashClient(client, pointName, markerPath),
|
||||
);
|
||||
if (point.operation === 'start') {
|
||||
await repository.admit(state.start);
|
||||
} else {
|
||||
await repository.complete(state.completion);
|
||||
}
|
||||
throw new Error(`crash point ${pointName} was not reached`);
|
||||
}
|
||||
|
||||
function facts(client) {
|
||||
return {
|
||||
...client
|
||||
.prepare(
|
||||
`SELECT
|
||||
step.status AS "stepStatus",
|
||||
step.version AS "stepVersion",
|
||||
run.version AS "runVersion",
|
||||
run.event_sequence AS "runEventSequence",
|
||||
(SELECT count(*) FROM "RunEvents") AS "eventCount",
|
||||
(SELECT count(*) FROM "StepRunMutations") AS "mutationCount",
|
||||
(SELECT count(*) FROM "ModelInvocationStarts") AS "startCount",
|
||||
(SELECT count(*) FROM "ModelInvocationCompletions")
|
||||
AS "completionCount"
|
||||
FROM "StepRuns" AS step
|
||||
JOIN "Runs" AS run ON run.id = step.run_id
|
||||
WHERE step.id = 'crash-step' AND run.id = 'crash-run'`,
|
||||
)
|
||||
.get(),
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyScenario({ databasePath, statePath, pointName }) {
|
||||
const point = CRASH_POINTS[pointName];
|
||||
if (!point) throw new Error(`unknown crash point ${pointName}`);
|
||||
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
||||
const client = openClient(databasePath, state.profile);
|
||||
const repository = new LocalModelInvocationRepository(client);
|
||||
try {
|
||||
assert.equal(
|
||||
client.prepare('PRAGMA integrity_check').get().integrity_check,
|
||||
'ok',
|
||||
);
|
||||
assert.equal(
|
||||
client.prepare('PRAGMA journal_mode').get().journal_mode,
|
||||
state.profile === 'edge' ? 'delete' : 'wal',
|
||||
);
|
||||
const operation = point.operation;
|
||||
const before = facts(client);
|
||||
if (operation === 'start') {
|
||||
assert.deepEqual(
|
||||
before,
|
||||
point.durable
|
||||
? {
|
||||
stepStatus: 'running',
|
||||
stepVersion: 2,
|
||||
runVersion: 2,
|
||||
runEventSequence: 2,
|
||||
eventCount: 1,
|
||||
mutationCount: 1,
|
||||
startCount: 1,
|
||||
completionCount: 0,
|
||||
}
|
||||
: {
|
||||
stepStatus: 'ready',
|
||||
stepVersion: 1,
|
||||
runVersion: 1,
|
||||
runEventSequence: 1,
|
||||
eventCount: 0,
|
||||
mutationCount: 0,
|
||||
startCount: 0,
|
||||
completionCount: 0,
|
||||
},
|
||||
);
|
||||
const replay = await repository.admit(state.start);
|
||||
assert.equal(replay.status, point.durable ? 'existing' : 'created');
|
||||
assert.equal((await repository.admit(state.start)).status, 'existing');
|
||||
} else {
|
||||
assert.deepEqual(
|
||||
before,
|
||||
point.durable
|
||||
? {
|
||||
stepStatus: 'succeeded',
|
||||
stepVersion: 3,
|
||||
runVersion: 3,
|
||||
runEventSequence: 3,
|
||||
eventCount: 2,
|
||||
mutationCount: 2,
|
||||
startCount: 1,
|
||||
completionCount: 1,
|
||||
}
|
||||
: {
|
||||
stepStatus: 'running',
|
||||
stepVersion: 2,
|
||||
runVersion: 2,
|
||||
runEventSequence: 2,
|
||||
eventCount: 1,
|
||||
mutationCount: 1,
|
||||
startCount: 1,
|
||||
completionCount: 0,
|
||||
},
|
||||
);
|
||||
const replay = await repository.complete(state.completion);
|
||||
assert.equal(replay.status, point.durable ? 'existing' : 'created');
|
||||
assert.equal(
|
||||
(await repository.complete(state.completion)).status,
|
||||
'existing',
|
||||
);
|
||||
}
|
||||
const after = facts(client);
|
||||
assert.equal(
|
||||
after.stepStatus,
|
||||
operation === 'start' ? 'running' : 'succeeded',
|
||||
);
|
||||
assert.equal(after.startCount, 1);
|
||||
assert.equal(after.completionCount, operation === 'start' ? 0 : 1);
|
||||
return Object.freeze({
|
||||
profile: state.profile,
|
||||
point: pointName,
|
||||
crashBeforeCommit: !point.durable,
|
||||
durableAfterCrash: point.durable,
|
||||
journalMode: client.prepare('PRAGMA journal_mode').get().journal_mode,
|
||||
integrityCheck: 'ok',
|
||||
});
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const [mode, databasePath, statePath, markerPath, pointName] =
|
||||
process.argv.slice(2);
|
||||
if (mode !== 'crash') throw new Error(`unknown mode ${mode}`);
|
||||
runCrashScenario({
|
||||
databasePath,
|
||||
statePath,
|
||||
markerPath,
|
||||
pointName,
|
||||
}).catch((error) => {
|
||||
process.stderr.write(`${error.stack ?? error.message}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CRASH_POINTS,
|
||||
setupScenario,
|
||||
verifyScenario,
|
||||
};
|
||||
@@ -0,0 +1,597 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
||||
const {
|
||||
LocalSqlitePluginPackageInstallRepository,
|
||||
} = require('@qinglong/local-sqlite/plugin-package-install');
|
||||
const {
|
||||
LocalSqlitePluginPackageMaterializedRevisionRepository,
|
||||
} = require('@qinglong/local-sqlite/plugin-package-materialized-revision');
|
||||
const {
|
||||
LocalSqlitePluginPackageTaskReconciliationRepository,
|
||||
} = require('@qinglong/local-sqlite/plugin-package-task-reconciliation');
|
||||
const {
|
||||
LocalSqlitePluginPackageAutomationPublicationRepository,
|
||||
} = require('@qinglong/local-sqlite/plugin-package-automation-publication');
|
||||
const {
|
||||
createInitialPluginPackageAutomationPublication,
|
||||
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
|
||||
const {
|
||||
activateInstall,
|
||||
pluginPackageTaskReconciliationFixture,
|
||||
} = require('../../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
|
||||
const {
|
||||
LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
|
||||
migrateLocalModelInvocationFeature,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
const {
|
||||
LocalModelInvocationFeatureActivationRepository,
|
||||
createLocalModelInvocationFeatureTransitionCommand,
|
||||
} = require('@qinglong/ai/local-feature-activation');
|
||||
const {
|
||||
LocalPluginPackagePromptAdmissionRepository,
|
||||
} = require('../../dist/prompt/localPluginPackagePromptAdmissionRepository.js');
|
||||
const {
|
||||
LocalModelInvocationRepository,
|
||||
} = require('../../dist/model-invocation/localModelInvocationRepository.js');
|
||||
const {
|
||||
DurableModelInvocationCoordinator,
|
||||
} = require('../../dist/model-invocation/durableModelInvocationCoordinator.js');
|
||||
const { BoundedModelGateway } = require('../../dist/model-gateway/gateway.js');
|
||||
const {
|
||||
preparePluginPackagePromptExecution,
|
||||
} = require('../../dist/prompt/pluginPackagePromptExecution.js');
|
||||
|
||||
const CRASH_POINTS = Object.freeze({
|
||||
admission_before_begin: Object.freeze({
|
||||
operation: 'admission',
|
||||
timing: 'beforeExec',
|
||||
sql: 'BEGIN IMMEDIATE',
|
||||
durable: false,
|
||||
}),
|
||||
admission_after_run: Object.freeze({
|
||||
operation: 'admission',
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "Runs"',
|
||||
durable: false,
|
||||
}),
|
||||
admission_after_step_mutation: Object.freeze({
|
||||
operation: 'admission',
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "StepRunMutations"',
|
||||
durable: false,
|
||||
}),
|
||||
admission_after_fact: Object.freeze({
|
||||
operation: 'admission',
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "ModelInvocationPromptAdmissions"',
|
||||
durable: false,
|
||||
}),
|
||||
admission_after_commit: Object.freeze({
|
||||
operation: 'admission',
|
||||
timing: 'afterExec',
|
||||
sql: 'COMMIT',
|
||||
durable: true,
|
||||
}),
|
||||
finalization_before_begin: Object.freeze({
|
||||
operation: 'finalization',
|
||||
timing: 'beforeExec',
|
||||
sql: 'BEGIN IMMEDIATE',
|
||||
durable: false,
|
||||
}),
|
||||
finalization_after_run: Object.freeze({
|
||||
operation: 'finalization',
|
||||
timing: 'afterRun',
|
||||
sql: 'UPDATE "Runs"',
|
||||
durable: false,
|
||||
}),
|
||||
finalization_after_event: Object.freeze({
|
||||
operation: 'finalization',
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "RunEvents"',
|
||||
durable: false,
|
||||
}),
|
||||
finalization_after_fact: Object.freeze({
|
||||
operation: 'finalization',
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "ModelInvocationPromptFinalizations"',
|
||||
durable: false,
|
||||
}),
|
||||
finalization_after_commit: Object.freeze({
|
||||
operation: 'finalization',
|
||||
timing: 'afterExec',
|
||||
sql: 'COMMIT',
|
||||
durable: true,
|
||||
}),
|
||||
});
|
||||
|
||||
function openClient(databasePath, profile) {
|
||||
const client = new DatabaseSync(databasePath);
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
client.exec('PRAGMA busy_timeout = 5000');
|
||||
client.exec(
|
||||
'PRAGMA journal_mode = ' + (profile === 'edge' ? 'DELETE' : 'WAL'),
|
||||
);
|
||||
client.exec('PRAGMA synchronous = FULL');
|
||||
return client;
|
||||
}
|
||||
|
||||
function promptResource() {
|
||||
return {
|
||||
schema: 'qinglong/plugin-package-prompt-resource@v1',
|
||||
id: 'summary',
|
||||
name: 'Summary',
|
||||
template: 'Summarize {{subject}} for {{audience}}.',
|
||||
parameters: [
|
||||
{ name: 'audience', required: false },
|
||||
{ name: 'subject', required: true },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function seedPublication(client, profile) {
|
||||
const fixture = pluginPackageTaskReconciliationFixture(
|
||||
'prompt-crash-matrix',
|
||||
{
|
||||
profile,
|
||||
tasks: [],
|
||||
prompts: [promptResource()],
|
||||
},
|
||||
);
|
||||
const publication = createInitialPluginPackageAutomationPublication(
|
||||
fixture.revision,
|
||||
fixture.registry,
|
||||
2_000,
|
||||
);
|
||||
client
|
||||
.prepare(
|
||||
'INSERT INTO "QingLong3Projects" ' +
|
||||
'(id, name, slug, status, version, created_at_ms, updated_at_ms) ' +
|
||||
"VALUES (?, ?, ?, 'active', 1, 1, 1)",
|
||||
)
|
||||
.run(fixture.projectId, fixture.projectId, fixture.projectId);
|
||||
await activateInstall(
|
||||
new LocalSqlitePluginPackageInstallRepository(client),
|
||||
fixture,
|
||||
);
|
||||
await new LocalSqlitePluginPackageMaterializedRevisionRepository(
|
||||
client,
|
||||
fixture.registry,
|
||||
).publish(fixture.revision);
|
||||
await new LocalSqlitePluginPackageTaskReconciliationRepository(
|
||||
client,
|
||||
fixture.registry,
|
||||
).reconcile(fixture.revision, {
|
||||
async findActiveResourceGeneration() {
|
||||
return fixture.revision.generation;
|
||||
},
|
||||
});
|
||||
await new LocalSqlitePluginPackageAutomationPublicationRepository(
|
||||
client,
|
||||
).publish(publication);
|
||||
return publication;
|
||||
}
|
||||
|
||||
function executionInput(publication) {
|
||||
return {
|
||||
publication,
|
||||
expectedPublicationDigest: publication.publicationDigest,
|
||||
promptId: 'summary',
|
||||
requestId: 'prompt-crash-request',
|
||||
traceId: 'prompt-crash-trace',
|
||||
requestedBySubject: { type: 'user', id: 'prompt-crash-owner' },
|
||||
policyFence: { projectVersion: 1, bindingVersion: 1 },
|
||||
parameters: { subject: 'private crash matrix input' },
|
||||
provider: 'openai-compatible',
|
||||
model: 'vendor/model-a',
|
||||
maxOutputTokens: 512,
|
||||
temperature: 0.2,
|
||||
plannedAtMs: 2_000,
|
||||
deadlineAtMs: 62_000,
|
||||
};
|
||||
}
|
||||
|
||||
function createGateway(repository) {
|
||||
return new BoundedModelGateway({
|
||||
providers: [
|
||||
{
|
||||
type: 'openai-compatible',
|
||||
async listModels() {
|
||||
return [{ id: 'vendor/model-a' }];
|
||||
},
|
||||
async generate() {
|
||||
return {
|
||||
provider: 'openai-compatible',
|
||||
model: 'vendor/model-a',
|
||||
text: 'private crash matrix output',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 7, outputTokens: 3, totalTokens: 10 },
|
||||
};
|
||||
},
|
||||
async *stream() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
return {
|
||||
revision: 'prompt-crash-policy-1',
|
||||
allowedProviders: ['openai-compatible'],
|
||||
allowedModels: ['vendor/model-a'],
|
||||
maxInputBytes: 4096,
|
||||
maxOutputBytes: 4096,
|
||||
maxOutputTokens: 512,
|
||||
maxTotalTokens: 1024,
|
||||
maxCostMicros: null,
|
||||
priceRevision: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
pricing: {
|
||||
async resolve() {
|
||||
throw new Error('pricing must remain unreachable');
|
||||
},
|
||||
},
|
||||
audit: new DurableModelInvocationCoordinator(repository),
|
||||
maxConcurrent: 1,
|
||||
now: () => 3_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function setupScenario({ databasePath, statePath, profile, operation }) {
|
||||
await migrateLocalSqlitePath({ databasePath, profile });
|
||||
const client = openClient(databasePath, profile);
|
||||
try {
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
new LocalModelInvocationFeatureActivationRepository(client).transition(
|
||||
createLocalModelInvocationFeatureTransitionCommand({
|
||||
featureId: 'model-invocation',
|
||||
expectedGeneration: 0,
|
||||
expectedState: null,
|
||||
state: 'active',
|
||||
mutationId: 'prompt-crash-feature-activation',
|
||||
requestId: 'prompt-crash-feature-request',
|
||||
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
|
||||
safety: {
|
||||
mode: 'fresh_database',
|
||||
backupEvidenceDigest: null,
|
||||
},
|
||||
principal: {
|
||||
subject: { type: 'user', id: 'prompt-crash-owner' },
|
||||
authenticationId: 'local_ai_feature:prompt-crash',
|
||||
authenticatedAtMs: 1,
|
||||
expiresAtMs: 301_000,
|
||||
assurance: 'local_console',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const publication = await seedPublication(client, profile);
|
||||
const prepared = preparePluginPackagePromptExecution(
|
||||
executionInput(publication),
|
||||
);
|
||||
if (operation === 'finalization') {
|
||||
const admissions = new LocalPluginPackagePromptAdmissionRepository(
|
||||
client,
|
||||
);
|
||||
assert.equal((await admissions.admit(prepared.plan)).status, 'created');
|
||||
const invocations = new LocalModelInvocationRepository(client);
|
||||
const result = await createGateway(invocations).generate(
|
||||
prepared.request,
|
||||
{
|
||||
projectId: prepared.plan.target.projectId,
|
||||
runId: prepared.plan.runId,
|
||||
stepRunId: prepared.plan.stepRunId,
|
||||
traceId: prepared.plan.traceId,
|
||||
requestId: prepared.plan.invocationId,
|
||||
deadlineAtMs: prepared.plan.deadlineAtMs,
|
||||
},
|
||||
);
|
||||
assert.equal(result.text, 'private crash matrix output');
|
||||
}
|
||||
fs.writeFileSync(
|
||||
statePath,
|
||||
JSON.stringify({ profile, operation, plan: prepared.plan }),
|
||||
{ encoding: 'utf8', flag: 'wx', mode: 0o600 },
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
function writeCrashMarker(markerPath, pointName) {
|
||||
const file = fs.openSync(markerPath, 'wx', 0o600);
|
||||
try {
|
||||
fs.writeSync(
|
||||
file,
|
||||
JSON.stringify({
|
||||
schema: 'qinglong/sqlite-plugin-package-prompt-crash-marker@v1',
|
||||
point: pointName,
|
||||
pid: process.pid,
|
||||
}),
|
||||
);
|
||||
fs.fsyncSync(file);
|
||||
} finally {
|
||||
fs.closeSync(file);
|
||||
}
|
||||
}
|
||||
|
||||
function crashClient(client, pointName, markerPath) {
|
||||
const point = CRASH_POINTS[pointName];
|
||||
if (!point) throw new Error('unknown crash point ' + pointName);
|
||||
let triggered = false;
|
||||
const crash = () => {
|
||||
if (triggered) return;
|
||||
triggered = true;
|
||||
writeCrashMarker(markerPath, pointName);
|
||||
process.kill(process.pid, 'SIGKILL');
|
||||
throw new Error('SIGKILL did not terminate ' + pointName);
|
||||
};
|
||||
const matches = (timing, sql) =>
|
||||
!triggered && point.timing === timing && sql.trim().includes(point.sql);
|
||||
return new Proxy(client, {
|
||||
get(target, property) {
|
||||
if (property === 'exec') {
|
||||
return (sql) => {
|
||||
if (matches('beforeExec', sql)) crash();
|
||||
const result = target.exec(sql);
|
||||
if (matches('afterExec', sql)) crash();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
if (property === 'prepare') {
|
||||
return (sql) => {
|
||||
const statement = target.prepare(sql);
|
||||
return new Proxy(statement, {
|
||||
get(statementTarget, statementProperty) {
|
||||
const value = Reflect.get(
|
||||
statementTarget,
|
||||
statementProperty,
|
||||
statementTarget,
|
||||
);
|
||||
if (statementProperty === 'run') {
|
||||
return (...values) => {
|
||||
const result = value.apply(statementTarget, values);
|
||||
if (matches('afterRun', sql)) crash();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
return typeof value === 'function'
|
||||
? value.bind(statementTarget)
|
||||
: value;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
const value = Reflect.get(target, property, target);
|
||||
return typeof value === 'function' ? value.bind(target) : value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function runCrashScenario({
|
||||
databasePath,
|
||||
statePath,
|
||||
markerPath,
|
||||
pointName,
|
||||
}) {
|
||||
const point = CRASH_POINTS[pointName];
|
||||
if (!point) throw new Error('unknown crash point ' + pointName);
|
||||
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
||||
if (state.operation !== point.operation) {
|
||||
throw new Error(
|
||||
'crash point ' + pointName + ' does not match ' + state.operation,
|
||||
);
|
||||
}
|
||||
const client = openClient(databasePath, state.profile);
|
||||
const repository = new LocalPluginPackagePromptAdmissionRepository(
|
||||
crashClient(client, pointName, markerPath),
|
||||
);
|
||||
if (point.operation === 'admission') {
|
||||
await repository.admit(state.plan);
|
||||
} else {
|
||||
await repository.finalize(state.plan.requestId);
|
||||
}
|
||||
throw new Error('crash point ' + pointName + ' was not reached');
|
||||
}
|
||||
|
||||
function count(client, sql, value) {
|
||||
return client.prepare(sql).get(value).count;
|
||||
}
|
||||
|
||||
function facts(client, plan) {
|
||||
const run = client
|
||||
.prepare(
|
||||
'SELECT status, version, event_sequence AS "eventSequence" ' +
|
||||
'FROM "Runs" WHERE id = ?',
|
||||
)
|
||||
.get(plan.runId);
|
||||
const step = client
|
||||
.prepare('SELECT kind, status, version FROM "StepRuns" WHERE id = ?')
|
||||
.get(plan.stepRunId);
|
||||
return {
|
||||
run: run ? { ...run } : null,
|
||||
step: step ? { ...step } : null,
|
||||
runEvents: count(
|
||||
client,
|
||||
'SELECT count(*) AS count FROM "RunEvents" WHERE run_id = ?',
|
||||
plan.runId,
|
||||
),
|
||||
attempts: count(
|
||||
client,
|
||||
'SELECT count(*) AS count FROM "RunAttempts" WHERE run_id = ?',
|
||||
plan.runId,
|
||||
),
|
||||
mutations: count(
|
||||
client,
|
||||
'SELECT count(*) AS count FROM "StepRunMutations" WHERE run_id = ?',
|
||||
plan.runId,
|
||||
),
|
||||
starts: count(
|
||||
client,
|
||||
'SELECT count(*) AS count FROM "ModelInvocationStarts" ' +
|
||||
'WHERE invocation_id = ?',
|
||||
plan.invocationId,
|
||||
),
|
||||
completions: count(
|
||||
client,
|
||||
'SELECT count(*) AS count FROM "ModelInvocationCompletions" ' +
|
||||
'WHERE invocation_id = ?',
|
||||
plan.invocationId,
|
||||
),
|
||||
admissions: count(
|
||||
client,
|
||||
'SELECT count(*) AS count FROM "ModelInvocationPromptAdmissions" ' +
|
||||
'WHERE request_id = ?',
|
||||
plan.requestId,
|
||||
),
|
||||
finalizations: count(
|
||||
client,
|
||||
'SELECT count(*) AS count FROM "ModelInvocationPromptFinalizations" ' +
|
||||
'WHERE request_id = ?',
|
||||
plan.requestId,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function emptyFacts() {
|
||||
return {
|
||||
run: null,
|
||||
step: null,
|
||||
runEvents: 0,
|
||||
attempts: 0,
|
||||
mutations: 0,
|
||||
starts: 0,
|
||||
completions: 0,
|
||||
admissions: 0,
|
||||
finalizations: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function admittedFacts() {
|
||||
return {
|
||||
run: { status: 'running', version: 2, eventSequence: 2 },
|
||||
step: { kind: 'model', status: 'ready', version: 1 },
|
||||
runEvents: 2,
|
||||
attempts: 0,
|
||||
mutations: 1,
|
||||
starts: 0,
|
||||
completions: 0,
|
||||
admissions: 1,
|
||||
finalizations: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function modelCompletedFacts() {
|
||||
return {
|
||||
run: { status: 'running', version: 4, eventSequence: 4 },
|
||||
step: { kind: 'model', status: 'succeeded', version: 3 },
|
||||
runEvents: 4,
|
||||
attempts: 0,
|
||||
mutations: 3,
|
||||
starts: 1,
|
||||
completions: 1,
|
||||
admissions: 1,
|
||||
finalizations: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function finalizedFacts() {
|
||||
return {
|
||||
run: { status: 'succeeded', version: 5, eventSequence: 5 },
|
||||
step: { kind: 'model', status: 'succeeded', version: 3 },
|
||||
runEvents: 5,
|
||||
attempts: 0,
|
||||
mutations: 3,
|
||||
starts: 1,
|
||||
completions: 1,
|
||||
admissions: 1,
|
||||
finalizations: 1,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyScenario({ databasePath, statePath, pointName }) {
|
||||
const point = CRASH_POINTS[pointName];
|
||||
if (!point) throw new Error('unknown crash point ' + pointName);
|
||||
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
||||
const client = openClient(databasePath, state.profile);
|
||||
const repository = new LocalPluginPackagePromptAdmissionRepository(client);
|
||||
try {
|
||||
assert.equal(
|
||||
client.prepare('PRAGMA integrity_check').get().integrity_check,
|
||||
'ok',
|
||||
);
|
||||
assert.equal(
|
||||
client.prepare('PRAGMA journal_mode').get().journal_mode,
|
||||
state.profile === 'edge' ? 'delete' : 'wal',
|
||||
);
|
||||
assert.deepEqual(client.prepare('PRAGMA foreign_key_check').all(), []);
|
||||
const before = facts(client, state.plan);
|
||||
if (point.operation === 'admission') {
|
||||
assert.deepEqual(before, point.durable ? admittedFacts() : emptyFacts());
|
||||
const replay = await repository.admit(state.plan);
|
||||
assert.equal(replay.status, point.durable ? 'existing' : 'created');
|
||||
assert.equal((await repository.admit(state.plan)).status, 'existing');
|
||||
assert.deepEqual(facts(client, state.plan), admittedFacts());
|
||||
} else {
|
||||
assert.deepEqual(
|
||||
before,
|
||||
point.durable ? finalizedFacts() : modelCompletedFacts(),
|
||||
);
|
||||
const replay = await repository.finalize(state.plan.requestId);
|
||||
assert.equal(replay.status, point.durable ? 'existing' : 'created');
|
||||
assert.equal(
|
||||
(await repository.finalize(state.plan.requestId)).status,
|
||||
'existing',
|
||||
);
|
||||
assert.deepEqual(facts(client, state.plan), finalizedFacts());
|
||||
}
|
||||
const durableBytes = fs.readFileSync(databasePath);
|
||||
assert.equal(
|
||||
durableBytes.includes(Buffer.from('private crash matrix input')),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
durableBytes.includes(Buffer.from('private crash matrix output')),
|
||||
false,
|
||||
);
|
||||
return Object.freeze({
|
||||
profile: state.profile,
|
||||
operation: point.operation,
|
||||
point: pointName,
|
||||
crashBeforeCommit: !point.durable,
|
||||
durableAfterCrash: point.durable,
|
||||
exactReplay: true,
|
||||
contentFree: true,
|
||||
journalMode: client.prepare('PRAGMA journal_mode').get().journal_mode,
|
||||
integrityCheck: 'ok',
|
||||
foreignKeyCheck: 'ok',
|
||||
physicalPowerLossProven: false,
|
||||
});
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const [mode, databasePath, statePath, markerPath, pointName] =
|
||||
process.argv.slice(2);
|
||||
if (mode !== 'crash') throw new Error('unknown mode ' + mode);
|
||||
runCrashScenario({
|
||||
databasePath,
|
||||
statePath,
|
||||
markerPath,
|
||||
pointName,
|
||||
}).catch((error) => {
|
||||
process.stderr.write((error.stack ?? error.message) + '\n');
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CRASH_POINTS,
|
||||
setupScenario,
|
||||
verifyScenario,
|
||||
};
|
||||
@@ -0,0 +1,602 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
BoundedModelGateway,
|
||||
ModelAuditUnavailableError,
|
||||
ModelBudgetExceededError,
|
||||
ModelGatewayBusyError,
|
||||
ModelInvocationAbortedError,
|
||||
ModelInvocationDeadlineExceededError,
|
||||
ModelInvocationReplayBlockedError,
|
||||
ModelPolicyDeniedError,
|
||||
} = require('../dist/model-gateway/gateway.js');
|
||||
const {
|
||||
ModelPriceUnavailableError,
|
||||
StaticModelPriceCatalog,
|
||||
createModelPriceCatalogEntry,
|
||||
} = require('../dist/pricing/pricing.js');
|
||||
const {
|
||||
InvalidModelValueError,
|
||||
normalizeModelInvocationPolicy,
|
||||
} = require('../dist/model-gateway/validation.js');
|
||||
|
||||
const NOW = 1_000_000;
|
||||
const disabledPricing = Object.freeze({
|
||||
async resolve() {
|
||||
throw new Error('pricing must remain unreachable');
|
||||
},
|
||||
});
|
||||
|
||||
function request(overrides = {}) {
|
||||
return {
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
messages: [{ role: 'user', content: 'top secret prompt' }],
|
||||
maxOutputTokens: 16,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function context(overrides = {}) {
|
||||
return {
|
||||
projectId: 'project-a',
|
||||
runId: 'run-a',
|
||||
stepRunId: 'step-a',
|
||||
traceId: 'trace-a',
|
||||
requestId: 'request-a',
|
||||
deadlineAtMs: NOW + 10_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function policy(overrides = {}) {
|
||||
return {
|
||||
revision: 'policy-1',
|
||||
allowedProviders: ['remote'],
|
||||
allowedModels: ['model-a'],
|
||||
maxInputBytes: 4096,
|
||||
maxOutputBytes: 4096,
|
||||
maxOutputTokens: 64,
|
||||
maxTotalTokens: 256,
|
||||
maxCostMicros: null,
|
||||
priceRevision: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function provider(overrides = {}) {
|
||||
return {
|
||||
type: 'remote',
|
||||
async listModels() {
|
||||
return [{ id: 'model-a' }];
|
||||
},
|
||||
async generate() {
|
||||
return {
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
text: 'safe summary',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
|
||||
};
|
||||
},
|
||||
async *stream() {
|
||||
yield { delta: 'safe ' };
|
||||
yield {
|
||||
delta: 'summary',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
|
||||
};
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('policy rejects an output token limit above its total token limit', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelInvocationPolicy(
|
||||
policy({ maxOutputTokens: 65, maxTotalTokens: 64 }),
|
||||
),
|
||||
InvalidModelValueError,
|
||||
);
|
||||
});
|
||||
|
||||
function gateway({
|
||||
modelProvider = provider(),
|
||||
resolvedPolicy = policy(),
|
||||
auditRecords = [],
|
||||
maxConcurrent = 1,
|
||||
} = {}) {
|
||||
return new BoundedModelGateway({
|
||||
providers: [modelProvider],
|
||||
policies: {
|
||||
async resolve() {
|
||||
return resolvedPolicy;
|
||||
},
|
||||
},
|
||||
pricing: disabledPricing,
|
||||
audit: {
|
||||
async record(record) {
|
||||
auditRecords.push(record);
|
||||
},
|
||||
},
|
||||
maxConcurrent,
|
||||
now: () => NOW,
|
||||
});
|
||||
}
|
||||
|
||||
test('generate binds Project/Run/StepRun and emits content-free bounded audit', async () => {
|
||||
const auditRecords = [];
|
||||
const instance = gateway({ auditRecords });
|
||||
|
||||
const result = await instance.generate(request(), context());
|
||||
|
||||
assert.equal(result.text, 'safe summary');
|
||||
assert.equal(instance.activeInvocations, 0);
|
||||
assert.deepEqual(
|
||||
auditRecords.map((record) => record.phase),
|
||||
['admitted', 'completed'],
|
||||
);
|
||||
assert.equal(auditRecords[0].projectId, 'project-a');
|
||||
assert.equal(auditRecords[0].runId, 'run-a');
|
||||
assert.equal(auditRecords[0].stepRunId, 'step-a');
|
||||
assert.match(auditRecords[0].requestDigest, /^sha256:[0-9a-f]{64}$/);
|
||||
assert.equal(
|
||||
JSON.stringify(auditRecords).includes('top secret prompt'),
|
||||
false,
|
||||
);
|
||||
assert.equal(JSON.stringify(auditRecords).includes('safe summary'), false);
|
||||
assert.deepEqual(auditRecords[1].usage, {
|
||||
inputTokens: 5,
|
||||
outputTokens: 2,
|
||||
totalTokens: 7,
|
||||
});
|
||||
});
|
||||
|
||||
test('priced invocation snapshots one exact revision before provider I/O', async () => {
|
||||
const records = [];
|
||||
let providerCalls = 0;
|
||||
const pricing = new StaticModelPriceCatalog([
|
||||
createModelPriceCatalogEntry({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: 150_000,
|
||||
outputMicrosPerMillionTokens: 600_000,
|
||||
publishedAtMs: NOW - 1,
|
||||
}),
|
||||
]);
|
||||
const instance = new BoundedModelGateway({
|
||||
providers: [
|
||||
provider({
|
||||
async generate() {
|
||||
providerCalls += 1;
|
||||
return {
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
text: 'safe summary',
|
||||
finishReason: 'stop',
|
||||
usage: {
|
||||
inputTokens: 5,
|
||||
outputTokens: 2,
|
||||
totalTokens: 7,
|
||||
costMicros: 99_999,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
return policy({
|
||||
priceRevision: 'price-1',
|
||||
maxCostMicros: 100,
|
||||
projectQuota: {
|
||||
revision: 'quota-1',
|
||||
windowMs: 3_600_000,
|
||||
maxInvocations: 10,
|
||||
maxTokens: 10_000,
|
||||
maxCostMicros: 1_000,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
pricing,
|
||||
audit: {
|
||||
async record(record) {
|
||||
records.push({ record });
|
||||
},
|
||||
async recordWithPricing(record, quote, quotaAdmission) {
|
||||
records.push({ record, quote, quotaAdmission });
|
||||
},
|
||||
},
|
||||
maxConcurrent: 1,
|
||||
now: () => NOW,
|
||||
});
|
||||
|
||||
const result = await instance.generate(request(), context());
|
||||
|
||||
assert.equal(providerCalls, 1);
|
||||
assert.equal(records[0].record.phase, 'admitted');
|
||||
assert.equal(records[0].quote.priceRevision, 'price-1');
|
||||
assert.equal(records[0].quote.reservedCostMicros, 46);
|
||||
assert.equal(records[0].quotaAdmission.reservedCostMicros, 46);
|
||||
assert.equal(records[1].record.usage.costMicros, 3);
|
||||
assert.equal(result.usage.costMicros, 3);
|
||||
});
|
||||
|
||||
test('missing exact price revision fails before provider I/O', async () => {
|
||||
let providerCalls = 0;
|
||||
const instance = new BoundedModelGateway({
|
||||
providers: [
|
||||
provider({
|
||||
async generate() {
|
||||
providerCalls += 1;
|
||||
throw new Error('must remain unreachable');
|
||||
},
|
||||
}),
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
return policy({ priceRevision: 'missing-price' });
|
||||
},
|
||||
},
|
||||
pricing: {
|
||||
async resolve() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
audit: {
|
||||
async record() {
|
||||
throw new Error('must remain unreachable');
|
||||
},
|
||||
},
|
||||
maxConcurrent: 1,
|
||||
now: () => NOW,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
instance.generate(request(), context()),
|
||||
ModelPriceUnavailableError,
|
||||
);
|
||||
assert.equal(providerCalls, 0);
|
||||
});
|
||||
|
||||
test('policy denies a provider or model before external I/O', async () => {
|
||||
let calls = 0;
|
||||
const instance = gateway({
|
||||
resolvedPolicy: policy({ allowedModels: ['model-b'] }),
|
||||
modelProvider: provider({
|
||||
async generate() {
|
||||
calls += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
instance.generate(request(), context()),
|
||||
ModelPolicyDeniedError,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
assert.equal(instance.activeInvocations, 0);
|
||||
});
|
||||
|
||||
test('durable admission replay never invokes the provider again', async () => {
|
||||
let calls = 0;
|
||||
const instance = new BoundedModelGateway({
|
||||
providers: [
|
||||
provider({
|
||||
async generate() {
|
||||
calls += 1;
|
||||
return provider().generate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
return policy();
|
||||
},
|
||||
},
|
||||
pricing: disabledPricing,
|
||||
audit: {
|
||||
async record(record) {
|
||||
return {
|
||||
status: record.phase === 'admitted' ? 'existing' : 'created',
|
||||
};
|
||||
},
|
||||
},
|
||||
maxConcurrent: 1,
|
||||
now: () => NOW,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
instance.generate(request(), context()),
|
||||
ModelInvocationReplayBlockedError,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
assert.equal(instance.activeInvocations, 0);
|
||||
});
|
||||
|
||||
test('post-response token and byte budgets fail closed and are audited', async () => {
|
||||
const auditRecords = [];
|
||||
const instance = gateway({
|
||||
auditRecords,
|
||||
resolvedPolicy: policy({ maxOutputBytes: 4 }),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
instance.generate(request(), context()),
|
||||
ModelBudgetExceededError,
|
||||
);
|
||||
assert.deepEqual(
|
||||
auditRecords.map((record) => [record.phase, record.errorCode]),
|
||||
[
|
||||
['admitted', null],
|
||||
['failed', 'MODEL_BUDGET_EXCEEDED'],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('process-local concurrency is bounded without a hidden queue', async () => {
|
||||
let release;
|
||||
const blocked = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const instance = gateway({
|
||||
modelProvider: provider({
|
||||
async generate() {
|
||||
await blocked;
|
||||
return {
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
text: 'ok',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const first = instance.generate(request(), context());
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(instance.activeInvocations, 1);
|
||||
await assert.rejects(
|
||||
instance.generate(
|
||||
request(),
|
||||
context({ requestId: 'request-b', traceId: 'trace-b' }),
|
||||
),
|
||||
ModelGatewayBusyError,
|
||||
);
|
||||
release();
|
||||
await first;
|
||||
assert.equal(instance.activeInvocations, 0);
|
||||
});
|
||||
|
||||
test('policy resolution consumes concurrency and cannot bypass the deadline', async (t) => {
|
||||
const now = 1_000_000;
|
||||
t.mock.timers.enable({
|
||||
apis: ['Date', 'setTimeout'],
|
||||
now,
|
||||
});
|
||||
let releasePolicy;
|
||||
const pendingPolicy = new Promise((resolve) => {
|
||||
releasePolicy = resolve;
|
||||
});
|
||||
let markPolicyEntered;
|
||||
const policyEntered = new Promise((resolve) => {
|
||||
markPolicyEntered = resolve;
|
||||
});
|
||||
let providerCalls = 0;
|
||||
const instance = new BoundedModelGateway({
|
||||
providers: [
|
||||
provider({
|
||||
async generate(...args) {
|
||||
providerCalls += 1;
|
||||
return provider().generate(...args);
|
||||
},
|
||||
}),
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
markPolicyEntered();
|
||||
return pendingPolicy;
|
||||
},
|
||||
},
|
||||
pricing: disabledPricing,
|
||||
audit: { async record() {} },
|
||||
maxConcurrent: 1,
|
||||
});
|
||||
const first = instance.generate(
|
||||
request(),
|
||||
context({ deadlineAtMs: now + 40 }),
|
||||
);
|
||||
await policyEntered;
|
||||
assert.equal(instance.activeInvocations, 1);
|
||||
await assert.rejects(
|
||||
instance.generate(
|
||||
request(),
|
||||
context({
|
||||
requestId: 'request-policy-b',
|
||||
traceId: 'trace-policy-b',
|
||||
deadlineAtMs: Date.now() + 1000,
|
||||
}),
|
||||
),
|
||||
ModelGatewayBusyError,
|
||||
);
|
||||
t.mock.timers.tick(40);
|
||||
await assert.rejects(first, ModelInvocationDeadlineExceededError);
|
||||
assert.equal(providerCalls, 0);
|
||||
assert.equal(instance.activeInvocations, 0);
|
||||
releasePolicy(policy());
|
||||
});
|
||||
|
||||
test('a provider that ignores AbortSignal cannot hold the gateway past deadline', async () => {
|
||||
const auditRecords = [];
|
||||
const instance = new BoundedModelGateway({
|
||||
providers: [
|
||||
provider({
|
||||
async generate() {
|
||||
return new Promise(() => {});
|
||||
},
|
||||
}),
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
return policy();
|
||||
},
|
||||
},
|
||||
pricing: disabledPricing,
|
||||
audit: {
|
||||
async record(record) {
|
||||
auditRecords.push(record);
|
||||
},
|
||||
},
|
||||
maxConcurrent: 1,
|
||||
});
|
||||
const startedAt = Date.now();
|
||||
|
||||
await assert.rejects(
|
||||
instance.generate(request(), context({ deadlineAtMs: Date.now() + 40 })),
|
||||
ModelInvocationDeadlineExceededError,
|
||||
);
|
||||
|
||||
assert.ok(Date.now() - startedAt < 500);
|
||||
assert.equal(instance.activeInvocations, 0);
|
||||
assert.deepEqual(
|
||||
auditRecords.map((record) => [record.phase, record.errorCode]),
|
||||
[
|
||||
['admitted', null],
|
||||
['failed', 'MODEL_INVOCATION_DEADLINE_EXCEEDED'],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('durable admission is never detached when its deadline expires', async () => {
|
||||
let releaseAdmission;
|
||||
const admissionBarrier = new Promise((resolve) => {
|
||||
releaseAdmission = resolve;
|
||||
});
|
||||
let providerCalls = 0;
|
||||
const auditRecords = [];
|
||||
const instance = new BoundedModelGateway({
|
||||
providers: [
|
||||
provider({
|
||||
async generate(...args) {
|
||||
providerCalls += 1;
|
||||
return provider().generate(...args);
|
||||
},
|
||||
}),
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
return policy();
|
||||
},
|
||||
},
|
||||
pricing: disabledPricing,
|
||||
audit: {
|
||||
async record(record) {
|
||||
auditRecords.push(record);
|
||||
if (record.phase === 'admitted') await admissionBarrier;
|
||||
},
|
||||
},
|
||||
maxConcurrent: 1,
|
||||
});
|
||||
const invocation = instance.generate(
|
||||
request(),
|
||||
context({ deadlineAtMs: Date.now() + 40 }),
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 70));
|
||||
assert.equal(providerCalls, 0);
|
||||
assert.equal(instance.activeInvocations, 1);
|
||||
releaseAdmission();
|
||||
await assert.rejects(invocation, ModelInvocationDeadlineExceededError);
|
||||
assert.equal(providerCalls, 0);
|
||||
assert.equal(instance.activeInvocations, 0);
|
||||
assert.deepEqual(
|
||||
auditRecords.map((record) => [record.phase, record.errorCode]),
|
||||
[
|
||||
['admitted', null],
|
||||
['failed', 'MODEL_INVOCATION_DEADLINE_EXCEEDED'],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('stream requires final usage and audits consumer cancellation', async () => {
|
||||
const auditRecords = [];
|
||||
const instance = gateway({ auditRecords });
|
||||
const deltas = [];
|
||||
for await (const chunk of instance.stream(request(), context())) {
|
||||
deltas.push(chunk.delta);
|
||||
}
|
||||
assert.equal(deltas.join(''), 'safe summary');
|
||||
assert.deepEqual(
|
||||
auditRecords.map((record) => record.phase),
|
||||
['admitted', 'completed'],
|
||||
);
|
||||
|
||||
auditRecords.length = 0;
|
||||
for await (const chunk of instance.stream(
|
||||
request(),
|
||||
context({ requestId: 'request-cancel', traceId: 'trace-cancel' }),
|
||||
)) {
|
||||
assert.equal(chunk.delta, 'safe ');
|
||||
break;
|
||||
}
|
||||
assert.deepEqual(
|
||||
auditRecords.map((record) => [record.phase, record.errorCode]),
|
||||
[
|
||||
['admitted', null],
|
||||
['failed', 'MODEL_STREAM_CANCELLED'],
|
||||
],
|
||||
);
|
||||
assert.equal(instance.activeInvocations, 0);
|
||||
});
|
||||
|
||||
test('stream releases provider and concurrency when cancellation audit fails', async () => {
|
||||
let disposed = false;
|
||||
const instance = new BoundedModelGateway({
|
||||
providers: [
|
||||
provider({
|
||||
async *stream() {
|
||||
try {
|
||||
yield { delta: 'first' };
|
||||
yield {
|
||||
delta: 'last',
|
||||
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
||||
};
|
||||
} finally {
|
||||
disposed = true;
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
return policy();
|
||||
},
|
||||
},
|
||||
pricing: disabledPricing,
|
||||
audit: {
|
||||
async record(record) {
|
||||
if (record.errorCode === 'MODEL_STREAM_CANCELLED') {
|
||||
throw new Error('audit unavailable');
|
||||
}
|
||||
},
|
||||
},
|
||||
maxConcurrent: 1,
|
||||
now: () => NOW,
|
||||
});
|
||||
|
||||
await assert.rejects(async () => {
|
||||
for await (const _chunk of instance.stream(request(), context())) {
|
||||
break;
|
||||
}
|
||||
}, ModelAuditUnavailableError);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(disposed, true);
|
||||
assert.equal(instance.activeInvocations, 0);
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const {
|
||||
LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
|
||||
migrateLocalModelInvocationFeature,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
const {
|
||||
LocalModelInvocationFeatureActivationRepository,
|
||||
LocalModelInvocationFeatureTransitionConflictError,
|
||||
LocalModelInvocationFeatureTransitionUnavailableError,
|
||||
assertLocalModelInvocationFeatureActive,
|
||||
createLocalModelInvocationFeatureTransitionCommand,
|
||||
} = require('@qinglong/ai/local-feature-activation');
|
||||
|
||||
function createMainSqliteContract(client) {
|
||||
client.exec(`
|
||||
CREATE TABLE "QingLong3SchemaMigrations" (
|
||||
migration_id TEXT PRIMARY KEY,
|
||||
stream_id TEXT NOT NULL,
|
||||
dialect TEXT NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE "Runs" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "RunEvents" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "StepRuns" (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
UNIQUE (run_id, id)
|
||||
);
|
||||
CREATE TABLE "StepRunMutations" (mutation_id TEXT PRIMARY KEY);
|
||||
`);
|
||||
}
|
||||
|
||||
function principal(authenticationId = 'local_ai_feature:proof-1') {
|
||||
return {
|
||||
subject: { type: 'user', id: 'owner-user' },
|
||||
authenticationId,
|
||||
authenticatedAtMs: 1_000,
|
||||
expiresAtMs: 301_000,
|
||||
assurance: 'local_console',
|
||||
};
|
||||
}
|
||||
|
||||
function transition(overrides = {}) {
|
||||
return createLocalModelInvocationFeatureTransitionCommand({
|
||||
featureId: 'model-invocation',
|
||||
expectedGeneration: 0,
|
||||
expectedState: null,
|
||||
state: 'active',
|
||||
mutationId: 'feature-activation-1',
|
||||
requestId: 'feature-request-1',
|
||||
expectedMigrationDigest: LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
|
||||
safety: {
|
||||
mode: 'fresh_database',
|
||||
backupEvidenceDigest: null,
|
||||
},
|
||||
principal: principal(),
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
async function fixture() {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
createMainSqliteContract(client);
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
test('local AI feature activation is append-only, replay-safe and non-destructive', async () => {
|
||||
const client = await fixture();
|
||||
const repository = new LocalModelInvocationFeatureActivationRepository(
|
||||
client,
|
||||
);
|
||||
|
||||
assert.equal(repository.findCurrent(), null);
|
||||
assert.throws(
|
||||
() => assertLocalModelInvocationFeatureActive(client),
|
||||
LocalModelInvocationFeatureTransitionUnavailableError,
|
||||
);
|
||||
|
||||
const activated = repository.transition(transition());
|
||||
assert.equal(activated.status, 'created');
|
||||
assert.equal(activated.transition.generation, 1);
|
||||
assert.equal(activated.transition.state, 'active');
|
||||
assert.equal(
|
||||
assertLocalModelInvocationFeatureActive(client).transitionDigest,
|
||||
activated.transition.transitionDigest,
|
||||
);
|
||||
|
||||
const replayed = repository.transition(transition());
|
||||
assert.equal(replayed.status, 'existing');
|
||||
assert.deepEqual(replayed.transition, activated.transition);
|
||||
|
||||
const deactivated = repository.transition(
|
||||
transition({
|
||||
expectedGeneration: 1,
|
||||
expectedState: 'active',
|
||||
state: 'inactive',
|
||||
mutationId: 'feature-deactivation-1',
|
||||
requestId: 'feature-request-2',
|
||||
safety: {
|
||||
mode: 'preserve_existing',
|
||||
backupEvidenceDigest: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.equal(deactivated.status, 'created');
|
||||
assert.equal(deactivated.transition.generation, 2);
|
||||
assert.equal(deactivated.transition.state, 'inactive');
|
||||
assert.throws(
|
||||
() => assertLocalModelInvocationFeatureActive(client),
|
||||
LocalModelInvocationFeatureTransitionUnavailableError,
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
{
|
||||
...client
|
||||
.prepare(
|
||||
`SELECT
|
||||
(SELECT count(*) FROM "ModelInvocationFeatureTransitions") AS transitions,
|
||||
(SELECT count(*) FROM "ModelInvocationFeatureHead") AS heads,
|
||||
(SELECT count(*) FROM "ModelInvocationStarts") AS starts,
|
||||
(SELECT count(*) FROM "ModelPriceCatalogPublications") AS publications`,
|
||||
)
|
||||
.get(),
|
||||
},
|
||||
{ transitions: 2, heads: 1, starts: 0, publications: 0 },
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('local AI feature activation rejects CAS, plan and identity drift', async () => {
|
||||
const client = await fixture();
|
||||
const repository = new LocalModelInvocationFeatureActivationRepository(
|
||||
client,
|
||||
);
|
||||
repository.transition(transition());
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.transition(
|
||||
transition({
|
||||
mutationId: 'feature-activation-conflict',
|
||||
requestId: 'feature-request-conflict',
|
||||
}),
|
||||
),
|
||||
LocalModelInvocationFeatureTransitionConflictError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.transition(
|
||||
transition({
|
||||
expectedGeneration: 1,
|
||||
expectedState: 'active',
|
||||
state: 'inactive',
|
||||
mutationId: 'feature-deactivation-plan-drift',
|
||||
requestId: 'feature-request-plan-drift',
|
||||
expectedMigrationDigest: 'f'.repeat(64),
|
||||
safety: {
|
||||
mode: 'preserve_existing',
|
||||
backupEvidenceDigest: null,
|
||||
},
|
||||
}),
|
||||
),
|
||||
LocalModelInvocationFeatureTransitionConflictError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
repository.transition(
|
||||
transition({
|
||||
principal: principal('local_ai_feature:different-proof'),
|
||||
}),
|
||||
),
|
||||
LocalModelInvocationFeatureTransitionConflictError,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('local AI feature transaction fence runs before replay and rolls back', async () => {
|
||||
const client = await fixture();
|
||||
let fences = 0;
|
||||
const first = new LocalModelInvocationFeatureActivationRepository(client, {
|
||||
beforeMutation() {
|
||||
fences += 1;
|
||||
},
|
||||
});
|
||||
first.transition(transition());
|
||||
first.transition(transition());
|
||||
assert.equal(fences, 2);
|
||||
|
||||
const rejected = new LocalModelInvocationFeatureActivationRepository(client, {
|
||||
beforeMutation() {
|
||||
throw new Error('fence rejected');
|
||||
},
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
rejected.transition(
|
||||
transition({
|
||||
expectedGeneration: 1,
|
||||
expectedState: 'active',
|
||||
state: 'inactive',
|
||||
mutationId: 'feature-deactivation-rejected',
|
||||
requestId: 'feature-request-rejected',
|
||||
safety: {
|
||||
mode: 'preserve_existing',
|
||||
backupEvidenceDigest: null,
|
||||
},
|
||||
}),
|
||||
),
|
||||
LocalModelInvocationFeatureTransitionUnavailableError,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count
|
||||
FROM "ModelInvocationFeatureTransitions"`,
|
||||
)
|
||||
.get().count,
|
||||
1,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,338 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const {
|
||||
ModelPriceCatalogConflictError,
|
||||
createModelPriceCatalogPublishCommand,
|
||||
createModelPriceCatalogTransitionCommand,
|
||||
} = require('../dist/pricing/modelPriceCatalog.js');
|
||||
const {
|
||||
ModelPriceCatalogManagementSeparationOfDutyError,
|
||||
createModelPriceCatalogAuthorizationCommand,
|
||||
createModelPriceCatalogManagementService,
|
||||
createModelPriceCatalogPolicyDecision,
|
||||
} = require('../dist/pricing/modelPriceCatalogManagement.js');
|
||||
const {
|
||||
LocalModelPriceCatalogRepository,
|
||||
} = require('../dist/pricing/storage/localModelPriceCatalogRepository.js');
|
||||
const {
|
||||
migrateLocalModelInvocationFeature,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
|
||||
function createMainSqliteContract(client) {
|
||||
client.exec(`
|
||||
CREATE TABLE "QingLong3SchemaMigrations" (
|
||||
migration_id TEXT PRIMARY KEY,
|
||||
stream_id TEXT NOT NULL,
|
||||
dialect TEXT NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE "Runs" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "RunEvents" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "StepRuns" (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
UNIQUE (run_id, id)
|
||||
);
|
||||
CREATE TABLE "StepRunMutations" (mutation_id TEXT PRIMARY KEY);
|
||||
`);
|
||||
}
|
||||
|
||||
async function fixture(decisionMode = 'human_confirmation') {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
createMainSqliteContract(client);
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
const repository = new LocalModelPriceCatalogRepository(client);
|
||||
const now = Date.now();
|
||||
const service = createModelPriceCatalogManagementService(repository, {
|
||||
decisionMode,
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return createModelPriceCatalogPolicyDecision({
|
||||
effect: 'allow',
|
||||
revision: 'platform-policy-1',
|
||||
reasons: ['catalog_operator'],
|
||||
});
|
||||
},
|
||||
},
|
||||
now: () => now,
|
||||
});
|
||||
return { client, repository, service, now };
|
||||
}
|
||||
|
||||
function principal(userId, now) {
|
||||
return {
|
||||
subject: { type: 'user', id: userId },
|
||||
authenticationId: `auth-${userId}`,
|
||||
authenticatedAtMs: now - 1_000,
|
||||
expiresAtMs: now + 60_000,
|
||||
assurance: 'multi_factor',
|
||||
};
|
||||
}
|
||||
|
||||
function publication(userId, now, overrides = {}) {
|
||||
return {
|
||||
authorizationId: 'authorize-publish-1',
|
||||
requestId: 'request-publish-1',
|
||||
mutationId: 'publish-price-1',
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
principal: principal(userId, now),
|
||||
priceRevision: 'price-1',
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: 150_000,
|
||||
outputMicrosPerMillionTokens: 600_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function activation(userId, now, overrides = {}) {
|
||||
return {
|
||||
authorizationId: 'authorize-activate-1',
|
||||
requestId: 'request-activate-1',
|
||||
mutationId: 'activate-price-1',
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
principal: principal(userId, now),
|
||||
expectedGeneration: 0,
|
||||
expectedHeadDigest: null,
|
||||
action: 'activate',
|
||||
priceRevision: 'price-1',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('SQLite atomically commits catalog mutations with exact authorization facts', async () => {
|
||||
const { client, repository, service, now } = await fixture();
|
||||
const published = await service.publish(publication('owner', now));
|
||||
const replayed = await service.publish(publication('owner', now));
|
||||
assert.equal(published.status, 'created');
|
||||
assert.equal(replayed.status, 'existing');
|
||||
assert.deepEqual(replayed, {
|
||||
status: 'existing',
|
||||
publication: published.publication,
|
||||
authorization: published.authorization,
|
||||
});
|
||||
|
||||
const activated = await service.transition(activation('owner', now));
|
||||
assert.equal(activated.status, 'created');
|
||||
assert.equal(activated.head.activePriceRevision, 'price-1');
|
||||
assert.deepEqual(
|
||||
await repository.findAuthorization('authorize-activate-1'),
|
||||
activated.authorization,
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
...client
|
||||
.prepare(
|
||||
`SELECT
|
||||
(SELECT count(*) FROM "ModelPriceCatalogPublications") AS publications,
|
||||
(SELECT count(*) FROM "ModelPriceCatalogHeads") AS heads,
|
||||
(SELECT count(*) FROM "ModelPriceCatalogAuthorizations") AS authorizations`,
|
||||
)
|
||||
.get(),
|
||||
},
|
||||
{ publications: 1, heads: 1, authorizations: 2 },
|
||||
);
|
||||
assert.equal(client.prepare('PRAGMA foreign_key_check').all().length, 0);
|
||||
|
||||
await assert.rejects(
|
||||
service.publish(
|
||||
publication('owner', now, {
|
||||
authorizationId: 'authorize-publish-drift',
|
||||
}),
|
||||
),
|
||||
ModelPriceCatalogConflictError,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count FROM "ModelPriceCatalogAuthorizations"`,
|
||||
)
|
||||
.get().count,
|
||||
2,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('SQLite replay accepts fresh reauthentication but preserves the first authorization fact', async () => {
|
||||
const { client, repository, service, now } = await fixture();
|
||||
const created = await service.publish(publication('owner', now));
|
||||
const freshService = createModelPriceCatalogManagementService(repository, {
|
||||
decisionMode: 'human_confirmation',
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return createModelPriceCatalogPolicyDecision({
|
||||
effect: 'allow',
|
||||
revision: 'platform-policy-1',
|
||||
reasons: ['catalog_operator'],
|
||||
});
|
||||
},
|
||||
},
|
||||
now: () => now + 1_000,
|
||||
});
|
||||
|
||||
const replayed = await freshService.publish(
|
||||
publication('owner', now + 1_000),
|
||||
);
|
||||
assert.equal(replayed.status, 'existing');
|
||||
assert.deepEqual(replayed.authorization, created.authorization);
|
||||
await assert.rejects(
|
||||
freshService.publish(
|
||||
publication('owner', now + 1_000, {
|
||||
principal: {
|
||||
...principal('owner', now + 1_000),
|
||||
authenticationId: 'auth-different-proof',
|
||||
},
|
||||
}),
|
||||
),
|
||||
ModelPriceCatalogConflictError,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('SQLite authorization fence hook runs inside the catalog transaction before mutation', async () => {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
createMainSqliteContract(client);
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
let checked = 0;
|
||||
const repository = new LocalModelPriceCatalogRepository(client, {
|
||||
beforeAuthorizedMutation() {
|
||||
checked += 1;
|
||||
const error = new Error('credential fence rejected');
|
||||
error.code = 'TEST_CREDENTIAL_FENCE_REJECTED';
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
const now = Date.now();
|
||||
const service = createModelPriceCatalogManagementService(repository, {
|
||||
decisionMode: 'human_confirmation',
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return createModelPriceCatalogPolicyDecision({
|
||||
effect: 'allow',
|
||||
revision: 'platform-policy-1',
|
||||
reasons: ['catalog_operator'],
|
||||
});
|
||||
},
|
||||
},
|
||||
now: () => now,
|
||||
});
|
||||
|
||||
await assert.rejects(service.publish(publication('owner', now)), {
|
||||
code: 'MODEL_PRICE_CATALOG_UNAVAILABLE',
|
||||
});
|
||||
assert.equal(checked, 1);
|
||||
assert.deepEqual(
|
||||
{
|
||||
...client
|
||||
.prepare(
|
||||
`SELECT
|
||||
(SELECT count(*) FROM "ModelPriceCatalogPublications") AS publications,
|
||||
(SELECT count(*) FROM "ModelPriceCatalogAuthorizations") AS authorizations`,
|
||||
)
|
||||
.get(),
|
||||
},
|
||||
{ publications: 0, authorizations: 0 },
|
||||
);
|
||||
assert.equal(client.isTransaction, false);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('authorized activation rejects legacy raw publication without evidence', async () => {
|
||||
const { client, repository, service, now } = await fixture();
|
||||
await repository.publish(
|
||||
createModelPriceCatalogPublishCommand({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: 150_000,
|
||||
outputMicrosPerMillionTokens: 600_000,
|
||||
mutationId: 'legacy-publish-price-1',
|
||||
publishedByUserId: 'owner',
|
||||
}),
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
service.transition(activation('owner', now)),
|
||||
ModelPriceCatalogConflictError,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(`SELECT count(*) AS count FROM "ModelPriceCatalogHeads"`)
|
||||
.get().count,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count FROM "ModelPriceCatalogAuthorizations"`,
|
||||
)
|
||||
.get().count,
|
||||
0,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('SQLite enforces separation of duty again inside the catalog transaction', async () => {
|
||||
const { client, repository, service, now } = await fixture(
|
||||
'separation_of_duty',
|
||||
);
|
||||
await service.publish(publication('publisher', now));
|
||||
await assert.rejects(
|
||||
service.transition(activation('publisher', now)),
|
||||
ModelPriceCatalogManagementSeparationOfDutyError,
|
||||
);
|
||||
|
||||
const directCommand = createModelPriceCatalogTransitionCommand({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
expectedGeneration: 0,
|
||||
expectedHeadDigest: null,
|
||||
action: 'activate',
|
||||
priceRevision: 'price-1',
|
||||
mutationId: 'direct-activate-price-1',
|
||||
changedByUserId: 'publisher',
|
||||
});
|
||||
const directAuthorization = createModelPriceCatalogAuthorizationCommand({
|
||||
authorizationId: 'direct-same-user',
|
||||
requestId: 'direct-same-user-request',
|
||||
operation: 'activate',
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
catalogCommandDigest: directCommand.commandDigest,
|
||||
principal: principal('publisher', now),
|
||||
policy: createModelPriceCatalogPolicyDecision({
|
||||
effect: 'allow',
|
||||
revision: 'platform-policy-1',
|
||||
reasons: ['catalog_operator'],
|
||||
}),
|
||||
decisionMode: 'separation_of_duty',
|
||||
});
|
||||
await assert.rejects(
|
||||
repository.transitionAuthorized(directCommand, directAuthorization),
|
||||
ModelPriceCatalogConflictError,
|
||||
);
|
||||
|
||||
const reviewed = await service.transition(activation('reviewer', now));
|
||||
assert.equal(reviewed.head.activePriceRevision, 'price-1');
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(`SELECT count(*) AS count FROM "ModelPriceCatalogHeads"`)
|
||||
.get().count,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
await repository
|
||||
.findCurrent('remote', 'model-a')
|
||||
.then((head) => head.activePriceRevision),
|
||||
'price-1',
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
@@ -0,0 +1,255 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const {
|
||||
InvalidModelPriceCatalogError,
|
||||
ModelPriceCatalogConflictError,
|
||||
ModelPriceCatalogUnavailableError,
|
||||
createModelPriceCatalogPublishCommand,
|
||||
createModelPriceCatalogTransitionCommand,
|
||||
} = require('../dist/pricing/modelPriceCatalog.js');
|
||||
const {
|
||||
LocalModelPriceCatalogRepository,
|
||||
} = require('../dist/pricing/storage/localModelPriceCatalogRepository.js');
|
||||
const {
|
||||
migrateLocalModelInvocationFeature,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
|
||||
function createMainSqliteContract(client) {
|
||||
client.exec(`
|
||||
CREATE TABLE "QingLong3SchemaMigrations" (
|
||||
migration_id TEXT PRIMARY KEY,
|
||||
stream_id TEXT NOT NULL,
|
||||
dialect TEXT NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE "Runs" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "RunEvents" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "StepRuns" (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
UNIQUE (run_id, id)
|
||||
);
|
||||
CREATE TABLE "StepRunMutations" (mutation_id TEXT PRIMARY KEY);
|
||||
`);
|
||||
}
|
||||
|
||||
async function fixture() {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
createMainSqliteContract(client);
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
return {
|
||||
client,
|
||||
repository: new LocalModelPriceCatalogRepository(client),
|
||||
};
|
||||
}
|
||||
|
||||
function publish(revision, mutationId, rate = 150_000) {
|
||||
return createModelPriceCatalogPublishCommand({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: revision,
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: rate,
|
||||
outputMicrosPerMillionTokens: rate * 4,
|
||||
mutationId,
|
||||
publishedByUserId: 'user-admin',
|
||||
});
|
||||
}
|
||||
|
||||
function transition(head, action, revision, mutationId) {
|
||||
return createModelPriceCatalogTransitionCommand({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
expectedGeneration: head?.generation ?? 0,
|
||||
expectedHeadDigest: head?.headDigest ?? null,
|
||||
action,
|
||||
priceRevision: revision,
|
||||
mutationId,
|
||||
changedByUserId: 'user-admin',
|
||||
});
|
||||
}
|
||||
|
||||
test('SQLite durable catalog publishes, activates, switches and revokes exactly', async () => {
|
||||
const { client, repository } = await fixture();
|
||||
const firstCommand = publish('price-1', 'publish-price-1');
|
||||
const first = await repository.publish(firstCommand);
|
||||
assert.equal(first.status, 'created');
|
||||
assert.deepEqual(await repository.publish(firstCommand), {
|
||||
status: 'existing',
|
||||
publication: first.publication,
|
||||
});
|
||||
assert.equal(
|
||||
await repository.resolve({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
|
||||
const firstActivationCommand = transition(
|
||||
null,
|
||||
'activate',
|
||||
'price-1',
|
||||
'activate-price-1',
|
||||
);
|
||||
const firstActivation = await repository.transition(firstActivationCommand);
|
||||
assert.equal(firstActivation.status, 'created');
|
||||
assert.deepEqual(await repository.transition(firstActivationCommand), {
|
||||
status: 'existing',
|
||||
head: firstActivation.head,
|
||||
});
|
||||
assert.equal(
|
||||
(
|
||||
await repository.resolve({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
})
|
||||
).catalogDigest,
|
||||
first.publication.entry.catalogDigest,
|
||||
);
|
||||
|
||||
const second = await repository.publish(
|
||||
publish('price-2', 'publish-price-2', 200_000),
|
||||
);
|
||||
const secondActivation = await repository.transition(
|
||||
transition(firstActivation.head, 'activate', 'price-2', 'activate-price-2'),
|
||||
);
|
||||
assert.equal(
|
||||
await repository.resolve({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await repository.resolve({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-2',
|
||||
})
|
||||
).catalogDigest,
|
||||
second.publication.entry.catalogDigest,
|
||||
);
|
||||
|
||||
const revokeFirst = await repository.transition(
|
||||
transition(secondActivation.head, 'revoke', 'price-1', 'revoke-price-1'),
|
||||
);
|
||||
assert.equal(revokeFirst.head.activePriceRevision, 'price-2');
|
||||
assert.equal(revokeFirst.head.revokedPriceRevision, 'price-1');
|
||||
await assert.rejects(
|
||||
repository.transition(
|
||||
transition(revokeFirst.head, 'activate', 'price-1', 'reactivate-price-1'),
|
||||
),
|
||||
ModelPriceCatalogConflictError,
|
||||
);
|
||||
|
||||
const revokeSecond = await repository.transition(
|
||||
transition(revokeFirst.head, 'revoke', 'price-2', 'revoke-price-2'),
|
||||
);
|
||||
assert.equal(revokeSecond.head.activePriceRevision, null);
|
||||
assert.equal(
|
||||
await repository.resolve({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-2',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(`SELECT count(*) AS count FROM "ModelPriceCatalogHeads"`)
|
||||
.get().count,
|
||||
4,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('SQLite catalog gives one winner and rolls stale mutations back', async () => {
|
||||
const { client, repository } = await fixture();
|
||||
await repository.publish(publish('price-1', 'publish-price-1'));
|
||||
await repository.publish(publish('price-2', 'publish-price-2'));
|
||||
const first = await repository.transition(
|
||||
transition(null, 'activate', 'price-1', 'activate-price-1'),
|
||||
);
|
||||
const competing = await Promise.allSettled([
|
||||
repository.transition(
|
||||
transition(first.head, 'activate', 'price-2', 'activate-price-2-a'),
|
||||
),
|
||||
repository.transition(
|
||||
transition(first.head, 'deactivate', null, 'deactivate-price-1-b'),
|
||||
),
|
||||
]);
|
||||
assert.equal(
|
||||
competing.filter((result) => result.status === 'fulfilled').length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
competing.filter(
|
||||
(result) =>
|
||||
result.status === 'rejected' &&
|
||||
result.reason instanceof ModelPriceCatalogConflictError,
|
||||
).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(`SELECT count(*) AS count FROM "ModelPriceCatalogHeads"`)
|
||||
.get().count,
|
||||
2,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('SQLite catalog fails closed on corrupted durable JSON', async () => {
|
||||
const { client, repository } = await fixture();
|
||||
await repository.publish(publish('price-1', 'publish-price-1'));
|
||||
client.exec(`
|
||||
PRAGMA ignore_check_constraints = ON;
|
||||
UPDATE "ModelPriceCatalogPublications"
|
||||
SET publication_json = '{}'
|
||||
WHERE price_revision = 'price-1';
|
||||
PRAGMA ignore_check_constraints = OFF;
|
||||
`);
|
||||
await assert.rejects(
|
||||
repository.findPublication({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
}),
|
||||
ModelPriceCatalogUnavailableError,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('SQLite catalog validates and preserves resolver cancellation', async () => {
|
||||
const { client, repository } = await fixture();
|
||||
await assert.rejects(
|
||||
repository.resolve({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
signal: {},
|
||||
}),
|
||||
InvalidModelPriceCatalogError,
|
||||
);
|
||||
|
||||
const controller = new AbortController();
|
||||
const reason = new Error('catalog lookup cancelled');
|
||||
const resolution = repository.resolve({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
signal: controller.signal,
|
||||
});
|
||||
controller.abort(reason);
|
||||
await assert.rejects(resolution, reason);
|
||||
client.close();
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const {
|
||||
MODEL_PROVIDER_CREDENTIAL_TRANSITION_COMMAND_SCHEMA,
|
||||
ModelProviderCredentialTransitionConflictError,
|
||||
createModelProviderCredentialTransitionCommand,
|
||||
} = require('../dist/model-provider-credential/modelProviderCredentialCatalog.js');
|
||||
const {
|
||||
ModelProviderCredentialAdministrationAuthorizationFenceConflictError,
|
||||
modelProviderCredentialAdministrationOperationId,
|
||||
} = require('../dist/model-provider-credential/modelProviderCredentialAdministration.js');
|
||||
const {
|
||||
LocalModelProviderCredentialRepository,
|
||||
} = require('../dist/model-provider-credential/localModelProviderCredentialRepository.js');
|
||||
const {
|
||||
LOCAL_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
|
||||
migrateLocalModelInvocationFeature,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
const {
|
||||
MODEL_PROVIDER_CREDENTIAL_AUDIT_SCHEMA,
|
||||
MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
digestModelProviderCredentialBinding,
|
||||
} = require('../dist/model-provider-credential/providerCredential.js');
|
||||
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||
|
||||
const PROJECT_ID = 'project-a';
|
||||
const PROVIDER = 'openai-compatible';
|
||||
const ACTOR = Object.freeze({ type: 'user', id: 'owner-a' });
|
||||
const FENCE = Object.freeze({ projectVersion: 3, bindingVersion: 7 });
|
||||
const BIND_MUTATION_ID = '019f7094-a853-4f3b-82ab-dfa08e6bd1c1';
|
||||
|
||||
function createMainContract(client) {
|
||||
client.exec(`
|
||||
PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE "QingLong3SchemaMigrations" (
|
||||
migration_id TEXT PRIMARY KEY,
|
||||
stream_id TEXT NOT NULL,
|
||||
dialect TEXT NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE "Runs" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "RunEvents" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "StepRuns" (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
UNIQUE (run_id, id)
|
||||
);
|
||||
CREATE TABLE "StepRunMutations" (mutation_id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "QingLong3LocalSecretEnvelopes" (
|
||||
project_id TEXT NOT NULL,
|
||||
secret_name TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
PRIMARY KEY (project_id, secret_name, version)
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
async function fixture(options = {}) {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
createMainContract(client);
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LocalSecretEnvelopes"
|
||||
(project_id, secret_name, version) VALUES (?, ?, ?)`,
|
||||
)
|
||||
.run(PROJECT_ID, 'openai-token', 1);
|
||||
return {
|
||||
client,
|
||||
repository: new LocalModelProviderCredentialRepository(client, {
|
||||
now: options.now ?? (() => 100),
|
||||
...(options.authorization
|
||||
? { authorization: options.authorization }
|
||||
: {}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function binding(overrides = {}) {
|
||||
return Object.freeze({
|
||||
schema: MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
projectId: PROJECT_ID,
|
||||
provider: PROVIDER,
|
||||
revision: 'credential-v1',
|
||||
secretRef: createSecretRef({
|
||||
projectId: PROJECT_ID,
|
||||
name: 'openai-token',
|
||||
version: 1,
|
||||
}),
|
||||
scheme: 'bearer',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function command(overrides = {}) {
|
||||
return createModelProviderCredentialTransitionCommand({
|
||||
schema: MODEL_PROVIDER_CREDENTIAL_TRANSITION_COMMAND_SCHEMA,
|
||||
mutationId: BIND_MUTATION_ID,
|
||||
projectId: PROJECT_ID,
|
||||
provider: PROVIDER,
|
||||
expectedGeneration: 0,
|
||||
action: 'bind',
|
||||
binding: binding(),
|
||||
changedBy: ACTOR,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function allowedAudit(catalogCommand, overrides = {}) {
|
||||
return Object.freeze({
|
||||
eventId: catalogCommand.mutationId,
|
||||
requestId: 'request-administration-1',
|
||||
operationId: modelProviderCredentialAdministrationOperationId(
|
||||
catalogCommand.action,
|
||||
),
|
||||
projectId: catalogCommand.projectId,
|
||||
subject: ACTOR,
|
||||
authenticationId: 'authentication-1',
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_owner'],
|
||||
fence: FENCE,
|
||||
occurredAtMs: 99,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function authorized(catalogCommand) {
|
||||
return Object.freeze({
|
||||
command: catalogCommand,
|
||||
actor: ACTOR,
|
||||
fence: FENCE,
|
||||
audit: allowedAudit(catalogCommand),
|
||||
});
|
||||
}
|
||||
|
||||
function useAudit(activeBinding, requestId, occurredAtMs) {
|
||||
return Object.freeze({
|
||||
schema: MODEL_PROVIDER_CREDENTIAL_AUDIT_SCHEMA,
|
||||
operation: 'generate',
|
||||
projectId: PROJECT_ID,
|
||||
provider: PROVIDER,
|
||||
requestId,
|
||||
bindingRevision: activeBinding.revision,
|
||||
bindingDigest: digestModelProviderCredentialBinding(activeBinding),
|
||||
occurredAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
test('local credential repository binds an existing SecretRef and replays exactly', async () => {
|
||||
const { client, repository } = await fixture();
|
||||
const bind = command();
|
||||
|
||||
const created = await repository.commit(bind);
|
||||
const replay = await repository.commit(bind);
|
||||
assert.equal(created.status, 'created');
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.deepEqual(replay.transition, created.transition);
|
||||
assert.deepEqual(
|
||||
await repository.findCurrentTransition(PROJECT_ID, PROVIDER),
|
||||
created.transition,
|
||||
);
|
||||
assert.deepEqual(
|
||||
await repository.resolveModelProviderCredentialBinding({
|
||||
projectId: PROJECT_ID,
|
||||
provider: PROVIDER,
|
||||
}),
|
||||
bind.binding,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count
|
||||
FROM "${LOCAL_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE}"`,
|
||||
)
|
||||
.get().count,
|
||||
13,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('local credential repository fails closed for missing SecretRef and stale generation', async () => {
|
||||
const { client, repository } = await fixture();
|
||||
const missing = command({
|
||||
mutationId: '019f7094-a853-4f3b-82ab-dfa08e6bd1c2',
|
||||
binding: binding({
|
||||
secretRef: createSecretRef({
|
||||
projectId: PROJECT_ID,
|
||||
name: 'missing-token',
|
||||
version: 1,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
await assert.rejects(
|
||||
repository.commit(missing),
|
||||
ModelProviderCredentialTransitionConflictError,
|
||||
);
|
||||
assert.equal(
|
||||
await repository.findCurrentTransition(PROJECT_ID, PROVIDER),
|
||||
null,
|
||||
);
|
||||
|
||||
await repository.commit(command());
|
||||
await assert.rejects(
|
||||
repository.commit(
|
||||
command({
|
||||
mutationId: '019f7094-a853-4f3b-82ab-dfa08e6bd1c3',
|
||||
}),
|
||||
),
|
||||
ModelProviderCredentialTransitionConflictError,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count
|
||||
FROM "ModelInvocationProviderCredentialTransitions"`,
|
||||
)
|
||||
.get().count,
|
||||
1,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('authorized mutation and inspection revalidate inside the repository transaction', async () => {
|
||||
const confirmations = [];
|
||||
const authorization = {
|
||||
confirm(input) {
|
||||
assert.equal(input.value.actor.id, ACTOR.id);
|
||||
assert.equal(input.value.fence.projectVersion, FENCE.projectVersion);
|
||||
confirmations.push({ kind: input.kind, replay: input.replay });
|
||||
},
|
||||
};
|
||||
const { client, repository } = await fixture({ authorization });
|
||||
const bind = command();
|
||||
|
||||
assert.equal(
|
||||
(await repository.commitAuthorized(authorized(bind))).status,
|
||||
'created',
|
||||
);
|
||||
assert.equal(
|
||||
(await repository.commitAuthorized(authorized(bind))).status,
|
||||
'existing',
|
||||
);
|
||||
const inspected = await repository.inspectAuthorized({
|
||||
projectId: PROJECT_ID,
|
||||
provider: PROVIDER,
|
||||
actor: ACTOR,
|
||||
fence: FENCE,
|
||||
audit: {
|
||||
...allowedAudit(bind),
|
||||
eventId: '019f7094-a853-4f3b-82ab-dfa08e6bd1c4',
|
||||
operationId: 'model_provider_credential.inspect',
|
||||
},
|
||||
});
|
||||
assert.equal(inspected.generation, 1);
|
||||
assert.deepEqual(confirmations, [
|
||||
{ kind: 'mutation', replay: false },
|
||||
{ kind: 'mutation', replay: true },
|
||||
{ kind: 'inspection', replay: false },
|
||||
]);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('authorized replay fails closed when no authorization guard is installed', async () => {
|
||||
const { client, repository } = await fixture();
|
||||
const bind = command();
|
||||
await repository.commit(bind);
|
||||
await assert.rejects(
|
||||
repository.commitAuthorized(authorized(bind)),
|
||||
ModelProviderCredentialAdministrationAuthorizationFenceConflictError,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('authorization fence rejection rolls the complete credential mutation back', async () => {
|
||||
const authorization = {
|
||||
confirm() {
|
||||
throw new ModelProviderCredentialAdministrationAuthorizationFenceConflictError();
|
||||
},
|
||||
};
|
||||
const { client, repository } = await fixture({ authorization });
|
||||
await assert.rejects(
|
||||
repository.commitAuthorized(authorized(command())),
|
||||
ModelProviderCredentialAdministrationAuthorizationFenceConflictError,
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
...client
|
||||
.prepare(
|
||||
`SELECT
|
||||
(SELECT count(*) FROM "ModelInvocationProviderCredentialBindings") AS bindings,
|
||||
(SELECT count(*) FROM "ModelInvocationProviderCredentialTransitions") AS transitions`,
|
||||
)
|
||||
.get(),
|
||||
},
|
||||
{ bindings: 0, transitions: 0 },
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('credential use audit is content-free, idempotent and invalid after revoke', async () => {
|
||||
const { client, repository } = await fixture();
|
||||
const bind = command();
|
||||
await repository.commit(bind);
|
||||
|
||||
await repository.record(useAudit(bind.binding, 'provider-request-1', 110));
|
||||
await repository.record(useAudit(bind.binding, 'provider-request-1', 999));
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count
|
||||
FROM "ModelInvocationProviderCredentialAudits"`,
|
||||
)
|
||||
.get().count,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT audit_json AS value
|
||||
FROM "ModelInvocationProviderCredentialAudits"`,
|
||||
)
|
||||
.get()
|
||||
.value.includes('secretRef'),
|
||||
false,
|
||||
);
|
||||
|
||||
const revoke = command({
|
||||
mutationId: '019f7094-a853-4f3b-82ab-dfa08e6bd1c5',
|
||||
expectedGeneration: 1,
|
||||
action: 'revoke',
|
||||
binding: null,
|
||||
});
|
||||
await repository.commit(revoke);
|
||||
assert.equal(
|
||||
await repository.resolveModelProviderCredentialBinding({
|
||||
projectId: PROJECT_ID,
|
||||
provider: PROVIDER,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
await assert.rejects(
|
||||
repository.record(useAudit(bind.binding, 'provider-request-2', 120)),
|
||||
ModelProviderCredentialTransitionConflictError,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash } = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
LocalPluginPackagePromptOutputKeyRetirementRepository,
|
||||
assertLocalPluginPackagePromptOutputKeyNotRetiring,
|
||||
} = require('../dist/prompt-output/storage/localPluginPackagePromptOutputKeyRetirementRepository.js');
|
||||
const {
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
PluginPackagePromptOutputKeyRetirementUnavailableError,
|
||||
createPluginPackagePromptOutputKeyRetirementPreparation,
|
||||
pluginPackagePromptOutputKeyRetirementAbsenceProof,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyRetirement.js');
|
||||
const {
|
||||
setupScenario,
|
||||
} = require('./fixtures/pluginPackagePromptCrashFixture.cjs');
|
||||
|
||||
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
||||
|
||||
async function harness(t) {
|
||||
const directory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ql3-prompt-output-key-retirement-'),
|
||||
);
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const databasePath = path.join(directory, 'runtime.sqlite');
|
||||
await setupScenario({
|
||||
databasePath,
|
||||
statePath: path.join(directory, 'state.json'),
|
||||
profile: 'edge',
|
||||
operation: 'admission',
|
||||
});
|
||||
const client = new DatabaseSync(databasePath);
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
t.after(() => client.close());
|
||||
const authority = {
|
||||
client,
|
||||
async enqueue(work) {
|
||||
return work();
|
||||
},
|
||||
};
|
||||
let nowMs = 100;
|
||||
return {
|
||||
client,
|
||||
repository: new LocalPluginPackagePromptOutputKeyRetirementRepository({
|
||||
authority,
|
||||
now: () => nowMs,
|
||||
}),
|
||||
setNow(value) {
|
||||
nowMs = value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function command() {
|
||||
return {
|
||||
keyId: 'prompt-output-key-old',
|
||||
retirementId: 'retire-prompt-output-key-old',
|
||||
requestId: 'request-retire-prompt-output-key-old',
|
||||
mutationId: 'mutation-retire-prompt-output-key-old',
|
||||
catalogDigest: digest('catalog-before'),
|
||||
materialProof: digest('material-old'),
|
||||
};
|
||||
}
|
||||
|
||||
test('SQLite appends one retirement preparation and completion with exact replay', async (t) => {
|
||||
const value = await harness(t);
|
||||
const prepared = await value.repository.prepare(command());
|
||||
assert.equal(prepared.status, 'created');
|
||||
assert.equal(prepared.preparation.preparedAtMs, 100);
|
||||
assert.throws(
|
||||
() =>
|
||||
assertLocalPluginPackagePromptOutputKeyNotRetiring(
|
||||
value.client,
|
||||
command().keyId,
|
||||
),
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
);
|
||||
const retiredCatalogDigest = digest('catalog-after');
|
||||
const absenceProof = pluginPackagePromptOutputKeyRetirementAbsenceProof(
|
||||
prepared.preparation,
|
||||
retiredCatalogDigest,
|
||||
);
|
||||
value.setNow(200);
|
||||
const completed = await value.repository.complete({
|
||||
preparation: prepared.preparation,
|
||||
retiredCatalogDigest,
|
||||
absenceProof,
|
||||
});
|
||||
assert.equal(completed.status, 'created');
|
||||
assert.equal(completed.completion.completedAtMs, 200);
|
||||
value.setNow(999);
|
||||
assert.equal((await value.repository.prepare(command())).status, 'existing');
|
||||
assert.equal(
|
||||
(
|
||||
await value.repository.complete({
|
||||
preparation: prepared.preparation,
|
||||
retiredCatalogDigest,
|
||||
absenceProof,
|
||||
})
|
||||
).status,
|
||||
'existing',
|
||||
);
|
||||
assert.deepEqual(await value.repository.find(command().keyId), {
|
||||
preparation: prepared.preparation,
|
||||
completion: completed.completion,
|
||||
});
|
||||
});
|
||||
|
||||
test('SQLite rejects detached completion, command drift and corrupt durable JSON', async (t) => {
|
||||
const value = await harness(t);
|
||||
const detached = createPluginPackagePromptOutputKeyRetirementPreparation({
|
||||
...command(),
|
||||
preparedAtMs: 100,
|
||||
});
|
||||
await assert.rejects(
|
||||
value.repository.complete({
|
||||
preparation: detached,
|
||||
retiredCatalogDigest: digest('catalog-after'),
|
||||
absenceProof: digest('absence'),
|
||||
}),
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
);
|
||||
const prepared = await value.repository.prepare(command());
|
||||
await assert.rejects(
|
||||
value.repository.prepare({ ...command(), requestId: 'drifted-request' }),
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
);
|
||||
value.client.exec('PRAGMA ignore_check_constraints = ON');
|
||||
value.client
|
||||
.prepare(
|
||||
`UPDATE "ModelInvocationPromptOutputKeyRetirementPreparations"
|
||||
SET preparation_json = '{"corrupt":true}'
|
||||
WHERE key_id = ?`,
|
||||
)
|
||||
.run(prepared.preparation.keyId);
|
||||
value.client.exec('PRAGMA ignore_check_constraints = OFF');
|
||||
await assert.rejects(
|
||||
value.repository.find(prepared.preparation.keyId),
|
||||
PluginPackagePromptOutputKeyRetirementUnavailableError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createStepRunRecord,
|
||||
transitionStepRunMutation,
|
||||
} = require('@qinglong/runtime-core/step-run');
|
||||
const {
|
||||
InvalidModelInvocationError,
|
||||
createModelInvocationCompletionCommand,
|
||||
createModelInvocationMutationIdentity,
|
||||
createModelInvocationStartCommand,
|
||||
normalizeModelInvocationCompletionCommand,
|
||||
normalizeModelInvocationStartCommand,
|
||||
} = require('../dist/model-invocation/modelInvocation.js');
|
||||
|
||||
const NOW = 1_000_000;
|
||||
|
||||
function readyStepRun() {
|
||||
return createStepRunRecord({
|
||||
id: 'step-a',
|
||||
runId: 'run-a',
|
||||
stepKey: 'summarize',
|
||||
kind: 'model',
|
||||
definitionRef: 'prompt:summary@1',
|
||||
definitionDigest: 'a'.repeat(64),
|
||||
required: true,
|
||||
initialStatus: 'ready',
|
||||
inputRef: 'artifact:input-a',
|
||||
mutationId: 'create-step-a',
|
||||
createdAtMs: NOW - 1,
|
||||
});
|
||||
}
|
||||
|
||||
function audit(phase, overrides = {}) {
|
||||
return {
|
||||
phase,
|
||||
projectId: 'project-a',
|
||||
runId: 'run-a',
|
||||
stepRunId: 'step-a',
|
||||
traceId: 'trace-a',
|
||||
requestId: 'request-a',
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
policyRevision: 'policy-1',
|
||||
requestDigest: `sha256:${'b'.repeat(64)}`,
|
||||
deadlineAtMs: NOW + 10_000,
|
||||
inputBytes: 128,
|
||||
maxOutputTokens: 64,
|
||||
outputBytes: 0,
|
||||
usage: null,
|
||||
errorCode: null,
|
||||
occurredAtMs: NOW,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function startFixture(requestId = 'request-a') {
|
||||
const current = readyStepRun();
|
||||
const identity = createModelInvocationMutationIdentity(requestId, 'start');
|
||||
const mutation = transitionStepRunMutation(
|
||||
current,
|
||||
{
|
||||
expectedVersion: current.version,
|
||||
expectedDigest: current.stepRunDigest,
|
||||
mutationId: identity.mutationId,
|
||||
to: 'running',
|
||||
atMs: NOW,
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 1,
|
||||
expectedRunEventSequence: 1,
|
||||
eventId: identity.eventId,
|
||||
dedupeKey: identity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
);
|
||||
return createModelInvocationStartCommand(
|
||||
audit('admitted', { requestId }),
|
||||
mutation,
|
||||
);
|
||||
}
|
||||
|
||||
function completionFixture({ requestId = 'request-a', errorCode = null } = {}) {
|
||||
const startCommand = startFixture(requestId);
|
||||
const identity = createModelInvocationMutationIdentity(
|
||||
requestId,
|
||||
'completion',
|
||||
);
|
||||
const failed = errorCode !== null;
|
||||
const timedOut = errorCode === 'MODEL_INVOCATION_DEADLINE_EXCEEDED';
|
||||
const lost =
|
||||
errorCode === 'MODEL_INVOCATION_ABORTED' ||
|
||||
errorCode === 'MODEL_STREAM_CANCELLED';
|
||||
const status = timedOut
|
||||
? 'timed_out'
|
||||
: lost
|
||||
? 'lost'
|
||||
: failed
|
||||
? 'failed'
|
||||
: 'succeeded';
|
||||
const mutation = transitionStepRunMutation(
|
||||
startCommand.stepRunMutation.stepRun,
|
||||
{
|
||||
expectedVersion: startCommand.start.startedStepRunVersion,
|
||||
expectedDigest: startCommand.start.startedStepRunDigest,
|
||||
mutationId: identity.mutationId,
|
||||
to: status,
|
||||
atMs: NOW + 25,
|
||||
...(failed
|
||||
? {
|
||||
resultCode: timedOut
|
||||
? 'model_deadline_exceeded'
|
||||
: lost
|
||||
? 'model_outcome_unknown'
|
||||
: 'model_provider_failed',
|
||||
errorSummary: timedOut
|
||||
? 'Model invocation deadline exceeded'
|
||||
: lost
|
||||
? 'Model invocation outcome is unknown'
|
||||
: 'Model invocation failed',
|
||||
}
|
||||
: { outputRef: `model-invocation:${requestId}` }),
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 2,
|
||||
expectedRunEventSequence: 2,
|
||||
eventId: identity.eventId,
|
||||
dedupeKey: identity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
);
|
||||
const completionAudit = audit(failed ? 'failed' : 'completed', {
|
||||
requestId,
|
||||
occurredAtMs: NOW + 25,
|
||||
outputBytes: failed ? 0 : 12,
|
||||
usage: failed ? null : { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
|
||||
errorCode,
|
||||
});
|
||||
return createModelInvocationCompletionCommand(
|
||||
startCommand.start,
|
||||
completionAudit,
|
||||
mutation,
|
||||
);
|
||||
}
|
||||
|
||||
test('model invocation reuses one StepRun mutation chain without content', () => {
|
||||
const start = startFixture();
|
||||
const completion = completionFixture();
|
||||
|
||||
assert.deepEqual(normalizeModelInvocationStartCommand(start), start);
|
||||
assert.deepEqual(
|
||||
normalizeModelInvocationCompletionCommand(completion),
|
||||
completion,
|
||||
);
|
||||
assert.equal(start.stepRunMutation.previousStatus, 'ready');
|
||||
assert.equal(start.stepRunMutation.stepRun.status, 'running');
|
||||
assert.equal(completion.stepRunMutation.previousStatus, 'running');
|
||||
assert.equal(completion.stepRunMutation.stepRun.status, 'succeeded');
|
||||
assert.equal(completion.completion.outcome, 'succeeded');
|
||||
assert.equal(
|
||||
JSON.stringify({ start, completion }).includes('top secret prompt'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('deadline completion is terminal timed_out and not replayable', () => {
|
||||
const completion = completionFixture({
|
||||
errorCode: 'MODEL_INVOCATION_DEADLINE_EXCEEDED',
|
||||
});
|
||||
|
||||
assert.equal(completion.completion.outcome, 'timed_out');
|
||||
assert.equal(completion.stepRunMutation.stepRun.status, 'timed_out');
|
||||
assert.equal(
|
||||
completion.stepRunMutation.stepRun.resultCode,
|
||||
'model_deadline_exceeded',
|
||||
);
|
||||
});
|
||||
|
||||
test('unknown provider outcome maps to lost for manual resolution', () => {
|
||||
const completion = completionFixture({
|
||||
errorCode: 'MODEL_INVOCATION_ABORTED',
|
||||
});
|
||||
|
||||
assert.equal(completion.completion.outcome, 'outcome_unknown');
|
||||
assert.equal(completion.stepRunMutation.stepRun.status, 'lost');
|
||||
assert.equal(
|
||||
completion.stepRunMutation.stepRun.resultCode,
|
||||
'model_outcome_unknown',
|
||||
);
|
||||
});
|
||||
|
||||
test('max-length invocation IDs derive bounded stable mutation identities', () => {
|
||||
const requestId = `r${'x'.repeat(127)}`;
|
||||
const first = createModelInvocationMutationIdentity(requestId, 'start');
|
||||
const replay = createModelInvocationMutationIdentity(requestId, 'start');
|
||||
|
||||
assert.deepEqual(first, replay);
|
||||
assert.ok(first.mutationId.length <= 128);
|
||||
assert.ok(first.eventId.length <= 128);
|
||||
assert.ok(first.dedupeKey.length <= 128);
|
||||
assert.doesNotThrow(() => startFixture(requestId));
|
||||
});
|
||||
|
||||
test('trace drift and command digest tampering fail closed', () => {
|
||||
const completion = completionFixture();
|
||||
const drifted = {
|
||||
...completion,
|
||||
completion: { ...completion.completion, traceId: 'trace-b' },
|
||||
};
|
||||
const tampered = {
|
||||
...completion,
|
||||
commandDigest: '0'.repeat(64),
|
||||
};
|
||||
|
||||
assert.throws(
|
||||
() => normalizeModelInvocationCompletionCommand(drifted),
|
||||
InvalidModelInvocationError,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeModelInvocationCompletionCommand(tampered),
|
||||
InvalidModelInvocationError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
CRASH_POINTS,
|
||||
setupScenario,
|
||||
verifyScenario,
|
||||
} = require('./fixtures/modelInvocationCrashFixture.cjs');
|
||||
|
||||
const FIXTURE_PATH = path.join(
|
||||
__dirname,
|
||||
'fixtures',
|
||||
'modelInvocationCrashFixture.cjs',
|
||||
);
|
||||
|
||||
test(
|
||||
'survives the SQLite ModelInvocation start and completion crash matrix',
|
||||
{ timeout: 120_000 },
|
||||
async (context) => {
|
||||
const reports = [];
|
||||
for (const profile of ['edge', 'standalone']) {
|
||||
for (const [pointName, point] of Object.entries(CRASH_POINTS)) {
|
||||
const directory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), `ql3-model-invocation-${profile}-`),
|
||||
);
|
||||
context.after(() => {
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
const databasePath = path.join(directory, 'runtime.sqlite');
|
||||
const statePath = path.join(directory, 'state.json');
|
||||
const markerPath = path.join(directory, 'crash-marker.json');
|
||||
await setupScenario({
|
||||
databasePath,
|
||||
statePath,
|
||||
profile,
|
||||
operation: point.operation,
|
||||
});
|
||||
const crashed = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
FIXTURE_PATH,
|
||||
'crash',
|
||||
databasePath,
|
||||
statePath,
|
||||
markerPath,
|
||||
pointName,
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
timeout: 20_000,
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
crashed.error,
|
||||
undefined,
|
||||
`${profile}/${pointName}: ${crashed.error?.message}`,
|
||||
);
|
||||
assert.equal(
|
||||
crashed.signal,
|
||||
'SIGKILL',
|
||||
`${profile}/${pointName}: status=${crashed.status}, stderr=${crashed.stderr}`,
|
||||
);
|
||||
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
||||
assert.deepEqual(marker, {
|
||||
schema: 'qinglong/sqlite-model-invocation-crash-marker@v1',
|
||||
point: pointName,
|
||||
pid: marker.pid,
|
||||
});
|
||||
reports.push(
|
||||
await verifyScenario({ databasePath, statePath, pointName }),
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.equal(reports.length, 14);
|
||||
assert.equal(
|
||||
reports.filter((report) => report.crashBeforeCommit).length,
|
||||
10,
|
||||
);
|
||||
assert.equal(
|
||||
reports.filter((report) => report.durableAfterCrash).length,
|
||||
4,
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...new Set(reports.map((report) => report.journalMode))].sort(),
|
||||
['delete', 'wal'],
|
||||
);
|
||||
assert.ok(reports.every((report) => report.integrityCheck === 'ok'));
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,992 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const {
|
||||
LOCAL_PLUGIN_PACKAGE_PROMPT_ADMISSION_MIGRATION_ID,
|
||||
LOCAL_PLUGIN_PACKAGE_PROMPT_FINALIZATION_MIGRATION_ID,
|
||||
LOCAL_PLUGIN_PACKAGE_PROMPT_OUTPUT_ARTIFACT_MIGRATION_ID,
|
||||
LOCAL_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_RETIREMENT_MIGRATION_ID,
|
||||
LOCAL_PLUGIN_PACKAGE_PROMPT_OUTPUT_TOMBSTONE_MIGRATION_ID,
|
||||
LOCAL_MODEL_INVOCATION_MIGRATION_ID,
|
||||
LOCAL_MODEL_INVOCATION_FEATURE_ACTIVATION_MIGRATION_ID,
|
||||
LOCAL_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
|
||||
LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
|
||||
LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
LOCAL_MODEL_PROVIDER_CREDENTIAL_CATALOG_MIGRATION_ID,
|
||||
LOCAL_MODEL_INVOCATION_QUOTA_MIGRATION_ID,
|
||||
LOCAL_MODEL_INVOCATION_PRICING_MIGRATION_ID,
|
||||
LOCAL_MODEL_INVOCATION_USAGE_MIGRATION_ID,
|
||||
LOCAL_MODEL_PRICE_CATALOG_AUTHORIZATION_MIGRATION_ID,
|
||||
LOCAL_MODEL_PRICE_CATALOG_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
POSTGRES_MODEL_INVOCATION_SCHEMA,
|
||||
POSTGRES_MODEL_PROVIDER_CREDENTIAL_CATALOG_MIGRATION_ID,
|
||||
POSTGRES_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_MIGRATION_ID,
|
||||
POSTGRES_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_IDENTITY_MIGRATION_ID,
|
||||
POSTGRES_MODEL_PROVIDER_CREDENTIAL_TEST_CONNECTION_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_QUOTA_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_PRICING_MIGRATION_ID,
|
||||
POSTGRES_MODEL_INVOCATION_USAGE_MIGRATION_ID,
|
||||
POSTGRES_MODEL_PRICE_CATALOG_AUTHORIZATION_MIGRATION_ID,
|
||||
POSTGRES_MODEL_PRICE_CATALOG_MIGRATION_ID,
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_ADMISSION_MIGRATION_ID,
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_FINALIZATION_MIGRATION_ID,
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_ARTIFACT_MIGRATION_ID,
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_RETIREMENT_MIGRATION_ID,
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_ROTATION_MIGRATION_ID,
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID,
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_TOMBSTONE_MIGRATION_ID,
|
||||
LocalModelInvocationFeatureNotReadyError,
|
||||
assertLocalModelInvocationFeatureReady,
|
||||
localModelInvocationMigrationDefinition,
|
||||
migrateLocalModelInvocationFeature,
|
||||
postgresModelInvocationMigrationDefinition,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
|
||||
function createMainSqliteContract(client) {
|
||||
client.exec(`
|
||||
CREATE TABLE "QingLong3SchemaMigrations" (
|
||||
migration_id TEXT PRIMARY KEY,
|
||||
stream_id TEXT NOT NULL,
|
||||
dialect TEXT NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
applied_at_ms INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE "Runs" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "RunEvents" (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "StepRuns" (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
UNIQUE (run_id, id)
|
||||
);
|
||||
CREATE TABLE "StepRunMutations" (mutation_id TEXT PRIMARY KEY);
|
||||
`);
|
||||
}
|
||||
|
||||
function localFeatureTables(client) {
|
||||
return client
|
||||
.prepare(
|
||||
`SELECT name FROM sqlite_schema
|
||||
WHERE type = 'table' AND
|
||||
(name LIKE 'ModelInvocation%' OR name LIKE 'ModelPriceCatalog%')
|
||||
ORDER BY name`,
|
||||
)
|
||||
.all()
|
||||
.map((row) => row.name);
|
||||
}
|
||||
|
||||
test('SQLite AI schema is an explicit independent feature migration', async () => {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
createMainSqliteContract(client);
|
||||
|
||||
assert.deepEqual(localFeatureTables(client), []);
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
assert.deepEqual(localFeatureTables(client), [
|
||||
'ModelInvocationCompletions',
|
||||
'ModelInvocationFeatureHead',
|
||||
'ModelInvocationFeatureTransitions',
|
||||
'ModelInvocationPriceQuotes',
|
||||
'ModelInvocationPriceSettlements',
|
||||
'ModelInvocationPromptAdmissions',
|
||||
'ModelInvocationPromptFinalizations',
|
||||
'ModelInvocationPromptOutputArtifactTombstones',
|
||||
'ModelInvocationPromptOutputArtifacts',
|
||||
'ModelInvocationPromptOutputKeyRetirementCompletions',
|
||||
'ModelInvocationPromptOutputKeyRetirementPreparations',
|
||||
'ModelInvocationProviderCredentialAudits',
|
||||
'ModelInvocationProviderCredentialBindings',
|
||||
'ModelInvocationProviderCredentialTransitions',
|
||||
'ModelInvocationQuotaReservations',
|
||||
'ModelInvocationQuotaSettlements',
|
||||
'ModelInvocationResolutions',
|
||||
'ModelInvocationStarts',
|
||||
'ModelInvocationUsageLedger',
|
||||
'ModelPriceCatalogAuthorizations',
|
||||
'ModelPriceCatalogHeads',
|
||||
'ModelPriceCatalogPublications',
|
||||
]);
|
||||
const history = client
|
||||
.prepare(
|
||||
`SELECT migration_id, stream_id, checksum
|
||||
FROM "${LOCAL_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE}"
|
||||
ORDER BY migration_id`,
|
||||
)
|
||||
.all();
|
||||
assert.deepEqual(
|
||||
history.map(({ migration_id, stream_id }) => ({
|
||||
migration_id,
|
||||
stream_id,
|
||||
})),
|
||||
[
|
||||
{
|
||||
migration_id: LOCAL_MODEL_INVOCATION_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_MODEL_INVOCATION_USAGE_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_MODEL_INVOCATION_QUOTA_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_MODEL_INVOCATION_PRICING_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_MODEL_PRICE_CATALOG_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_MODEL_PRICE_CATALOG_AUTHORIZATION_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_MODEL_INVOCATION_FEATURE_ACTIVATION_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_PLUGIN_PACKAGE_PROMPT_ADMISSION_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_PLUGIN_PACKAGE_PROMPT_FINALIZATION_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_PLUGIN_PACKAGE_PROMPT_OUTPUT_ARTIFACT_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_PLUGIN_PACKAGE_PROMPT_OUTPUT_TOMBSTONE_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id:
|
||||
LOCAL_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_RETIREMENT_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
{
|
||||
migration_id: LOCAL_MODEL_PROVIDER_CREDENTIAL_CATALOG_MIGRATION_ID,
|
||||
stream_id: LOCAL_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
},
|
||||
],
|
||||
);
|
||||
assert.equal(
|
||||
history[0].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[0].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[0].checksum,
|
||||
'258e3fd9a250d53d7d0574c3c05b3f91c40c53b20a60b68a931babfc58a0451a',
|
||||
);
|
||||
assert.equal(
|
||||
history[1].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[1].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[1].checksum,
|
||||
'37e5c9bbf3f459ff036a032fc564dfd8cd78325234c9d7916bfff886b1018da5',
|
||||
);
|
||||
assert.equal(
|
||||
history[2].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[2].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[2].checksum,
|
||||
'fa734aac1a3f5affaf69f4fbe53a2c6ca628255ecdcde14c08b87b49d8162012',
|
||||
);
|
||||
assert.equal(
|
||||
history[3].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[3].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[3].checksum,
|
||||
'572e37d2f44df43a50b51a07c1b4b0bb87fbb22e9cafbd3421ec7ab250036951',
|
||||
);
|
||||
assert.equal(
|
||||
history[4].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[4].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[4].checksum,
|
||||
'20d5c288dfab65ac7ea75a96b7302f9d59cd1bfdf06af28f3868261f6e2e3013',
|
||||
);
|
||||
assert.equal(
|
||||
history[5].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[5].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[5].checksum,
|
||||
'3ee48d1468569c9dc1fa9f04031a48a220161762d48eeac4cd924e2dcd7abd21',
|
||||
);
|
||||
assert.equal(
|
||||
history[6].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[6].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[6].checksum,
|
||||
'2454987c61a48dc5286a883d755c709000e6fd630025373cb276723001bdcc6c',
|
||||
);
|
||||
assert.equal(
|
||||
history[7].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[7].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[7].checksum,
|
||||
'7f0b675231a79a5917dab1b7088ac8c393afef448e35309ca2e3a691af45bc79',
|
||||
);
|
||||
assert.equal(
|
||||
history[8].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[8].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[8].checksum,
|
||||
'bd4c6f9f72a16f7a0e8f6d7afc702c7fe1293e7fc6f60bc31f5604b30bbdd0b6',
|
||||
);
|
||||
assert.equal(
|
||||
history[9].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[9].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[9].checksum,
|
||||
'79bf3edcccf273046cb8b8ab60a7a0da881efd4e641009fa3c5833013cb7a75b',
|
||||
);
|
||||
assert.equal(
|
||||
history[10].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[10].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[10].checksum,
|
||||
'4283d738e3eeb99fce30d011fd7d090577aa897581ee83e691ae021eff3f369e',
|
||||
);
|
||||
assert.equal(
|
||||
history[11].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[11].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[11].checksum,
|
||||
'213d255cc40c536bbdc5fa41691839e0eb8e869db5a53ed93df50295ca8d5fc5',
|
||||
);
|
||||
assert.equal(
|
||||
history[12].checksum,
|
||||
localModelInvocationMigrationDefinition.migrations[12].checksum,
|
||||
);
|
||||
assert.equal(
|
||||
localModelInvocationMigrationDefinition.migrations[12].checksum,
|
||||
'ca9bcb4370d747884a34fdeae5079fdd7ccdaadd5ba9f66dba2faab09cfa3abb',
|
||||
);
|
||||
assert.equal(
|
||||
LOCAL_MODEL_INVOCATION_MIGRATION_PLAN_DIGEST,
|
||||
'2720c6e45f82adbb03641d1c19e8ff7e1875a763a0b53d4910a46ca308800aa0',
|
||||
);
|
||||
assert.deepEqual(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT migration_id FROM "QingLong3SchemaMigrations"
|
||||
ORDER BY migration_id`,
|
||||
)
|
||||
.all(),
|
||||
[],
|
||||
);
|
||||
assert.equal(
|
||||
client.prepare('PRAGMA integrity_check').get().integrity_check,
|
||||
'ok',
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('SQLite AI feature readiness is read-only and rejects partial or drifted schema', async () => {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
client.exec('PRAGMA foreign_keys = ON');
|
||||
createMainSqliteContract(client);
|
||||
|
||||
assert.throws(
|
||||
() => assertLocalModelInvocationFeatureReady(client),
|
||||
LocalModelInvocationFeatureNotReadyError,
|
||||
);
|
||||
await migrateLocalModelInvocationFeature(client);
|
||||
assert.doesNotThrow(() => assertLocalModelInvocationFeatureReady(client));
|
||||
|
||||
client
|
||||
.prepare(
|
||||
`UPDATE "${LOCAL_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE}"
|
||||
SET checksum = ?
|
||||
WHERE migration_id = ?`,
|
||||
)
|
||||
.run('f'.repeat(64), LOCAL_MODEL_PRICE_CATALOG_AUTHORIZATION_MIGRATION_ID);
|
||||
assert.throws(
|
||||
() => assertLocalModelInvocationFeatureReady(client),
|
||||
LocalModelInvocationFeatureNotReadyError,
|
||||
);
|
||||
assert.equal(
|
||||
client
|
||||
.prepare(
|
||||
`SELECT count(*) AS count
|
||||
FROM "${LOCAL_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE}"`,
|
||||
)
|
||||
.get().count,
|
||||
13,
|
||||
);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('SQLite AI migration refuses to create a parallel baseline', async () => {
|
||||
const client = new DatabaseSync(':memory:');
|
||||
|
||||
await assert.rejects(
|
||||
migrateLocalModelInvocationFeature(client),
|
||||
/requires the main SQLite migration stream/,
|
||||
);
|
||||
assert.deepEqual(localFeatureTables(client), []);
|
||||
client.close();
|
||||
});
|
||||
|
||||
test('PostgreSQL AI schema is an independent reviewed feature stream', async () => {
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
'ql3-ai-model-invocation-postgresql',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_ID,
|
||||
'pg-9001-ai-model-invocations',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_INVOCATION_USAGE_MIGRATION_ID,
|
||||
'pg-9002-ai-model-usage-ledger',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_INVOCATION_QUOTA_MIGRATION_ID,
|
||||
'pg-9003-ai-model-usage-quota',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_INVOCATION_PRICING_MIGRATION_ID,
|
||||
'pg-9004-ai-model-pricing-snapshots',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_PRICE_CATALOG_MIGRATION_ID,
|
||||
'pg-9005-ai-model-price-catalog',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_PRICE_CATALOG_AUTHORIZATION_MIGRATION_ID,
|
||||
'pg-9006-ai-model-price-catalog-authorizations',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_ADMISSION_MIGRATION_ID,
|
||||
'pg-9007-ai-plugin-package-prompt-admissions',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_FINALIZATION_MIGRATION_ID,
|
||||
'pg-9008-ai-plugin-package-prompt-finalizations',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_ARTIFACT_MIGRATION_ID,
|
||||
'pg-9009-ai-plugin-package-prompt-output-artifacts',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_TOMBSTONE_MIGRATION_ID,
|
||||
'pg-9010-ai-plugin-package-prompt-output-tombstones',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_RETIREMENT_MIGRATION_ID,
|
||||
'pg-9011-ai-plugin-package-prompt-output-key-retirements',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_PROVIDER_CREDENTIAL_CATALOG_MIGRATION_ID,
|
||||
'pg-9012-ai-model-provider-credential-catalog',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_MIGRATION_ID,
|
||||
'pg-9013-ai-model-provider-credential-management-boundary',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_IDENTITY_MIGRATION_ID,
|
||||
'pg-9014-ai-model-provider-credential-management-identity-ledger',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_PROVIDER_CREDENTIAL_TEST_CONNECTION_MIGRATION_ID,
|
||||
'pg-9015-ai-model-provider-credential-test-connection',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_ROTATION_MIGRATION_ID,
|
||||
'pg-9016-ai-plugin-package-prompt-output-key-rotation',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_PLUGIN_PACKAGE_PROMPT_PRODUCT_AUTHORIZATION_MIGRATION_ID,
|
||||
'pg-9017-ai-plugin-package-prompt-product-authorization',
|
||||
);
|
||||
assert.equal(
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_HISTORY_TABLE,
|
||||
'ai_schema_migrations',
|
||||
);
|
||||
assert.equal(POSTGRES_MODEL_INVOCATION_SCHEMA, 'ql3_ai');
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrationIdScheme,
|
||||
'postgres-prefixed',
|
||||
);
|
||||
assert.match(
|
||||
postgresModelInvocationMigrationDefinition.migrations[0].checksum,
|
||||
/^[0-9a-f]{64}$/,
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[0].checksum,
|
||||
'69f72286fba2988ba372f006eb894a7f8b89f4b1acd9da68dc1cdafc3ca96ea7',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[1].checksum,
|
||||
'95ad6f46163b0bbc2583dddf492f91f767a00554683186f244d3f6a22a2ad00c',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[2].checksum,
|
||||
'13ea1a904eb799bcae1b474d76b164a70748bdcca8e1e6ded9952921a291a855',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[3].checksum,
|
||||
'd38b12c2640fdd9fe21dc43a4743fb3480c988fa0a87e210fd81074d87569d2f',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[4].checksum,
|
||||
'7db1a80fab1aa3dee3a4c4bcae5add53758418504f63f4b7d253b090506d7864',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[5].checksum,
|
||||
'486d46115e28e90604a47231fe95e3b1687649c063d93bf7ce267783f2a7165f',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[6].checksum,
|
||||
'1ed94eae2225b26e89e8c5d34e265e143e8150ff1811f5d8ae1fda52a6603db0',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[7].checksum,
|
||||
'8dfd02e8e4947acb03516795ae37ef7aae5ee52a25b1fe3cd1a8640782c5b235',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[8].checksum,
|
||||
'cb4156b109694c2b3aaf60a870179da04e10b47dbddfc92db3a7c4a58dfd1c2d',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[9].checksum,
|
||||
'8972237fa41c80d131c32b380f617e8c7720687980e9accfb08f8fb19b7d8b5f',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[10].checksum,
|
||||
'843ea53460f6580801cb12428d35c5571fd36bbee64e7801d2c3f98ea39d8392',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[11].checksum,
|
||||
'4c83f5dda3c922aefd77c58760881f926e63e33628120f2fe6c71a53b30a1248',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[12].checksum,
|
||||
'c02a4c6b2953cc331580b6287283d33739c30c2e189d011f392c13ecea497224',
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations[13].checksum,
|
||||
'02098fad764199bc5a7750483d050be5e5acdbcecd05c0975c3fe4e5be03c782',
|
||||
);
|
||||
assert.match(
|
||||
postgresModelInvocationMigrationDefinition.migrations[14].checksum,
|
||||
/^[0-9a-f]{64}$/,
|
||||
);
|
||||
assert.match(
|
||||
postgresModelInvocationMigrationDefinition.migrations[16].checksum,
|
||||
/^[0-9a-f]{64}$/,
|
||||
);
|
||||
assert.equal(
|
||||
postgresModelInvocationMigrationDefinition.migrations.length,
|
||||
17,
|
||||
);
|
||||
|
||||
const retirementStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[10].up({
|
||||
async query(statement) {
|
||||
retirementStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const retirementSql = retirementStatements.join('\n');
|
||||
assert.match(
|
||||
retirementSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_prompt_output_key_retirement_preparations"/,
|
||||
);
|
||||
assert.match(
|
||||
retirementSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_prompt_output_key_retirement_completions"/,
|
||||
);
|
||||
assert.match(retirementSql, /TO ql3_runtime/);
|
||||
assert.match(retirementSql, /TO ql3_ai_maintenance/);
|
||||
|
||||
const credentialStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[11].up({
|
||||
async query(statement) {
|
||||
credentialStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const credentialSql = credentialStatements.join('\n');
|
||||
assert.match(
|
||||
credentialSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_provider_credential_bindings"/,
|
||||
);
|
||||
assert.match(
|
||||
credentialSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_provider_credential_transitions"/,
|
||||
);
|
||||
assert.match(
|
||||
credentialSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_provider_credential_audits"/,
|
||||
);
|
||||
assert.match(credentialSql, /TO ql3_runtime/);
|
||||
assert.match(credentialSql, /TO ql3_ai_maintenance/);
|
||||
|
||||
const credentialManagementStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[12].up({
|
||||
async query(statement) {
|
||||
credentialManagementStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const credentialManagementSql = credentialManagementStatements.join('\n');
|
||||
assert.match(
|
||||
credentialManagementSql,
|
||||
/GRANT CONNECT ON DATABASE %I TO ql3_ai_credential_manager/,
|
||||
);
|
||||
assert.match(
|
||||
credentialManagementSql,
|
||||
/GRANT SELECT ON TABLE[\s\S]*"ql3"\."projects"[\s\S]*"ql3"\."project_role_bindings"[\s\S]*"ql3"\."security_audit_events"[\s\S]*TO ql3_ai_credential_manager/,
|
||||
);
|
||||
assert.match(
|
||||
credentialManagementSql,
|
||||
/GRANT INSERT ON TABLE "ql3"\."security_audit_events"[\s\S]*TO ql3_ai_credential_manager/,
|
||||
);
|
||||
assert.match(
|
||||
credentialManagementSql,
|
||||
/GRANT INSERT ON TABLE[\s\S]*model_provider_credential_bindings[\s\S]*model_provider_credential_transitions[\s\S]*TO ql3_ai_credential_manager/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
credentialManagementSql,
|
||||
/GRANT[^;]*(?:model_invocation_prompt_output|ql3_ai_maintenance)/,
|
||||
);
|
||||
assert.match(
|
||||
credentialManagementSql,
|
||||
/REVOKE ALL ON TABLE[\s\S]*model_provider_credential_bindings[\s\S]*model_provider_credential_transitions[\s\S]*model_provider_credential_audits[\s\S]*FROM ql3_ai_maintenance/,
|
||||
);
|
||||
|
||||
const credentialIdentityStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[13].up({
|
||||
async query(statement) {
|
||||
credentialIdentityStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const credentialIdentitySql = credentialIdentityStatements.join('\n');
|
||||
assert.match(
|
||||
credentialIdentitySql,
|
||||
/CREATE TABLE "ql3_ai"\."model_provider_credential_management_identity_keyset_ledger"/,
|
||||
);
|
||||
assert.match(
|
||||
credentialIdentitySql,
|
||||
/GRANT SELECT, INSERT, UPDATE[\s\S]*TO ql3_ai_credential_manager/,
|
||||
);
|
||||
assert.match(credentialIdentitySql, /FROM PUBLIC, ql3_ai_maintenance/);
|
||||
assert.doesNotMatch(credentialIdentitySql, /GRANT[^;]*DELETE/);
|
||||
|
||||
const credentialTestStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[14].up({
|
||||
async query(statement) {
|
||||
credentialTestStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const credentialTestSql = credentialTestStatements.join('\n');
|
||||
for (const table of [
|
||||
'model_provider_credential_test_plans',
|
||||
'model_provider_credential_test_quota_buckets',
|
||||
'model_provider_credential_test_executions',
|
||||
'model_provider_credential_test_results',
|
||||
]) {
|
||||
assert.match(
|
||||
credentialTestSql,
|
||||
new RegExp(`CREATE TABLE "ql3_ai"\\."${table}"`),
|
||||
);
|
||||
}
|
||||
assert.match(
|
||||
credentialTestSql,
|
||||
/GRANT CONNECT ON DATABASE %I TO ql3_ai_credential_tester/,
|
||||
);
|
||||
assert.match(
|
||||
credentialTestSql,
|
||||
/GRANT SELECT, INSERT, UPDATE ON TABLE[\s\S]*model_provider_credential_test_quota_buckets[\s\S]*TO ql3_ai_credential_manager/,
|
||||
);
|
||||
assert.match(
|
||||
credentialTestSql,
|
||||
/GRANT INSERT ON TABLE[\s\S]*model_provider_credential_test_executions[\s\S]*model_provider_credential_test_results[\s\S]*TO ql3_ai_credential_tester/,
|
||||
);
|
||||
for (const statement of credentialTestStatements.filter((value) =>
|
||||
value.includes('TO ql3_ai_credential_tester'),
|
||||
)) {
|
||||
assert.doesNotMatch(statement, /GRANT[\s\S]*(?:UPDATE|DELETE)/);
|
||||
}
|
||||
|
||||
const rotationStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[15].up({
|
||||
async query(statement) {
|
||||
rotationStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const rotationSql = rotationStatements.join('\n');
|
||||
assert.match(
|
||||
rotationSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_prompt_output_key_rotation_preparations"/,
|
||||
);
|
||||
assert.match(
|
||||
rotationSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_prompt_output_key_rotation_completions"/,
|
||||
);
|
||||
assert.match(
|
||||
rotationSql,
|
||||
/UNIQUE \(expected_secret_uid, expected_catalog_digest\)/,
|
||||
);
|
||||
assert.match(rotationSql, /TO ql3_ai_maintenance/);
|
||||
assert.doesNotMatch(rotationSql, /GRANT[^;]*TO ql3_runtime/);
|
||||
assert.doesNotMatch(rotationSql, /GRANT[^;]*(?:UPDATE|DELETE)/);
|
||||
|
||||
const productAuthorizationStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[16].up({
|
||||
async query(statement) {
|
||||
productAuthorizationStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const productAuthorizationSql = productAuthorizationStatements.join('\n');
|
||||
assert.match(
|
||||
productAuthorizationSql,
|
||||
/CREATE FUNCTION[\s\S]*plugin_package_prompt_authorize_admission/,
|
||||
);
|
||||
assert.match(productAuthorizationSql, /SECURITY DEFINER/);
|
||||
assert.match(productAuthorizationSql, /TO ql3_runtime/);
|
||||
assert.doesNotMatch(productAuthorizationSql, /GRANT[^;]*TO ql3_admin/);
|
||||
|
||||
const statements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[0].up({
|
||||
async query(statement) {
|
||||
statements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const sql = statements.join('\n');
|
||||
assert.match(sql, /CREATE TABLE "ql3_ai"\."model_invocation_starts"/);
|
||||
assert.match(sql, /CREATE TABLE "ql3_ai"\."model_invocation_completions"/);
|
||||
assert.match(sql, /CREATE TABLE "ql3_ai"\."model_invocation_resolutions"/);
|
||||
assert.match(
|
||||
sql,
|
||||
/FOREIGN KEY \(mutation_id\)[\s\S]*"ql3"\."step_run_mutations"/,
|
||||
);
|
||||
assert.match(sql, /FOREIGN KEY \(run_event_id\)[\s\S]*"ql3"\."run_events"/);
|
||||
assert.match(
|
||||
sql,
|
||||
/REVOKE ALL ON TABLE[\s\S]*model_invocation_starts[\s\S]*FROM PUBLIC/,
|
||||
);
|
||||
assert.match(
|
||||
sql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_invocation_resolutions[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.doesNotMatch(sql, /model_invocation_(?:starts|completions)_step_uidx/);
|
||||
assert.match(sql, /model_invocation_starts_step_history_idx/);
|
||||
assert.match(sql, /model_invocation_completions_step_history_idx/);
|
||||
assert.doesNotMatch(sql, /TO ql3_admin/);
|
||||
assert.doesNotMatch(sql, /TO ql3_worker_ingress/);
|
||||
|
||||
const usageStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[1].up({
|
||||
async query(statement) {
|
||||
usageStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const usageSql = usageStatements.join('\n');
|
||||
assert.match(
|
||||
usageSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_usage_ledger"/,
|
||||
);
|
||||
assert.match(
|
||||
usageSql,
|
||||
/FOREIGN KEY \(invocation_id, completion_digest\)[\s\S]*model_invocation_completions/,
|
||||
);
|
||||
assert.match(
|
||||
usageSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_invocation_usage_ledger[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.deepEqual(
|
||||
usageStatements.filter((statement) => statement.startsWith('GRANT ')),
|
||||
[
|
||||
`GRANT SELECT, INSERT ON TABLE
|
||||
"ql3_ai"."model_invocation_usage_ledger"
|
||||
TO ql3_runtime`,
|
||||
],
|
||||
);
|
||||
assert.doesNotMatch(usageSql, /TO ql3_admin|TO ql3_worker_ingress/);
|
||||
|
||||
const quotaStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[2].up({
|
||||
async query(statement) {
|
||||
quotaStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const quotaSql = quotaStatements.join('\n');
|
||||
assert.match(
|
||||
quotaSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_quota_reservations"/,
|
||||
);
|
||||
assert.match(
|
||||
quotaSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_quota_settlements"/,
|
||||
);
|
||||
assert.match(
|
||||
quotaSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_invocation_quota_reservations[\s\S]*model_invocation_quota_settlements[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.deepEqual(
|
||||
quotaStatements.filter((statement) =>
|
||||
/^(?:UPDATE|DELETE)\b/.test(statement),
|
||||
),
|
||||
[],
|
||||
);
|
||||
assert.doesNotMatch(quotaSql, /TO ql3_admin|TO ql3_worker_ingress/);
|
||||
|
||||
const pricingStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[3].up({
|
||||
async query(statement) {
|
||||
pricingStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const pricingSql = pricingStatements.join('\n');
|
||||
assert.match(
|
||||
pricingSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_price_quotes"/,
|
||||
);
|
||||
assert.match(
|
||||
pricingSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_price_settlements"/,
|
||||
);
|
||||
assert.match(
|
||||
pricingSql,
|
||||
/FOREIGN KEY \(invocation_id, quote_digest\)[\s\S]*model_invocation_price_quotes/,
|
||||
);
|
||||
assert.match(
|
||||
pricingSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_invocation_price_quotes[\s\S]*model_invocation_price_settlements[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.deepEqual(
|
||||
pricingStatements.filter((statement) =>
|
||||
/^(?:UPDATE|DELETE)\b/.test(statement),
|
||||
),
|
||||
[],
|
||||
);
|
||||
assert.doesNotMatch(pricingSql, /TO ql3_admin|TO ql3_worker_ingress/);
|
||||
|
||||
const catalogStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[4].up({
|
||||
async query(statement) {
|
||||
catalogStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const catalogSql = catalogStatements.join('\n');
|
||||
assert.match(
|
||||
catalogSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_price_catalog_publications"/,
|
||||
);
|
||||
assert.match(
|
||||
catalogSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_price_catalog_heads"/,
|
||||
);
|
||||
assert.match(
|
||||
catalogSql,
|
||||
/GRANT SELECT ON TABLE[\s\S]*model_price_catalog_publications[\s\S]*model_price_catalog_heads[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.match(
|
||||
catalogSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_price_catalog_publications[\s\S]*model_price_catalog_heads[\s\S]*TO ql3_admin/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
catalogSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
catalogSql,
|
||||
/TO ql3_package_manager|TO ql3_package_executor|TO ql3_worker_ingress/,
|
||||
);
|
||||
assert.deepEqual(
|
||||
catalogStatements.filter((statement) =>
|
||||
/^(?:UPDATE|DELETE)\b/.test(statement),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const authorizationStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[5].up({
|
||||
async query(statement) {
|
||||
authorizationStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const authorizationSql = authorizationStatements.join('\n');
|
||||
assert.match(
|
||||
authorizationSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_price_catalog_authorizations"/,
|
||||
);
|
||||
assert.match(
|
||||
authorizationSql,
|
||||
/FOREIGN KEY \(publication_digest\)[\s\S]*model_price_catalog_publications/,
|
||||
);
|
||||
assert.match(
|
||||
authorizationSql,
|
||||
/FOREIGN KEY \(head_digest\)[\s\S]*model_price_catalog_heads/,
|
||||
);
|
||||
assert.match(
|
||||
authorizationSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_price_catalog_authorizations[\s\S]*TO ql3_admin/,
|
||||
);
|
||||
assert.doesNotMatch(authorizationSql, /TO ql3_runtime/);
|
||||
assert.deepEqual(
|
||||
authorizationStatements.filter((statement) =>
|
||||
/^(?:UPDATE|DELETE)\b/.test(statement),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const promptAdmissionStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[6].up({
|
||||
async query(statement) {
|
||||
promptAdmissionStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const promptAdmissionSql = promptAdmissionStatements.join('\n');
|
||||
assert.match(
|
||||
promptAdmissionSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_prompt_admissions"/,
|
||||
);
|
||||
assert.match(
|
||||
promptAdmissionSql,
|
||||
/REFERENCES "ql3"\."plugin_package_automation_publications"/,
|
||||
);
|
||||
assert.match(
|
||||
promptAdmissionSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_invocation_prompt_admissions[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.match(
|
||||
promptAdmissionSql,
|
||||
/CREATE FUNCTION[\s\S]*"ql3_ai"\."plugin_package_prompt_admission_snapshot"/,
|
||||
);
|
||||
assert.match(promptAdmissionSql, /plugin_package_automation_start_allowed/);
|
||||
assert.match(
|
||||
promptAdmissionSql,
|
||||
/GRANT EXECUTE ON FUNCTION[\s\S]*plugin_package_prompt_admission_snapshot[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
promptAdmissionSql,
|
||||
/TO ql3_admin|TO ql3_package_manager|TO ql3_worker_ingress/,
|
||||
);
|
||||
|
||||
const promptFinalizationStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[7].up({
|
||||
async query(statement) {
|
||||
promptFinalizationStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const promptFinalizationSql = promptFinalizationStatements.join('\n');
|
||||
assert.match(
|
||||
promptFinalizationSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_prompt_finalizations"/,
|
||||
);
|
||||
assert.match(
|
||||
promptFinalizationSql,
|
||||
/FOREIGN KEY \(event_id\)[\s\S]*REFERENCES "ql3"\."run_events"/,
|
||||
);
|
||||
assert.match(
|
||||
promptFinalizationSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_invocation_prompt_finalizations[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
`${promptAdmissionSql}\n${promptFinalizationSql}`,
|
||||
/GRANT (?:UPDATE|DELETE)|TO ql3_admin|TO ql3_worker_ingress/,
|
||||
);
|
||||
|
||||
const promptOutputArtifactStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[8].up({
|
||||
async query(statement) {
|
||||
promptOutputArtifactStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const promptOutputArtifactSql = promptOutputArtifactStatements.join('\n');
|
||||
assert.match(
|
||||
promptOutputArtifactSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_prompt_output_artifacts"/,
|
||||
);
|
||||
assert.match(
|
||||
promptOutputArtifactSql,
|
||||
/FOREIGN KEY \(invocation_id\)[\s\S]*model_invocation_prompt_admissions/,
|
||||
);
|
||||
assert.match(
|
||||
promptOutputArtifactSql,
|
||||
/FOREIGN KEY \(invocation_id\)[\s\S]*model_invocation_starts/,
|
||||
);
|
||||
assert.match(
|
||||
promptOutputArtifactSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_invocation_prompt_output_artifacts[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
promptOutputArtifactSql,
|
||||
/GRANT (?:UPDATE|DELETE)|TO ql3_admin|TO ql3_package_manager|TO ql3_package_executor|TO ql3_worker_ingress/,
|
||||
);
|
||||
|
||||
const promptOutputTombstoneStatements = [];
|
||||
await postgresModelInvocationMigrationDefinition.migrations[9].up({
|
||||
async query(statement) {
|
||||
promptOutputTombstoneStatements.push(statement);
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
const promptOutputTombstoneSql = promptOutputTombstoneStatements.join('\n');
|
||||
assert.match(
|
||||
promptOutputTombstoneSql,
|
||||
/CREATE TABLE "ql3_ai"\."model_invocation_prompt_output_artifact_tombstones"/,
|
||||
);
|
||||
assert.match(
|
||||
promptOutputTombstoneSql,
|
||||
/GRANT CONNECT ON DATABASE %I TO ql3_ai_maintenance/,
|
||||
);
|
||||
assert.match(
|
||||
promptOutputTombstoneSql,
|
||||
/FOREIGN KEY \(invocation_id\)[\s\S]*model_invocation_prompt_admissions/,
|
||||
);
|
||||
assert.match(
|
||||
promptOutputTombstoneSql,
|
||||
/GRANT SELECT ON TABLE[\s\S]*model_invocation_prompt_output_artifact_tombstones[\s\S]*TO ql3_runtime/,
|
||||
);
|
||||
assert.match(
|
||||
promptOutputTombstoneSql,
|
||||
/GRANT SELECT, DELETE ON TABLE[\s\S]*model_invocation_prompt_output_artifacts[\s\S]*TO ql3_ai_maintenance/,
|
||||
);
|
||||
assert.match(
|
||||
promptOutputTombstoneSql,
|
||||
/GRANT SELECT, INSERT ON TABLE[\s\S]*model_invocation_prompt_output_artifact_tombstones[\s\S]*TO ql3_ai_maintenance/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
promptOutputTombstoneSql,
|
||||
/GRANT (?:UPDATE|DELETE)[\s\S]*model_invocation_prompt_output_artifact_tombstones|TO ql3_admin|TO ql3_package_manager|TO ql3_package_executor|TO ql3_worker_ingress/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidModelPriceCatalogError,
|
||||
MODEL_PRICE_CATALOG_HEAD_SCHEMA,
|
||||
MODEL_PRICE_CATALOG_PUBLICATION_SCHEMA,
|
||||
createModelPriceCatalogHead,
|
||||
createModelPriceCatalogPublication,
|
||||
createModelPriceCatalogPublishCommand,
|
||||
createModelPriceCatalogTransitionCommand,
|
||||
normalizeModelPriceCatalogHead,
|
||||
normalizeModelPriceCatalogPublication,
|
||||
normalizeModelPriceCatalogPublishCommand,
|
||||
normalizeModelPriceCatalogTransitionCommand,
|
||||
} = require('../dist/pricing/modelPriceCatalog.js');
|
||||
|
||||
const NOW = 1_000_000;
|
||||
|
||||
function publishCommand(overrides = {}) {
|
||||
return createModelPriceCatalogPublishCommand({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-1',
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: 150_000,
|
||||
outputMicrosPerMillionTokens: 600_000,
|
||||
mutationId: 'publish-price-1',
|
||||
publishedByUserId: 'user-admin',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function transitionCommand(overrides = {}) {
|
||||
return createModelPriceCatalogTransitionCommand({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
expectedGeneration: 0,
|
||||
expectedHeadDigest: null,
|
||||
action: 'activate',
|
||||
priceRevision: 'price-1',
|
||||
mutationId: 'activate-price-1',
|
||||
changedByUserId: 'user-admin',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test('publication binds a database time, exact price and User mutation', () => {
|
||||
const command = publishCommand();
|
||||
const publication = createModelPriceCatalogPublication(command, NOW);
|
||||
|
||||
assert.deepEqual(normalizeModelPriceCatalogPublishCommand(command), command);
|
||||
assert.equal(publication.schema, MODEL_PRICE_CATALOG_PUBLICATION_SCHEMA);
|
||||
assert.equal(publication.entry.publishedAtMs, NOW);
|
||||
assert.equal(publication.publishedByUserId, 'user-admin');
|
||||
assert.deepEqual(
|
||||
normalizeModelPriceCatalogPublication(publication),
|
||||
publication,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelPriceCatalogPublishCommand({
|
||||
...command,
|
||||
inputMicrosPerMillionTokens: command.inputMicrosPerMillionTokens + 1,
|
||||
}),
|
||||
InvalidModelPriceCatalogError,
|
||||
);
|
||||
});
|
||||
|
||||
test('head transitions activate, deactivate and permanently revoke revisions', () => {
|
||||
const publication = createModelPriceCatalogPublication(publishCommand(), NOW);
|
||||
const activate = transitionCommand();
|
||||
const active = createModelPriceCatalogHead(
|
||||
null,
|
||||
activate,
|
||||
publication,
|
||||
false,
|
||||
NOW + 1,
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeModelPriceCatalogTransitionCommand(activate),
|
||||
activate,
|
||||
);
|
||||
assert.equal(active.schema, MODEL_PRICE_CATALOG_HEAD_SCHEMA);
|
||||
assert.equal(active.generation, 1);
|
||||
assert.equal(active.activePriceRevision, 'price-1');
|
||||
assert.deepEqual(normalizeModelPriceCatalogHead(active), active);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelPriceCatalogHead({
|
||||
...active,
|
||||
action: 'deactivate',
|
||||
}),
|
||||
InvalidModelPriceCatalogError,
|
||||
);
|
||||
|
||||
const deactivate = transitionCommand({
|
||||
expectedGeneration: 1,
|
||||
expectedHeadDigest: active.headDigest,
|
||||
action: 'deactivate',
|
||||
priceRevision: null,
|
||||
mutationId: 'deactivate-price-1',
|
||||
});
|
||||
const inactive = createModelPriceCatalogHead(
|
||||
active,
|
||||
deactivate,
|
||||
null,
|
||||
false,
|
||||
NOW + 2,
|
||||
);
|
||||
assert.equal(inactive.activePriceRevision, null);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelPriceCatalogHead({
|
||||
...inactive,
|
||||
action: 'activate',
|
||||
}),
|
||||
InvalidModelPriceCatalogError,
|
||||
);
|
||||
|
||||
const revoke = transitionCommand({
|
||||
expectedGeneration: 2,
|
||||
expectedHeadDigest: inactive.headDigest,
|
||||
action: 'revoke',
|
||||
mutationId: 'revoke-price-1',
|
||||
});
|
||||
const revoked = createModelPriceCatalogHead(
|
||||
inactive,
|
||||
revoke,
|
||||
publication,
|
||||
false,
|
||||
NOW + 3,
|
||||
);
|
||||
assert.equal(revoked.activePriceRevision, null);
|
||||
assert.equal(revoked.revokedPriceRevision, 'price-1');
|
||||
assert.deepEqual(normalizeModelPriceCatalogHead(revoked), revoked);
|
||||
|
||||
const reactivate = transitionCommand({
|
||||
expectedGeneration: 3,
|
||||
expectedHeadDigest: revoked.headDigest,
|
||||
mutationId: 'reactivate-price-1',
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
createModelPriceCatalogHead(
|
||||
revoked,
|
||||
reactivate,
|
||||
publication,
|
||||
true,
|
||||
NOW + 4,
|
||||
),
|
||||
InvalidModelPriceCatalogError,
|
||||
);
|
||||
});
|
||||
|
||||
test('transition fences reject stale, detached and no-op changes', () => {
|
||||
const publication = createModelPriceCatalogPublication(publishCommand(), NOW);
|
||||
const active = createModelPriceCatalogHead(
|
||||
null,
|
||||
transitionCommand(),
|
||||
publication,
|
||||
false,
|
||||
NOW + 1,
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
createModelPriceCatalogHead(
|
||||
active,
|
||||
transitionCommand({ mutationId: 'stale-activation' }),
|
||||
publication,
|
||||
false,
|
||||
NOW + 2,
|
||||
),
|
||||
InvalidModelPriceCatalogError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createModelPriceCatalogHead(
|
||||
active,
|
||||
transitionCommand({
|
||||
expectedGeneration: 1,
|
||||
expectedHeadDigest: active.headDigest,
|
||||
mutationId: 'duplicate-activation',
|
||||
}),
|
||||
publication,
|
||||
false,
|
||||
NOW + 2,
|
||||
),
|
||||
InvalidModelPriceCatalogError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createModelPriceCatalogTransitionCommand({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
expectedGeneration: 0,
|
||||
expectedHeadDigest: null,
|
||||
action: 'deactivate',
|
||||
priceRevision: 'price-1',
|
||||
mutationId: 'invalid-deactivate',
|
||||
changedByUserId: 'user-admin',
|
||||
}),
|
||||
InvalidModelPriceCatalogError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createModelPriceCatalogHead,
|
||||
createModelPriceCatalogPublication,
|
||||
createModelPriceCatalogPublishCommand,
|
||||
} = require('../dist/pricing/modelPriceCatalog.js');
|
||||
const {
|
||||
InvalidModelPriceCatalogManagementValueError,
|
||||
ModelPriceCatalogManagementAuthenticationError,
|
||||
ModelPriceCatalogManagementAuthorizationError,
|
||||
ModelPriceCatalogManagementSeparationOfDutyError,
|
||||
createModelPriceCatalogAuthorization,
|
||||
createModelPriceCatalogManagementService,
|
||||
createModelPriceCatalogPolicyDecision,
|
||||
normalizeModelPriceCatalogAuthorization,
|
||||
normalizeModelPriceCatalogAuthorizationCommand,
|
||||
} = require('../dist/pricing/modelPriceCatalogManagement.js');
|
||||
|
||||
const NOW = 2_000_000;
|
||||
|
||||
function principal(userId, assurance = 'multi_factor') {
|
||||
return {
|
||||
subject: { type: 'user', id: userId },
|
||||
authenticationId: `auth-${userId}`,
|
||||
authenticatedAtMs: NOW - 1_000,
|
||||
expiresAtMs: NOW + 60_000,
|
||||
assurance,
|
||||
};
|
||||
}
|
||||
|
||||
function policy(effect = 'allow') {
|
||||
return createModelPriceCatalogPolicyDecision({
|
||||
effect,
|
||||
revision: 'platform-policy-7',
|
||||
reasons: [effect === 'allow' ? 'catalog_operator' : 'catalog_denied'],
|
||||
});
|
||||
}
|
||||
|
||||
function fakeRepository(publishedByUserId = 'publisher') {
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
async findPublication(lookup) {
|
||||
calls.push(['findPublication', lookup]);
|
||||
return createModelPriceCatalogPublication(
|
||||
createModelPriceCatalogPublishCommand({
|
||||
provider: lookup.provider,
|
||||
model: lookup.model,
|
||||
priceRevision: lookup.priceRevision,
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: 100,
|
||||
outputMicrosPerMillionTokens: 400,
|
||||
mutationId: 'existing-publication',
|
||||
publishedByUserId,
|
||||
}),
|
||||
NOW - 100,
|
||||
);
|
||||
},
|
||||
async findCurrent() {
|
||||
return null;
|
||||
},
|
||||
async findAuthorization() {
|
||||
return null;
|
||||
},
|
||||
async publish() {
|
||||
throw new Error('raw publish must not be used');
|
||||
},
|
||||
async transition() {
|
||||
throw new Error('raw transition must not be used');
|
||||
},
|
||||
async resolve() {
|
||||
return null;
|
||||
},
|
||||
async publishAuthorized(command, authorizationCommand) {
|
||||
calls.push(['publishAuthorized', command, authorizationCommand]);
|
||||
const publication = createModelPriceCatalogPublication(command, NOW);
|
||||
const authorization = createModelPriceCatalogAuthorization(
|
||||
authorizationCommand,
|
||||
publication.publicationDigest,
|
||||
NOW,
|
||||
);
|
||||
return { status: 'created', publication, authorization };
|
||||
},
|
||||
async transitionAuthorized(command, authorizationCommand) {
|
||||
calls.push(['transitionAuthorized', command, authorizationCommand]);
|
||||
const publication = await this.findPublication({
|
||||
provider: command.provider,
|
||||
model: command.model,
|
||||
priceRevision: command.priceRevision,
|
||||
});
|
||||
const head = createModelPriceCatalogHead(
|
||||
null,
|
||||
command,
|
||||
publication,
|
||||
false,
|
||||
NOW,
|
||||
);
|
||||
const authorization = createModelPriceCatalogAuthorization(
|
||||
authorizationCommand,
|
||||
head.headDigest,
|
||||
NOW,
|
||||
);
|
||||
return { status: 'created', head, authorization };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function publishRequest(userId = 'publisher') {
|
||||
return {
|
||||
authorizationId: 'authorize-publish-1',
|
||||
requestId: 'request-publish-1',
|
||||
mutationId: 'publish-price-1',
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
principal: principal(userId),
|
||||
priceRevision: 'price-1',
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: 150_000,
|
||||
outputMicrosPerMillionTokens: 600_000,
|
||||
};
|
||||
}
|
||||
|
||||
function transitionRequest(userId = 'reviewer') {
|
||||
return {
|
||||
authorizationId: 'authorize-activate-1',
|
||||
requestId: 'request-activate-1',
|
||||
mutationId: 'activate-price-1',
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
principal: principal(userId),
|
||||
expectedGeneration: 0,
|
||||
expectedHeadDigest: null,
|
||||
action: 'activate',
|
||||
priceRevision: 'price-1',
|
||||
};
|
||||
}
|
||||
|
||||
test('authorization fact binds strong User, policy, catalog command and commit', () => {
|
||||
const decision = policy();
|
||||
const serviceRepository = fakeRepository();
|
||||
const service = createModelPriceCatalogManagementService(serviceRepository, {
|
||||
decisionMode: 'human_confirmation',
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return decision;
|
||||
},
|
||||
},
|
||||
now: () => NOW,
|
||||
});
|
||||
|
||||
return service.publish(publishRequest()).then((result) => {
|
||||
assert.equal(result.authorization.operation, 'publish');
|
||||
assert.equal(result.authorization.principal.subject.id, 'publisher');
|
||||
assert.equal(result.authorization.policy.revision, 'platform-policy-7');
|
||||
assert.equal(
|
||||
result.authorization.resultDigest,
|
||||
result.publication.publicationDigest,
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeModelPriceCatalogAuthorization(result.authorization),
|
||||
result.authorization,
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeModelPriceCatalogAuthorizationCommand(
|
||||
serviceRepository.calls[0][2],
|
||||
),
|
||||
serviceRepository.calls[0][2],
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelPriceCatalogAuthorization({
|
||||
...result.authorization,
|
||||
resultDigest: 'f'.repeat(64),
|
||||
}),
|
||||
InvalidModelPriceCatalogManagementValueError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('management rejects weak principals and deny decisions before mutation', async () => {
|
||||
const repository = fakeRepository();
|
||||
let authorizations = 0;
|
||||
const service = createModelPriceCatalogManagementService(repository, {
|
||||
decisionMode: 'human_confirmation',
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
authorizations += 1;
|
||||
return policy('deny');
|
||||
},
|
||||
},
|
||||
now: () => NOW,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
service.publish({
|
||||
...publishRequest(),
|
||||
principal: principal('publisher', 'single_factor'),
|
||||
}),
|
||||
ModelPriceCatalogManagementAuthenticationError,
|
||||
);
|
||||
assert.equal(authorizations, 0);
|
||||
await assert.rejects(
|
||||
service.publish(publishRequest()),
|
||||
ModelPriceCatalogManagementAuthorizationError,
|
||||
);
|
||||
assert.equal(authorizations, 1);
|
||||
assert.equal(
|
||||
repository.calls.some(([name]) => name === 'publishAuthorized'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('cluster activation requires a different strong publishing User', async () => {
|
||||
const sameUserRepository = fakeRepository('publisher');
|
||||
const sameUserService = createModelPriceCatalogManagementService(
|
||||
sameUserRepository,
|
||||
{
|
||||
decisionMode: 'separation_of_duty',
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return policy();
|
||||
},
|
||||
},
|
||||
now: () => NOW,
|
||||
},
|
||||
);
|
||||
await assert.rejects(
|
||||
sameUserService.transition(transitionRequest('publisher')),
|
||||
ModelPriceCatalogManagementSeparationOfDutyError,
|
||||
);
|
||||
assert.equal(
|
||||
sameUserRepository.calls.some(([name]) => name === 'transitionAuthorized'),
|
||||
false,
|
||||
);
|
||||
|
||||
const reviewedRepository = fakeRepository('publisher');
|
||||
const reviewedService = createModelPriceCatalogManagementService(
|
||||
reviewedRepository,
|
||||
{
|
||||
decisionMode: 'separation_of_duty',
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return policy();
|
||||
},
|
||||
},
|
||||
now: () => NOW,
|
||||
},
|
||||
);
|
||||
const result = await reviewedService.transition(
|
||||
transitionRequest('reviewer'),
|
||||
);
|
||||
assert.equal(result.head.activePriceRevision, 'price-1');
|
||||
assert.equal(result.authorization.principal.subject.id, 'reviewer');
|
||||
assert.equal(result.authorization.decisionMode, 'separation_of_duty');
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidModelProviderCredentialAdministrationMutationError,
|
||||
modelProviderCredentialAdministrationOperationId,
|
||||
normalizeAuthorizedModelProviderCredentialTransitionMutation,
|
||||
} = require('../dist/model-provider-credential/modelProviderCredentialAdministration.js');
|
||||
const {
|
||||
MODEL_PROVIDER_CREDENTIAL_TRANSITION_COMMAND_SCHEMA,
|
||||
createModelProviderCredentialTransitionCommand,
|
||||
} = require('../dist/model-provider-credential/modelProviderCredentialCatalog.js');
|
||||
const {
|
||||
MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
} = require('../dist/model-provider-credential/providerCredential.js');
|
||||
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||
|
||||
const MUTATION_ID = '019f7094-a853-4f3b-82ab-dfa08e6bd1c1';
|
||||
|
||||
function command(overrides = {}) {
|
||||
return createModelProviderCredentialTransitionCommand({
|
||||
schema: MODEL_PROVIDER_CREDENTIAL_TRANSITION_COMMAND_SCHEMA,
|
||||
mutationId: MUTATION_ID,
|
||||
projectId: 'project-a',
|
||||
provider: 'openai-compatible',
|
||||
expectedGeneration: 0,
|
||||
action: 'bind',
|
||||
binding: {
|
||||
schema: MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
projectId: 'project-a',
|
||||
provider: 'openai-compatible',
|
||||
revision: 'credential-v1',
|
||||
secretRef: createSecretRef({
|
||||
projectId: 'project-a',
|
||||
name: 'openai-token',
|
||||
}),
|
||||
scheme: 'bearer',
|
||||
},
|
||||
changedBy: { type: 'user', id: 'owner-a' },
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function mutation(overrides = {}) {
|
||||
const { command: commandOverrides, ...mutationOverrides } = overrides;
|
||||
const catalogCommand = command(commandOverrides);
|
||||
const fence = { projectVersion: 3, bindingVersion: 7 };
|
||||
return {
|
||||
command: catalogCommand,
|
||||
actor: { type: 'user', id: 'owner-a' },
|
||||
fence,
|
||||
audit: {
|
||||
eventId: catalogCommand.mutationId,
|
||||
requestId: 'request-1',
|
||||
operationId: modelProviderCredentialAdministrationOperationId(
|
||||
catalogCommand.action,
|
||||
),
|
||||
projectId: catalogCommand.projectId,
|
||||
subject: { type: 'user', id: 'owner-a' },
|
||||
authenticationId: 'authentication-1',
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_owner'],
|
||||
fence,
|
||||
occurredAtMs: 100,
|
||||
},
|
||||
...mutationOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('bind administration binds actor, Project fence and allowed audit', () => {
|
||||
const normalized =
|
||||
normalizeAuthorizedModelProviderCredentialTransitionMutation(mutation());
|
||||
assert.equal(normalized.command.action, 'bind');
|
||||
assert.equal(normalized.audit.operationId, 'model_provider_credential.bind');
|
||||
assert.deepEqual(normalized.actor, { type: 'user', id: 'owner-a' });
|
||||
assert.deepEqual(normalized.fence, {
|
||||
projectVersion: 3,
|
||||
bindingVersion: 7,
|
||||
});
|
||||
});
|
||||
|
||||
test('revoke administration uses the exact revoke audit operation', () => {
|
||||
const normalized =
|
||||
normalizeAuthorizedModelProviderCredentialTransitionMutation(
|
||||
mutation({
|
||||
command: {
|
||||
action: 'revoke',
|
||||
binding: null,
|
||||
expectedGeneration: 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.equal(normalized.command.action, 'revoke');
|
||||
assert.equal(
|
||||
normalized.audit.operationId,
|
||||
'model_provider_credential.revoke',
|
||||
);
|
||||
});
|
||||
|
||||
test('administration rejects actor, Project, audit and fence drift', () => {
|
||||
const candidate = mutation();
|
||||
for (const drift of [
|
||||
{ actor: { type: 'user', id: 'owner-b' } },
|
||||
{ audit: { ...candidate.audit, projectId: 'project-b' } },
|
||||
{ audit: { ...candidate.audit, outcome: 'denied' } },
|
||||
{
|
||||
audit: {
|
||||
...candidate.audit,
|
||||
operationId: 'model_provider_credential.revoke',
|
||||
},
|
||||
},
|
||||
{ fence: { projectVersion: 3, bindingVersion: 8 } },
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeAuthorizedModelProviderCredentialTransitionMutation({
|
||||
...candidate,
|
||||
...drift,
|
||||
}),
|
||||
InvalidModelProviderCredentialAdministrationMutationError,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
MODEL_PROVIDER_CREDENTIAL_TRANSITION_COMMAND_SCHEMA,
|
||||
InvalidModelProviderCredentialTransitionError,
|
||||
ModelProviderCredentialTransitionConflictError,
|
||||
createModelProviderCredentialTransition,
|
||||
createModelProviderCredentialTransitionCommand,
|
||||
modelProviderCredentialBindingForTransition,
|
||||
normalizeModelProviderCredentialTransition,
|
||||
} = require('../dist/model-provider-credential/modelProviderCredentialCatalog.js');
|
||||
const {
|
||||
MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
} = require('../dist/model-provider-credential/providerCredential.js');
|
||||
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||
|
||||
function binding(overrides = {}) {
|
||||
return {
|
||||
schema: MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
projectId: 'project-a',
|
||||
provider: 'openai-compatible',
|
||||
revision: 'credential-v1',
|
||||
secretRef: createSecretRef({
|
||||
projectId: 'project-a',
|
||||
name: 'openai-token',
|
||||
}),
|
||||
scheme: 'bearer',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function command(overrides = {}) {
|
||||
return createModelProviderCredentialTransitionCommand({
|
||||
schema: MODEL_PROVIDER_CREDENTIAL_TRANSITION_COMMAND_SCHEMA,
|
||||
mutationId: 'credential-bind-1',
|
||||
projectId: 'project-a',
|
||||
provider: 'openai-compatible',
|
||||
expectedGeneration: 0,
|
||||
action: 'bind',
|
||||
binding: binding(),
|
||||
changedBy: { type: 'user', id: 'owner-a' },
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test('credential catalog creates an immutable bind/revoke hash chain', () => {
|
||||
const bind = command();
|
||||
const first = createModelProviderCredentialTransition(bind, null, 10);
|
||||
assert.equal(first.generation, 1);
|
||||
assert.equal(first.action, 'bind');
|
||||
assert.equal(first.previousTransitionDigest, null);
|
||||
assert.deepEqual(
|
||||
modelProviderCredentialBindingForTransition(first, bind.binding),
|
||||
bind.binding,
|
||||
);
|
||||
assert.deepEqual(normalizeModelProviderCredentialTransition(first), first);
|
||||
|
||||
const revoke = command({
|
||||
mutationId: 'credential-revoke-2',
|
||||
expectedGeneration: 1,
|
||||
action: 'revoke',
|
||||
binding: null,
|
||||
});
|
||||
const second = createModelProviderCredentialTransition(revoke, first, 20);
|
||||
assert.equal(second.generation, 2);
|
||||
assert.equal(second.action, 'revoke');
|
||||
assert.equal(second.previousTransitionDigest, first.transitionDigest);
|
||||
assert.equal(modelProviderCredentialBindingForTransition(second, null), null);
|
||||
});
|
||||
|
||||
test('credential catalog rejects cross-Project binding and stale CAS', () => {
|
||||
assert.throws(
|
||||
() => command({ binding: binding({ projectId: 'project-b' }) }),
|
||||
InvalidModelProviderCredentialTransitionError,
|
||||
);
|
||||
const firstCommand = command();
|
||||
const first = createModelProviderCredentialTransition(firstCommand, null, 1);
|
||||
assert.throws(
|
||||
() => createModelProviderCredentialTransition(command(), first, 2),
|
||||
ModelProviderCredentialTransitionConflictError,
|
||||
);
|
||||
});
|
||||
|
||||
test('credential transition tampering is detected before storage use', () => {
|
||||
const first = createModelProviderCredentialTransition(command(), null, 1);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelProviderCredentialTransition({
|
||||
...first,
|
||||
activeBindingRevision: 'credential-v2',
|
||||
}),
|
||||
InvalidModelProviderCredentialTransitionError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
modelProviderCredentialBindingForTransition(
|
||||
first,
|
||||
binding({ revision: 'credential-v2' }),
|
||||
),
|
||||
/unavailable/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { randomUUID } = require('node:crypto');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidModelProviderCredentialTestConnectionError,
|
||||
createModelProviderCredentialTestAllowlist,
|
||||
createModelProviderCredentialTestExecution,
|
||||
createModelProviderCredentialTestPlan,
|
||||
createModelProviderCredentialTestResult,
|
||||
normalizeModelProviderCredentialTestAllowlist,
|
||||
normalizeModelProviderCredentialTestExecution,
|
||||
normalizeModelProviderCredentialTestPlan,
|
||||
normalizeModelProviderCredentialTestResult,
|
||||
resolveModelProviderCredentialTestEndpoint,
|
||||
} = require('../dist/model-provider-credential/modelProviderCredentialTestConnection.js');
|
||||
|
||||
function endpoint(overrides = {}) {
|
||||
return {
|
||||
provider: 'openai-compatible',
|
||||
adapter: 'openai-compatible',
|
||||
baseUrl: 'https://provider.example.test/v1/',
|
||||
revision: 'provider-test-v1',
|
||||
deadlineMs: 5_000,
|
||||
maxResponseBytes: 64 * 1_024,
|
||||
maxModels: 64,
|
||||
maxCostMicrousd: 0,
|
||||
retryLimit: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function allowlist() {
|
||||
return createModelProviderCredentialTestAllowlist({
|
||||
revision: 'catalog-v1',
|
||||
providers: [endpoint()],
|
||||
});
|
||||
}
|
||||
|
||||
test('freezes an exact HTTPS allowlist with zero retry and zero cost', () => {
|
||||
const catalog = allowlist();
|
||||
assert.deepEqual(
|
||||
normalizeModelProviderCredentialTestAllowlist(catalog),
|
||||
catalog,
|
||||
);
|
||||
assert.equal(catalog.providers[0].retryLimit, 0);
|
||||
assert.equal(catalog.providers[0].maxCostMicrousd, 0);
|
||||
assert.equal(
|
||||
resolveModelProviderCredentialTestEndpoint(catalog, 'openai-compatible')
|
||||
.configDigest,
|
||||
catalog.providers[0].configDigest,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createModelProviderCredentialTestAllowlist({
|
||||
revision: 'catalog-v1',
|
||||
providers: [endpoint({ baseUrl: 'http://metadata.internal/' })],
|
||||
}),
|
||||
InvalidModelProviderCredentialTestConnectionError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createModelProviderCredentialTestAllowlist({
|
||||
revision: 'catalog-v1',
|
||||
providers: [endpoint({ retryLimit: 1 })],
|
||||
}),
|
||||
InvalidModelProviderCredentialTestConnectionError,
|
||||
);
|
||||
});
|
||||
|
||||
test('binds one short-lived plan to the server-selected endpoint and fence', () => {
|
||||
const selected = allowlist().providers[0];
|
||||
const plan = createModelProviderCredentialTestPlan({
|
||||
testId: randomUUID(),
|
||||
requestId: 'provider-test-request-1',
|
||||
projectId: 'project-a',
|
||||
provider: selected.provider,
|
||||
endpoint: selected,
|
||||
requestedBy: { type: 'user', id: 'owner-a' },
|
||||
fence: { projectVersion: 7, bindingVersion: 9 },
|
||||
plannedAtMs: 1_000,
|
||||
expiresAtMs: 61_000,
|
||||
});
|
||||
assert.deepEqual(normalizeModelProviderCredentialTestPlan(plan), plan);
|
||||
assert.equal(JSON.stringify(plan).includes('secretRef'), false);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelProviderCredentialTestPlan({
|
||||
...plan,
|
||||
endpoint: { ...plan.endpoint, maxCostMicrousd: 1 },
|
||||
}),
|
||||
InvalidModelProviderCredentialTestConnectionError,
|
||||
);
|
||||
});
|
||||
|
||||
test('creates immutable execution intent before a content-free result', () => {
|
||||
const testId = randomUUID();
|
||||
const executionId = randomUUID();
|
||||
const execution = createModelProviderCredentialTestExecution({
|
||||
executionId,
|
||||
testId,
|
||||
planDigest: 'a'.repeat(64),
|
||||
startedAtMs: 2_000,
|
||||
});
|
||||
assert.deepEqual(
|
||||
normalizeModelProviderCredentialTestExecution(execution),
|
||||
execution,
|
||||
);
|
||||
const result = createModelProviderCredentialTestResult({
|
||||
executionId,
|
||||
testId,
|
||||
planDigest: execution.planDigest,
|
||||
outcome: 'reachable',
|
||||
modelCount: 12,
|
||||
durationMs: 250,
|
||||
completedAtMs: 2_250,
|
||||
});
|
||||
assert.deepEqual(normalizeModelProviderCredentialTestResult(result), result);
|
||||
assert.deepEqual(Object.keys(result).sort(), [
|
||||
'completedAtMs',
|
||||
'durationMs',
|
||||
'executionId',
|
||||
'modelCount',
|
||||
'outcome',
|
||||
'planDigest',
|
||||
'resultDigest',
|
||||
'schema',
|
||||
'testId',
|
||||
]);
|
||||
assert.throws(
|
||||
() =>
|
||||
createModelProviderCredentialTestResult({
|
||||
executionId,
|
||||
testId,
|
||||
planDigest: execution.planDigest,
|
||||
outcome: 'unreachable',
|
||||
modelCount: 1,
|
||||
durationMs: 250,
|
||||
completedAtMs: 2_250,
|
||||
}),
|
||||
InvalidModelProviderCredentialTestConnectionError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
OpenAiCompatibleConfigurationError,
|
||||
OpenAiCompatibleProtocolError,
|
||||
OpenAiCompatibleProvider,
|
||||
} = require('../dist/model-gateway/openAiCompatibleProvider.js');
|
||||
|
||||
function context() {
|
||||
return {
|
||||
projectId: 'project-a',
|
||||
runId: 'run-a',
|
||||
stepRunId: 'step-a',
|
||||
traceId: 'trace-a',
|
||||
requestId: 'request-a',
|
||||
deadlineAtMs: Date.now() + 10_000,
|
||||
};
|
||||
}
|
||||
|
||||
function request() {
|
||||
return {
|
||||
provider: 'openai-compatible',
|
||||
model: 'model-a',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
maxOutputTokens: 32,
|
||||
};
|
||||
}
|
||||
|
||||
test('remote endpoints require HTTPS while explicit loopback HTTP remains possible', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'http://models.example.test/v1/',
|
||||
}),
|
||||
OpenAiCompatibleConfigurationError,
|
||||
);
|
||||
assert.doesNotThrow(
|
||||
() =>
|
||||
new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'http://127.0.0.1:8080/v1/',
|
||||
allowPlaintextLoopback: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('generate sends one bounded OpenAI-compatible request without retry', async () => {
|
||||
const calls = [];
|
||||
const credentialRequests = [];
|
||||
let credentialDisposed = 0;
|
||||
const instance = new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'https://models.example.test/v1/',
|
||||
credentials: {
|
||||
async authorizationHeader(request) {
|
||||
credentialRequests.push(request);
|
||||
return {
|
||||
value: 'Bearer ephemeral-token',
|
||||
dispose() {
|
||||
credentialDisposed += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
async fetch(url, init) {
|
||||
assert.equal(credentialDisposed, 0);
|
||||
calls.push({ url: String(url), init });
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
model: 'model-a',
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant', content: 'world' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 2,
|
||||
completion_tokens: 1,
|
||||
total_tokens: 3,
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await instance.generate(request(), context());
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(credentialDisposed, 1);
|
||||
assert.deepEqual(credentialRequests, [
|
||||
{
|
||||
operation: 'generate',
|
||||
provider: 'openai-compatible',
|
||||
projectId: 'project-a',
|
||||
requestId: 'request-a',
|
||||
},
|
||||
]);
|
||||
assert.equal(calls[0].url, 'https://models.example.test/v1/chat/completions');
|
||||
assert.equal(calls[0].init.headers.authorization, 'Bearer ephemeral-token');
|
||||
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
||||
model: 'model-a',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
max_tokens: 32,
|
||||
stream: false,
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
provider: 'openai-compatible',
|
||||
model: 'model-a',
|
||||
text: 'world',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 2, outputTokens: 1, totalTokens: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
test('credential leases are disposed on network failure and malformed leases never reach fetch', async () => {
|
||||
let disposed = 0;
|
||||
const failing = new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'https://models.example.test/v1/',
|
||||
credentials: {
|
||||
async authorizationHeader() {
|
||||
return {
|
||||
value: 'Bearer short-lived',
|
||||
dispose() {
|
||||
disposed += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
async fetch() {
|
||||
throw new Error('network unavailable');
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
failing.generate(request(), context()),
|
||||
/network unavailable/,
|
||||
);
|
||||
assert.equal(disposed, 1);
|
||||
|
||||
let calls = 0;
|
||||
const malformed = new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'https://models.example.test/v1/',
|
||||
credentials: {
|
||||
async authorizationHeader() {
|
||||
return {
|
||||
value: 'Bearer value',
|
||||
dispose() {
|
||||
disposed += 1;
|
||||
},
|
||||
retained: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
async fetch() {
|
||||
calls += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
malformed.generate(request(), context()),
|
||||
OpenAiCompatibleConfigurationError,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
assert.equal(disposed, 2);
|
||||
});
|
||||
|
||||
test('adapter rejects provider identity drift before network access', async () => {
|
||||
let calls = 0;
|
||||
const instance = new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'https://models.example.test/v1/',
|
||||
async fetch() {
|
||||
calls += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
instance.generate(
|
||||
{ ...request(), provider: 'another-provider' },
|
||||
context(),
|
||||
),
|
||||
/request provider does not match the adapter/,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
test('stream parses arbitrarily split CRLF SSE and preserves final usage', async () => {
|
||||
const encoder = new TextEncoder();
|
||||
const payload = [
|
||||
'data: {"choices":[{"delta":{"content":"hel"},"finish_reason":null}]}\r',
|
||||
'\n\r\ndata: {"choices":[{"delta":{"content":"lo"},"finish_reason":"stop"}]}\r\n\r\n',
|
||||
'data: {"choices":[],"usage":{"prompt_tokens":2,"completion_tokens":1,"total_tokens":3}}\r\n\r\n',
|
||||
'data: [DONE]\r\n\r\n',
|
||||
];
|
||||
const instance = new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'https://models.example.test/v1/',
|
||||
async fetch(_url, init) {
|
||||
assert.equal(JSON.parse(init.body).stream_options.include_usage, true);
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
for (const part of payload)
|
||||
controller.enqueue(encoder.encode(part));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/event-stream' },
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of instance.stream(request(), context())) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
assert.equal(chunks.map((chunk) => chunk.delta).join(''), 'hello');
|
||||
assert.equal(chunks[1].finishReason, 'stop');
|
||||
assert.deepEqual(chunks[2].usage, {
|
||||
inputTokens: 2,
|
||||
outputTokens: 1,
|
||||
totalTokens: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('protocol rejects missing usage and over-limit responses', async () => {
|
||||
const missingUsage = new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'https://models.example.test/v1/',
|
||||
async fetch() {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
model: 'model-a',
|
||||
choices: [
|
||||
{
|
||||
message: { content: 'world' },
|
||||
finish_reason: 'stop',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
missingUsage.generate(request(), context()),
|
||||
OpenAiCompatibleProtocolError,
|
||||
);
|
||||
|
||||
const tooLarge = new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'https://models.example.test/v1/',
|
||||
maxResponseBytes: 32,
|
||||
async fetch() {
|
||||
return new Response(JSON.stringify({ data: [{ id: 'x'.repeat(64) }] }));
|
||||
},
|
||||
});
|
||||
await assert.rejects(tooLarge.listModels(), OpenAiCompatibleProtocolError);
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
createPluginPackagePromptCatalogResult,
|
||||
PLUGIN_PACKAGE_PROMPT_CATALOG_SCHEMA,
|
||||
} = require('../dist/prompt/pluginPackagePromptCatalog.js');
|
||||
const {
|
||||
PostgresPluginPackagePromptCatalogService,
|
||||
} = require('../dist/prompt/postgresPluginPackagePromptApplication.js');
|
||||
const {
|
||||
createInitialPluginPackageAutomationPublication,
|
||||
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
|
||||
const {
|
||||
pluginPackageTaskReconciliationFixture,
|
||||
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
|
||||
|
||||
function publication() {
|
||||
const source = pluginPackageTaskReconciliationFixture('prompt-catalog', {
|
||||
profile: 'cluster-control',
|
||||
prompts: [
|
||||
{
|
||||
schema: 'qinglong/plugin-package-prompt-resource@v1',
|
||||
id: 'summary',
|
||||
name: 'Summary',
|
||||
description: 'Summarizes input',
|
||||
template: 'Private template {{subject}}.',
|
||||
parameters: [
|
||||
{
|
||||
name: 'subject',
|
||||
description: 'Text to summarize',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
return createInitialPluginPackageAutomationPublication(
|
||||
source.revision,
|
||||
source.registry,
|
||||
20_000,
|
||||
);
|
||||
}
|
||||
|
||||
test('catalog projection excludes Prompt template content', () => {
|
||||
const current = publication();
|
||||
const result = createPluginPackagePromptCatalogResult(
|
||||
current.target.projectId,
|
||||
current.target.packageName,
|
||||
current,
|
||||
);
|
||||
assert.equal(result.schema, PLUGIN_PACKAGE_PROMPT_CATALOG_SCHEMA);
|
||||
assert.deepEqual(result.prompts, [
|
||||
{
|
||||
id: 'summary',
|
||||
name: 'Summary',
|
||||
description: 'Summarizes input',
|
||||
parameters: [
|
||||
{
|
||||
name: 'subject',
|
||||
description: 'Text to summarize',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
assert.equal(JSON.stringify(result).includes('Private template'), false);
|
||||
});
|
||||
|
||||
test('PostgreSQL catalog reads one current publication and returns empty for absence', async () => {
|
||||
const current = publication();
|
||||
const queries = [];
|
||||
const service = new PostgresPluginPackagePromptCatalogService({
|
||||
async query(sql, parameters) {
|
||||
queries.push({ sql, parameters });
|
||||
return { rows: [{ publicationJson: current }] };
|
||||
},
|
||||
});
|
||||
const result = await service.inspect(
|
||||
current.target.projectId,
|
||||
current.target.packageName,
|
||||
);
|
||||
assert.equal(result.found, true);
|
||||
assert.equal(queries.length, 1);
|
||||
assert.match(queries[0].sql, /LIMIT 2/);
|
||||
|
||||
const absent = new PostgresPluginPackagePromptCatalogService({
|
||||
async query() {
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
await absent.inspect(current.target.projectId, current.target.packageName),
|
||||
{
|
||||
schema: PLUGIN_PACKAGE_PROMPT_CATALOG_SCHEMA,
|
||||
projectId: current.target.projectId,
|
||||
packageName: current.target.packageName,
|
||||
found: false,
|
||||
publicationState: null,
|
||||
prompts: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
CRASH_POINTS,
|
||||
setupScenario,
|
||||
verifyScenario,
|
||||
} = require('./fixtures/pluginPackagePromptCrashFixture.cjs');
|
||||
|
||||
const FIXTURE_PATH = path.join(
|
||||
__dirname,
|
||||
'fixtures',
|
||||
'pluginPackagePromptCrashFixture.cjs',
|
||||
);
|
||||
|
||||
test(
|
||||
'survives the SQLite Package Prompt admission and finalization crash matrix',
|
||||
{ timeout: 180_000 },
|
||||
async (context) => {
|
||||
const reports = [];
|
||||
for (const profile of ['edge', 'standalone']) {
|
||||
for (const [pointName, point] of Object.entries(CRASH_POINTS)) {
|
||||
const directory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), `ql3-package-prompt-${profile}-`),
|
||||
);
|
||||
context.after(() => {
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
const databasePath = path.join(directory, 'runtime.sqlite');
|
||||
const statePath = path.join(directory, 'state.json');
|
||||
const markerPath = path.join(directory, 'crash-marker.json');
|
||||
await setupScenario({
|
||||
databasePath,
|
||||
statePath,
|
||||
profile,
|
||||
operation: point.operation,
|
||||
});
|
||||
const crashed = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
FIXTURE_PATH,
|
||||
'crash',
|
||||
databasePath,
|
||||
statePath,
|
||||
markerPath,
|
||||
pointName,
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
timeout: 30_000,
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
crashed.error,
|
||||
undefined,
|
||||
`${profile}/${pointName}: ${crashed.error?.message}`,
|
||||
);
|
||||
assert.equal(
|
||||
crashed.signal,
|
||||
'SIGKILL',
|
||||
`${profile}/${pointName}: status=${crashed.status}, stderr=${crashed.stderr}`,
|
||||
);
|
||||
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
||||
assert.deepEqual(marker, {
|
||||
schema: 'qinglong/sqlite-plugin-package-prompt-crash-marker@v1',
|
||||
point: pointName,
|
||||
pid: marker.pid,
|
||||
});
|
||||
reports.push(
|
||||
await verifyScenario({ databasePath, statePath, pointName }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(reports.length, 20);
|
||||
assert.equal(
|
||||
reports.filter((report) => report.operation === 'admission').length,
|
||||
10,
|
||||
);
|
||||
assert.equal(
|
||||
reports.filter((report) => report.operation === 'finalization').length,
|
||||
10,
|
||||
);
|
||||
assert.equal(
|
||||
reports.filter((report) => report.crashBeforeCommit).length,
|
||||
16,
|
||||
);
|
||||
assert.equal(
|
||||
reports.filter((report) => report.durableAfterCrash).length,
|
||||
4,
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...new Set(reports.map((report) => report.journalMode))].sort(),
|
||||
['delete', 'wal'],
|
||||
);
|
||||
assert.ok(reports.every((report) => report.integrityCheck === 'ok'));
|
||||
assert.ok(reports.every((report) => report.foreignKeyCheck === 'ok'));
|
||||
assert.ok(reports.every((report) => report.exactReplay));
|
||||
assert.ok(reports.every((report) => report.contentFree));
|
||||
assert.ok(reports.every((report) => !report.physicalPowerLossProven));
|
||||
context.diagnostic(
|
||||
`QL3_RESOURCE_EVIDENCE=${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
workload: 'plugin_package_prompt_outer_transaction_crash_matrix',
|
||||
profiles: ['edge', 'standalone'],
|
||||
operations: ['admission', 'finalization'],
|
||||
crashPointsPerProfile: 10,
|
||||
scenarios: reports.length,
|
||||
crashBeforeCommit: 16,
|
||||
durableAfterCrash: 4,
|
||||
exactReplay: true,
|
||||
contentFree: true,
|
||||
integrityCheck: 'ok',
|
||||
foreignKeyCheck: 'ok',
|
||||
promptAdmissionFinalizationCrashProven: true,
|
||||
physicalPowerLossProven: false,
|
||||
})}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,223 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidPluginPackagePromptExecutionPlanError,
|
||||
PLUGIN_PACKAGE_PROMPT_EXECUTION_PLAN_SCHEMA,
|
||||
createPluginPackagePromptAdmissionBundle,
|
||||
normalizePluginPackagePromptExecutionPlan,
|
||||
pluginPackagePromptExecutionPlanDigest,
|
||||
preparePluginPackagePromptExecution,
|
||||
} = require('../dist/prompt/pluginPackagePromptExecution.js');
|
||||
const {
|
||||
pluginPackageAutomationPublicationDigest,
|
||||
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
|
||||
|
||||
function publication(overrides = {}) {
|
||||
const unsigned = {
|
||||
schema: 'qinglong/plugin-package-automation-publication@v1',
|
||||
target: {
|
||||
projectId: 'project-a',
|
||||
packageName: 'package-a',
|
||||
installationId: 'installation-a',
|
||||
lockDigest: '1'.repeat(64),
|
||||
generation: 3,
|
||||
generationDigest: '2'.repeat(64),
|
||||
materializedRevisionDigest: '3'.repeat(64),
|
||||
},
|
||||
state: 'active',
|
||||
version: 1,
|
||||
previousPublicationDigest: null,
|
||||
lifecycleEventDigest: null,
|
||||
definitions: {
|
||||
workflows: [],
|
||||
prompts: [
|
||||
{
|
||||
schema: 'qinglong/plugin-package-prompt-resource@v1',
|
||||
id: 'summary',
|
||||
name: 'Summary',
|
||||
template: 'Summarize {{subject}} for {{audience}}.',
|
||||
parameters: [
|
||||
{ name: 'audience', required: false },
|
||||
{ name: 'subject', required: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
publishedAtMs: 1_000,
|
||||
...overrides,
|
||||
};
|
||||
return {
|
||||
...unsigned,
|
||||
publicationDigest: pluginPackageAutomationPublicationDigest(unsigned),
|
||||
};
|
||||
}
|
||||
|
||||
function prepare(overrides = {}) {
|
||||
const active = publication();
|
||||
return preparePluginPackagePromptExecution({
|
||||
publication: active,
|
||||
expectedPublicationDigest: active.publicationDigest,
|
||||
promptId: 'summary',
|
||||
requestId: 'prompt-request-a',
|
||||
traceId: 'trace-a',
|
||||
requestedBySubject: { type: 'user', id: 'user-a' },
|
||||
policyFence: { projectVersion: 1, bindingVersion: 1 },
|
||||
parameters: { subject: 'QingLong 3.0' },
|
||||
provider: 'openai-compatible',
|
||||
model: 'model-a',
|
||||
maxOutputTokens: 512,
|
||||
temperature: 0.2,
|
||||
plannedAtMs: 2_000,
|
||||
deadlineAtMs: 62_000,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test('prepares a content-free immutable Prompt execution plan', () => {
|
||||
const first = prepare();
|
||||
const second = prepare();
|
||||
assert.deepEqual(first, second);
|
||||
assert.equal(first.plan.schema, PLUGIN_PACKAGE_PROMPT_EXECUTION_PLAN_SCHEMA);
|
||||
assert.deepEqual(first.plan.output, { mode: 'live_only' });
|
||||
assert.equal(
|
||||
first.request.messages[0].content,
|
||||
'Summarize QingLong 3.0 for .',
|
||||
);
|
||||
assert.match(first.plan.invocationId, /^ppi:[0-9a-f]{32}$/);
|
||||
assert.match(first.plan.runId, /^ppr:[0-9a-f]{32}$/);
|
||||
assert.match(first.plan.stepRunId, /^pps:[0-9a-f]{32}$/);
|
||||
assert.match(first.plan.modelRequestDigest, /^sha256:[0-9a-f]{64}$/);
|
||||
const durable = JSON.stringify(first.plan);
|
||||
assert.equal(durable.includes('QingLong 3.0'), false);
|
||||
assert.equal(durable.includes('Summarize '), false);
|
||||
assert.equal(Object.isFrozen(first.plan), true);
|
||||
});
|
||||
|
||||
test('binds explicit durable output retention and preserves legacy live-only replay', () => {
|
||||
const durable = prepare({
|
||||
output: {
|
||||
mode: 'durable_artifact',
|
||||
retentionPolicy: {
|
||||
revision: 'edge-output-v1',
|
||||
retentionMs: 86_400_000,
|
||||
},
|
||||
},
|
||||
}).plan;
|
||||
assert.equal(durable.output.mode, 'durable_artifact');
|
||||
assert.match(durable.output.retentionPolicyDigest, /^[0-9a-f]{64}$/);
|
||||
assert.notEqual(durable.planDigest, prepare().plan.planDigest);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizePluginPackagePromptExecutionPlan({
|
||||
...durable,
|
||||
output: {
|
||||
...durable.output,
|
||||
retentionPolicyDigest: 'f'.repeat(64),
|
||||
},
|
||||
}),
|
||||
InvalidPluginPackagePromptExecutionPlanError,
|
||||
);
|
||||
|
||||
const current = prepare().plan;
|
||||
const {
|
||||
output: _output,
|
||||
planDigest: _planDigest,
|
||||
...legacyUnsigned
|
||||
} = current;
|
||||
const legacy = {
|
||||
...legacyUnsigned,
|
||||
planDigest: pluginPackagePromptExecutionPlanDigest(legacyUnsigned),
|
||||
};
|
||||
assert.equal(
|
||||
normalizePluginPackagePromptExecutionPlan(legacy).output,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test('binds parameter presence and content without recursively rendering values', () => {
|
||||
const omitted = prepare();
|
||||
const empty = prepare({
|
||||
parameters: { subject: 'QingLong 3.0', audience: '' },
|
||||
});
|
||||
const literal = prepare({
|
||||
parameters: { subject: '{{audience}}', audience: 'operators' },
|
||||
});
|
||||
assert.notEqual(omitted.plan.parameterDigest, empty.plan.parameterDigest);
|
||||
assert.equal(
|
||||
literal.request.messages[0].content,
|
||||
'Summarize {{audience}} for operators.',
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects stale publication, missing or widened parameters and unsafe budgets', () => {
|
||||
const active = publication();
|
||||
assert.throws(
|
||||
() =>
|
||||
preparePluginPackagePromptExecution({
|
||||
...prepare().plan,
|
||||
publication: active,
|
||||
expectedPublicationDigest: 'f'.repeat(64),
|
||||
promptId: 'summary',
|
||||
requestId: 'request-b',
|
||||
traceId: 'trace-b',
|
||||
requestedBySubject: { type: 'user', id: 'user-b' },
|
||||
policyFence: { projectVersion: 1, bindingVersion: 1 },
|
||||
parameters: { subject: 'x' },
|
||||
provider: 'provider-a',
|
||||
model: 'model-a',
|
||||
maxOutputTokens: 1,
|
||||
plannedAtMs: 2_000,
|
||||
deadlineAtMs: 3_000,
|
||||
}),
|
||||
InvalidPluginPackagePromptExecutionPlanError,
|
||||
);
|
||||
assert.throws(() => prepare({ parameters: {} }), /subject is required/);
|
||||
assert.throws(
|
||||
() => prepare({ parameters: { subject: 'x', extra: 'y' } }),
|
||||
/undeclared name/,
|
||||
);
|
||||
assert.throws(
|
||||
() => prepare({ deadlineAtMs: 2_000 + 5 * 60_000 + 1 }),
|
||||
/deadline/,
|
||||
);
|
||||
assert.throws(() => prepare({ maxOutputTokens: 32_769 }), /maxOutputTokens/);
|
||||
});
|
||||
|
||||
test('creates one model StepRun admission without persisting Prompt content', () => {
|
||||
const prepared = prepare();
|
||||
const bundle = createPluginPackagePromptAdmissionBundle(prepared.plan);
|
||||
assert.equal(bundle.run.status, 'running');
|
||||
assert.equal(bundle.run.version, 2);
|
||||
assert.equal(bundle.run.eventSequence, 2);
|
||||
assert.equal(bundle.run.triggerType, 'plugin_package_prompt');
|
||||
assert.equal(bundle.stepMutation.stepRun.kind, 'model');
|
||||
assert.equal(bundle.stepMutation.stepRun.status, 'ready');
|
||||
assert.equal(bundle.stepMutation.expectedRunVersion, 1);
|
||||
assert.equal(bundle.stepMutation.event.sequence, 2);
|
||||
assert.equal(bundle.receipt.finalRunVersion, 2);
|
||||
assert.equal(bundle.receipt.invocationId, prepared.plan.invocationId);
|
||||
const durable = JSON.stringify(bundle);
|
||||
assert.equal(durable.includes('QingLong 3.0'), false);
|
||||
assert.equal(durable.includes('Summarize '), false);
|
||||
});
|
||||
|
||||
test('normalizer rejects identity and digest drift', () => {
|
||||
const plan = prepare().plan;
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizePluginPackagePromptExecutionPlan({
|
||||
...plan,
|
||||
runId: 'ppr:drift',
|
||||
}),
|
||||
InvalidPluginPackagePromptExecutionPlanError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizePluginPackagePromptExecutionPlan({
|
||||
...plan,
|
||||
parameterDigest: 'f'.repeat(64),
|
||||
}),
|
||||
InvalidPluginPackagePromptExecutionPlanError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_SCHEMA,
|
||||
normalizeAuthorizedPluginPackagePromptExecutionInspection,
|
||||
normalizePluginPackagePromptExecutionInspectionResult,
|
||||
} = require('../dist/prompt/pluginPackagePromptExecutionInspection.js');
|
||||
const {
|
||||
LocalPluginPackagePromptExecutionInspectionRepository,
|
||||
} = require('../dist/prompt/localPluginPackagePromptExecutionInspectionRepository.js');
|
||||
const {
|
||||
PostgresPluginPackagePromptExecutionInspectionRepository,
|
||||
} = require('../dist/prompt/postgresPluginPackagePromptExecutionInspectionRepository.js');
|
||||
|
||||
function authorized(eventId = '00000000-0000-4000-8000-000000000001') {
|
||||
return {
|
||||
projectId: 'project-1',
|
||||
packageName: 'example',
|
||||
promptId: 'summary',
|
||||
executionRequestId: 'execution-request-1',
|
||||
actor: { type: 'user', id: 'owner-1' },
|
||||
fence: { projectVersion: 3, bindingVersion: 7 },
|
||||
audit: {
|
||||
eventId,
|
||||
requestId: 'inspection-request-1',
|
||||
operationId: 'prompt.execution.read',
|
||||
projectId: 'project-1',
|
||||
subject: { type: 'user', id: 'owner-1' },
|
||||
authenticationId: 'api_credential:credential-1:1',
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_policy_allowed'],
|
||||
fence: { projectVersion: 3, bindingVersion: 7 },
|
||||
occurredAtMs: 2_000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function terminalResult() {
|
||||
return {
|
||||
schema: PLUGIN_PACKAGE_PROMPT_EXECUTION_INSPECTION_SCHEMA,
|
||||
found: true,
|
||||
projectId: 'project-1',
|
||||
packageName: 'example',
|
||||
promptId: 'summary',
|
||||
executionRequestId: 'execution-request-1',
|
||||
execution: {
|
||||
invocationId: 'invocation-1',
|
||||
runId: '00000000-0000-4000-8000-000000000010',
|
||||
stepRunId: 'step-1',
|
||||
runStatus: 'succeeded',
|
||||
runVersion: 5,
|
||||
eventSequence: 5,
|
||||
stepStatus: 'succeeded',
|
||||
stepVersion: 3,
|
||||
admittedAtMs: 1_000,
|
||||
startedAtMs: 1_000,
|
||||
finishedAtMs: 1_500,
|
||||
finalizedAtMs: 1_500,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('normalizes an exact content-free execution inspection contract', () => {
|
||||
const command = normalizeAuthorizedPluginPackagePromptExecutionInspection(
|
||||
authorized(),
|
||||
);
|
||||
const result = normalizePluginPackagePromptExecutionInspectionResult(
|
||||
terminalResult(),
|
||||
);
|
||||
assert.equal(command.executionRequestId, 'execution-request-1');
|
||||
assert.equal(result.execution.runStatus, 'succeeded');
|
||||
assert.equal(JSON.stringify(result).includes('template'), false);
|
||||
assert.throws(() =>
|
||||
normalizePluginPackagePromptExecutionInspectionResult({
|
||||
...terminalResult(),
|
||||
execution: { ...terminalResult().execution, privateOutput: 'secret' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('SQLite inspection commits authorization, exact read and audit replay atomically', async (t) => {
|
||||
const database = new DatabaseSync(':memory:');
|
||||
t.after(() => database.close());
|
||||
database.exec(`
|
||||
CREATE TABLE "QingLong3SecurityAuditEvents" (event_id TEXT PRIMARY KEY);
|
||||
CREATE TABLE "ModelInvocationPromptAdmissions" (
|
||||
request_id TEXT PRIMARY KEY, invocation_id TEXT, run_id TEXT,
|
||||
step_run_id TEXT, project_id TEXT, package_name TEXT, prompt_id TEXT,
|
||||
admitted_at_ms INTEGER
|
||||
);
|
||||
CREATE TABLE "Runs" (
|
||||
id TEXT PRIMARY KEY, project_id TEXT, status TEXT, version INTEGER,
|
||||
event_sequence INTEGER, started_at_ms INTEGER, finished_at_ms INTEGER
|
||||
);
|
||||
CREATE TABLE "StepRuns" (
|
||||
id TEXT, run_id TEXT, status TEXT, version INTEGER,
|
||||
PRIMARY KEY (run_id, id)
|
||||
);
|
||||
CREATE TABLE "ModelInvocationPromptFinalizations" (
|
||||
request_id TEXT PRIMARY KEY, finalized_at_ms INTEGER
|
||||
);
|
||||
`);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "ModelInvocationPromptAdmissions" VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
'execution-request-1',
|
||||
'invocation-1',
|
||||
'00000000-0000-4000-8000-000000000010',
|
||||
'step-1',
|
||||
'project-1',
|
||||
'example',
|
||||
'summary',
|
||||
1_000,
|
||||
);
|
||||
database
|
||||
.prepare(`INSERT INTO "Runs" VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(
|
||||
'00000000-0000-4000-8000-000000000010',
|
||||
'project-1',
|
||||
'succeeded',
|
||||
5,
|
||||
5,
|
||||
1_000,
|
||||
1_500,
|
||||
);
|
||||
database
|
||||
.prepare(`INSERT INTO "StepRuns" VALUES (?, ?, ?, ?)`)
|
||||
.run('step-1', '00000000-0000-4000-8000-000000000010', 'succeeded', 3);
|
||||
database
|
||||
.prepare(`INSERT INTO "ModelInvocationPromptFinalizations" VALUES (?, ?)`)
|
||||
.run('execution-request-1', 1_500);
|
||||
const replays = [];
|
||||
const repository = new LocalPluginPackagePromptExecutionInspectionRepository(
|
||||
{
|
||||
client: database,
|
||||
async enqueue(work) {
|
||||
return work();
|
||||
},
|
||||
},
|
||||
{
|
||||
confirm(inspection, replay) {
|
||||
assert.equal(database.isTransaction, true);
|
||||
replays.push(replay);
|
||||
if (!replay) {
|
||||
database
|
||||
.prepare(`INSERT INTO "QingLong3SecurityAuditEvents" VALUES (?)`)
|
||||
.run(inspection.audit.eventId);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
await repository.inspectAuthorized(authorized()),
|
||||
terminalResult(),
|
||||
);
|
||||
assert.deepEqual(
|
||||
await repository.inspectAuthorized(authorized()),
|
||||
terminalResult(),
|
||||
);
|
||||
assert.deepEqual(replays, [false, true]);
|
||||
});
|
||||
|
||||
test('PostgreSQL inspection uses one serializable authorization snapshot', async () => {
|
||||
const queries = [];
|
||||
let released = 0;
|
||||
const client = {
|
||||
async query(sql, parameters = []) {
|
||||
queries.push({ sql, parameters });
|
||||
if (sql.includes('FROM "ql3"."api_credentials"')) {
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
version: 1,
|
||||
state: 'active',
|
||||
subjectType: 'user',
|
||||
subjectId: 'owner-1',
|
||||
notBeforeAtMs: 0,
|
||||
expiresAtMs: 10_000,
|
||||
subjectStatus: 'active',
|
||||
nowMs: 2_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (sql.includes('FROM "ql3"."projects"')) {
|
||||
return { rows: [{ status: 'active', version: 3 }] };
|
||||
}
|
||||
if (sql.includes('FROM "ql3"."project_role_bindings"')) {
|
||||
return { rows: [{ state: 'active', version: 7 }] };
|
||||
}
|
||||
if (sql.includes('model_invocation_prompt_admissions" AS admission')) {
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
invocationId: 'invocation-1',
|
||||
runId: '00000000-0000-4000-8000-000000000010',
|
||||
stepRunId: 'step-1',
|
||||
admittedAtMs: '1000',
|
||||
runStatus: 'succeeded',
|
||||
runVersion: 5,
|
||||
eventSequence: 5,
|
||||
startedAtMs: 1000,
|
||||
finishedAtMs: 1500,
|
||||
stepStatus: 'succeeded',
|
||||
stepVersion: 3,
|
||||
finalizedAtMs: 1500,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
release() {
|
||||
released += 1;
|
||||
},
|
||||
};
|
||||
const repository =
|
||||
new PostgresPluginPackagePromptExecutionInspectionRepository({
|
||||
async query() {
|
||||
return { rows: [] };
|
||||
},
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
await repository.inspectAuthorized(authorized()),
|
||||
terminalResult(),
|
||||
);
|
||||
assert.equal(released, 1);
|
||||
assert.match(queries[0].sql, /BEGIN ISOLATION LEVEL SERIALIZABLE READ WRITE/);
|
||||
const target = queries.find(({ sql }) =>
|
||||
sql.includes('model_invocation_prompt_admissions" AS admission'),
|
||||
);
|
||||
assert.deepEqual(target.parameters, [
|
||||
'execution-request-1',
|
||||
'project-1',
|
||||
'example',
|
||||
'summary',
|
||||
]);
|
||||
assert.match(target.sql, /LIMIT 2/);
|
||||
assert.equal(
|
||||
queries.some(({ sql }) => sql.includes('security_audit_events')),
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidPluginPackagePromptExecutionOutputReadError,
|
||||
PLUGIN_PACKAGE_PROMPT_EXECUTION_OUTPUT_READ_RESULT_SCHEMA,
|
||||
PluginPackagePromptExecutionOutputReadService,
|
||||
PluginPackagePromptExecutionOutputReadUnavailableError,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptExecutionOutputRead.js');
|
||||
const {
|
||||
LocalPluginPackagePromptExecutionOutputReferenceRepository,
|
||||
} = require('../dist/prompt-output/storage/localPluginPackagePromptExecutionOutputReferenceRepository.js');
|
||||
const {
|
||||
PostgresPluginPackagePromptExecutionOutputReferenceRepository,
|
||||
} = require('../dist/prompt-output/storage/postgresPluginPackagePromptExecutionOutputReferenceRepository.js');
|
||||
const {
|
||||
pluginPackagePromptOutputArtifactIdentity,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputArtifact.js');
|
||||
|
||||
const PRINCIPAL = Object.freeze({
|
||||
subject: Object.freeze({ type: 'user', id: 'owner-1' }),
|
||||
authenticationId: 'authentication-1',
|
||||
authenticatedAtMs: 1_000,
|
||||
expiresAtMs: 10_000,
|
||||
assurance: 'multi_factor',
|
||||
});
|
||||
const TARGET = Object.freeze({
|
||||
projectId: 'project-1',
|
||||
packageName: 'example',
|
||||
promptId: 'summary',
|
||||
executionRequestId: 'execution-request-1',
|
||||
});
|
||||
const REFERENCE = Object.freeze({
|
||||
runId: '00000000-0000-4000-8000-000000000010',
|
||||
artifactId: pluginPackagePromptOutputArtifactIdentity('invocation-1'),
|
||||
artifactDigest: 'a'.repeat(64),
|
||||
});
|
||||
const OUTPUT_REFERENCE = Object.freeze({
|
||||
schema: 'qinglong/plugin-package-prompt-output-artifact-reference@v1',
|
||||
artifactId: REFERENCE.artifactId,
|
||||
projectId: TARGET.projectId,
|
||||
runId: REFERENCE.runId,
|
||||
stepRunId: 'step-1',
|
||||
invocationId: 'invocation-1',
|
||||
contentDigest: 'b'.repeat(64),
|
||||
outputBytes: 14,
|
||||
retentionPolicyDigest: 'c'.repeat(64),
|
||||
retentionEligibleAtMs: 20_000,
|
||||
keyId: 'key-1',
|
||||
algorithm: 'aes-256-gcm',
|
||||
artifactDigest: REFERENCE.artifactDigest,
|
||||
});
|
||||
const RESULT = Object.freeze({
|
||||
provider: 'provider-1',
|
||||
model: 'model-1',
|
||||
text: 'private output',
|
||||
finishReason: 'stop',
|
||||
usage: Object.freeze({ inputTokens: 2, outputTokens: 3, totalTokens: 5 }),
|
||||
});
|
||||
|
||||
test('resolves one execution request before delegating protected output read', async () => {
|
||||
const calls = [];
|
||||
const service = new PluginPackagePromptExecutionOutputReadService({
|
||||
references: {
|
||||
async find(target) {
|
||||
calls.push({ kind: 'reference', target });
|
||||
return REFERENCE;
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
async read(command) {
|
||||
calls.push({ kind: 'output', command });
|
||||
return {
|
||||
schema: 'qinglong/plugin-package-prompt-output-read-result@v1',
|
||||
status: 'available',
|
||||
reference: OUTPUT_REFERENCE,
|
||||
result: RESULT,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const value = await service.read({ principal: PRINCIPAL, ...TARGET });
|
||||
assert.deepEqual(value, {
|
||||
schema: PLUGIN_PACKAGE_PROMPT_EXECUTION_OUTPUT_READ_RESULT_SCHEMA,
|
||||
status: 'available',
|
||||
...TARGET,
|
||||
reference: OUTPUT_REFERENCE,
|
||||
result: RESULT,
|
||||
});
|
||||
assert.deepEqual(calls[0], { kind: 'reference', target: TARGET });
|
||||
assert.deepEqual(calls[1], {
|
||||
kind: 'output',
|
||||
command: {
|
||||
principal: PRINCIPAL,
|
||||
projectId: TARGET.projectId,
|
||||
...REFERENCE,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('masks absent, live-only, tombstoned and cross-target output as not found', async () => {
|
||||
let outputReads = 0;
|
||||
const absent = new PluginPackagePromptExecutionOutputReadService({
|
||||
references: {
|
||||
async find() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
async read() {
|
||||
outputReads += 1;
|
||||
throw new Error('unreachable');
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await absent.read({ principal: PRINCIPAL, ...TARGET }), {
|
||||
schema: PLUGIN_PACKAGE_PROMPT_EXECUTION_OUTPUT_READ_RESULT_SCHEMA,
|
||||
status: 'not_found',
|
||||
...TARGET,
|
||||
});
|
||||
assert.equal(outputReads, 0);
|
||||
|
||||
const unavailableOutput = new PluginPackagePromptExecutionOutputReadService({
|
||||
references: {
|
||||
async find() {
|
||||
return REFERENCE;
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
async read() {
|
||||
return {
|
||||
schema: 'qinglong/plugin-package-prompt-output-read-result@v1',
|
||||
status: 'not_found',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
(await unavailableOutput.read({ principal: PRINCIPAL, ...TARGET })).status,
|
||||
'not_found',
|
||||
);
|
||||
await assert.rejects(
|
||||
unavailableOutput.read({
|
||||
principal: PRINCIPAL,
|
||||
...TARGET,
|
||||
packageName: '../invalid',
|
||||
}),
|
||||
InvalidPluginPackagePromptExecutionOutputReadError,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects widened output envelopes before returning content', async () => {
|
||||
const widened = new PluginPackagePromptExecutionOutputReadService({
|
||||
references: {
|
||||
async find() {
|
||||
return REFERENCE;
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
async read() {
|
||||
return {
|
||||
schema: 'qinglong/plugin-package-prompt-output-read-result@v1',
|
||||
status: 'available',
|
||||
reference: OUTPUT_REFERENCE,
|
||||
result: { ...RESULT, privateTrace: 'must-not-escape' },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
widened.read({ principal: PRINCIPAL, ...TARGET }),
|
||||
PluginPackagePromptExecutionOutputReadUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
function sqliteRepository(database) {
|
||||
return new LocalPluginPackagePromptExecutionOutputReferenceRepository({
|
||||
client: database,
|
||||
async enqueue(work) {
|
||||
return work();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test('SQLite locator requires an exact terminal durable binding', async (t) => {
|
||||
const database = new DatabaseSync(':memory:');
|
||||
t.after(() => database.close());
|
||||
database.exec(`
|
||||
CREATE TABLE "ModelInvocationPromptAdmissions" (
|
||||
request_id TEXT PRIMARY KEY, invocation_id TEXT, run_id TEXT,
|
||||
step_run_id TEXT, project_id TEXT, package_name TEXT, prompt_id TEXT
|
||||
);
|
||||
CREATE TABLE "ModelInvocationPromptFinalizations" (
|
||||
request_id TEXT PRIMARY KEY, run_status TEXT
|
||||
);
|
||||
CREATE TABLE "ModelInvocationCompletions" (
|
||||
invocation_id TEXT PRIMARY KEY, outcome TEXT
|
||||
);
|
||||
CREATE TABLE "Runs" (
|
||||
id TEXT PRIMARY KEY, project_id TEXT, status TEXT
|
||||
);
|
||||
CREATE TABLE "StepRuns" (
|
||||
id TEXT, run_id TEXT, status TEXT, output_ref TEXT,
|
||||
PRIMARY KEY (run_id, id)
|
||||
);
|
||||
CREATE TABLE "ModelInvocationPromptOutputArtifacts" (
|
||||
artifact_id TEXT PRIMARY KEY, project_id TEXT, run_id TEXT,
|
||||
step_run_id TEXT, invocation_id TEXT, artifact_digest TEXT
|
||||
);
|
||||
`);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "ModelInvocationPromptAdmissions" VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
TARGET.executionRequestId,
|
||||
'invocation-1',
|
||||
REFERENCE.runId,
|
||||
'step-1',
|
||||
TARGET.projectId,
|
||||
TARGET.packageName,
|
||||
TARGET.promptId,
|
||||
);
|
||||
database
|
||||
.prepare(`INSERT INTO "ModelInvocationPromptFinalizations" VALUES (?, ?)`)
|
||||
.run(TARGET.executionRequestId, 'succeeded');
|
||||
database
|
||||
.prepare(`INSERT INTO "ModelInvocationCompletions" VALUES (?, ?)`)
|
||||
.run('invocation-1', 'succeeded');
|
||||
database
|
||||
.prepare(`INSERT INTO "Runs" VALUES (?, ?, ?)`)
|
||||
.run(REFERENCE.runId, TARGET.projectId, 'succeeded');
|
||||
database
|
||||
.prepare(`INSERT INTO "StepRuns" VALUES (?, ?, ?, ?)`)
|
||||
.run('step-1', REFERENCE.runId, 'succeeded', REFERENCE.artifactId);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "ModelInvocationPromptOutputArtifacts" VALUES
|
||||
(?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
REFERENCE.artifactId,
|
||||
TARGET.projectId,
|
||||
REFERENCE.runId,
|
||||
'step-1',
|
||||
'invocation-1',
|
||||
REFERENCE.artifactDigest,
|
||||
);
|
||||
|
||||
const repository = sqliteRepository(database);
|
||||
assert.deepEqual(await repository.find(TARGET), REFERENCE);
|
||||
assert.equal(
|
||||
await repository.find({ ...TARGET, promptId: 'another-prompt' }),
|
||||
null,
|
||||
);
|
||||
database.prepare(`UPDATE "StepRuns" SET output_ref = NULL`).run();
|
||||
assert.equal(await repository.find(TARGET), null);
|
||||
});
|
||||
|
||||
test('PostgreSQL locator uses the existing request primary key and terminal joins', async () => {
|
||||
const calls = [];
|
||||
const repository =
|
||||
new PostgresPluginPackagePromptExecutionOutputReferenceRepository({
|
||||
async query(sql, parameters) {
|
||||
calls.push({ sql, parameters });
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
runId: REFERENCE.runId,
|
||||
artifactId: REFERENCE.artifactId,
|
||||
artifactDigest: REFERENCE.artifactDigest,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await repository.find(TARGET), REFERENCE);
|
||||
assert.deepEqual(calls[0].parameters, [
|
||||
TARGET.executionRequestId,
|
||||
TARGET.projectId,
|
||||
TARGET.packageName,
|
||||
TARGET.promptId,
|
||||
]);
|
||||
assert.match(calls[0].sql, /admission\.request_id = \$1/);
|
||||
assert.match(calls[0].sql, /step\.output_ref = artifact\.artifact_id/);
|
||||
assert.match(calls[0].sql, /LIMIT 2/);
|
||||
assert.equal(calls[0].sql.includes('artifact_json'), false);
|
||||
assert.equal(calls[0].sql.includes('ciphertext'), false);
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidPluginPackagePromptOutputArtifactError,
|
||||
PluginPackagePromptOutputArtifactUnavailableError,
|
||||
createPluginPackagePromptOutputArtifact,
|
||||
normalizePluginPackagePromptOutputArtifact,
|
||||
normalizePluginPackagePromptOutputArtifactReference,
|
||||
openPluginPackagePromptOutputArtifact,
|
||||
pluginPackagePromptOutputArtifactIdentity,
|
||||
pluginPackagePromptOutputArtifactReference,
|
||||
pluginPackagePromptOutputArtifactRetentionPolicyDigest,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputArtifact.js');
|
||||
|
||||
const KEY = Buffer.alloc(32, 7);
|
||||
const NONCE = Buffer.alloc(12, 9);
|
||||
const RETENTION = Object.freeze({
|
||||
revision: 'edge-default-v1',
|
||||
retentionMs: 86_400_000,
|
||||
});
|
||||
const RESULT = Object.freeze({
|
||||
provider: 'openai-compatible',
|
||||
model: 'bounded-model',
|
||||
text: 'private durable answer: ql3-artifact-secret',
|
||||
finishReason: 'stop',
|
||||
usage: Object.freeze({
|
||||
inputTokens: 12,
|
||||
outputTokens: 7,
|
||||
totalTokens: 19,
|
||||
costMicros: 42,
|
||||
}),
|
||||
});
|
||||
|
||||
function create(overrides = {}) {
|
||||
return createPluginPackagePromptOutputArtifact(
|
||||
{
|
||||
projectId: 'project-a',
|
||||
runId: 'run-a',
|
||||
stepRunId: 'step-a',
|
||||
invocationId: 'invocation-a',
|
||||
requestedBy: { type: 'user', id: 'user-a' },
|
||||
result: RESULT,
|
||||
retentionPolicy: RETENTION,
|
||||
keyId: 'prompt-key-1',
|
||||
key: Buffer.from(KEY),
|
||||
sealedAtMs: 1_700_000_000_000,
|
||||
...overrides,
|
||||
},
|
||||
() => Buffer.from(NONCE),
|
||||
);
|
||||
}
|
||||
|
||||
test('Prompt output Artifact encrypts bounded result and opens exact content', () => {
|
||||
const artifact = create();
|
||||
const serialized = JSON.stringify(artifact);
|
||||
|
||||
assert.equal(
|
||||
artifact.artifactId,
|
||||
pluginPackagePromptOutputArtifactIdentity('invocation-a'),
|
||||
);
|
||||
assert.equal(
|
||||
artifact.retentionPolicyDigest,
|
||||
pluginPackagePromptOutputArtifactRetentionPolicyDigest(RETENTION),
|
||||
);
|
||||
assert.equal(artifact.retentionEligibleAtMs, 1_700_086_400_000);
|
||||
assert.equal(artifact.outputBytes, Buffer.byteLength(RESULT.text, 'utf8'));
|
||||
assert.equal(serialized.includes(RESULT.text), false);
|
||||
assert.equal(serialized.includes('ql3-artifact-secret'), false);
|
||||
assert.deepEqual(
|
||||
openPluginPackagePromptOutputArtifact(artifact, Buffer.from(KEY)),
|
||||
RESULT,
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizePluginPackagePromptOutputArtifact(artifact),
|
||||
artifact,
|
||||
);
|
||||
});
|
||||
|
||||
test('Prompt output Artifact reference is content-free and identity-bound', () => {
|
||||
const artifact = create();
|
||||
const reference = pluginPackagePromptOutputArtifactReference(artifact);
|
||||
|
||||
assert.deepEqual(
|
||||
normalizePluginPackagePromptOutputArtifactReference(reference),
|
||||
reference,
|
||||
);
|
||||
assert.equal(JSON.stringify(reference).includes(RESULT.text), false);
|
||||
assert.equal(reference.artifactDigest, artifact.artifactDigest);
|
||||
assert.equal(reference.contentDigest, artifact.contentDigest);
|
||||
assert.equal(reference.keyId, artifact.keyId);
|
||||
assert.equal(reference.retentionPolicyDigest, artifact.retentionPolicyDigest);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizePluginPackagePromptOutputArtifactReference({
|
||||
...reference,
|
||||
invocationId: 'invocation-b',
|
||||
}),
|
||||
InvalidPluginPackagePromptOutputArtifactError,
|
||||
);
|
||||
});
|
||||
|
||||
test('Prompt output Artifact rejects metadata and ciphertext tampering', () => {
|
||||
const artifact = create();
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizePluginPackagePromptOutputArtifact({
|
||||
...artifact,
|
||||
projectId: 'project-b',
|
||||
}),
|
||||
InvalidPluginPackagePromptOutputArtifactError,
|
||||
);
|
||||
|
||||
const ciphertext = Buffer.from(artifact.ciphertext, 'base64url');
|
||||
ciphertext[0] ^= 1;
|
||||
const tampered = {
|
||||
...artifact,
|
||||
ciphertext: ciphertext.toString('base64url'),
|
||||
};
|
||||
assert.throws(
|
||||
() => normalizePluginPackagePromptOutputArtifact(tampered),
|
||||
InvalidPluginPackagePromptOutputArtifactError,
|
||||
);
|
||||
|
||||
const wrongKeyArtifact = create();
|
||||
assert.throws(
|
||||
() =>
|
||||
openPluginPackagePromptOutputArtifact(
|
||||
wrongKeyArtifact,
|
||||
Buffer.alloc(32, 8),
|
||||
),
|
||||
PluginPackagePromptOutputArtifactUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('Prompt output Artifact rejects unsafe retention and key material', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
create({
|
||||
retentionPolicy: { revision: 'bad', retentionMs: 1 },
|
||||
}),
|
||||
InvalidPluginPackagePromptOutputArtifactError,
|
||||
);
|
||||
assert.throws(
|
||||
() => create({ key: Buffer.alloc(31) }),
|
||||
PluginPackagePromptOutputArtifactUnavailableError,
|
||||
);
|
||||
assert.throws(
|
||||
() => create({ sealedAtMs: Number.MAX_SAFE_INTEGER }),
|
||||
InvalidPluginPackagePromptOutputArtifactError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash, generateKeyPairSync, sign } = require('node:crypto');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
||||
createPluginPackagePromptOutputExternalCustodyReceipt,
|
||||
normalizePluginPackagePromptOutputExternalCustodyReceipt,
|
||||
verifyPluginPackagePromptOutputExternalCustodyReceipt,
|
||||
verifyPluginPackagePromptOutputRecoveredMaterial,
|
||||
verifyPluginPackagePromptOutputWrappedBackup,
|
||||
} = require('../dist/prompt-output/custody/pluginPackagePromptOutputExternalCustody.js');
|
||||
const {
|
||||
createPluginPackagePromptOutputArtifact,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputArtifact.js');
|
||||
const {
|
||||
pluginPackagePromptOutputKeyRotationMaterialProof,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyRotation.js');
|
||||
|
||||
const MATERIAL = Buffer.alloc(32, 0x5a);
|
||||
const WRAPPED = Buffer.from('kms-wrapped-prompt-output-key-material');
|
||||
const CATALOG_DIGEST = '1'.repeat(64);
|
||||
const WRAPPING_KEY_REF_DIGEST = '2'.repeat(64);
|
||||
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
||||
|
||||
function receipt(overrides = {}) {
|
||||
return createPluginPackagePromptOutputExternalCustodyReceipt(
|
||||
{
|
||||
custodyId: 'custody-001',
|
||||
keyId: 'prompt-key-001',
|
||||
materialProof: pluginPackagePromptOutputKeyRotationMaterialProof(
|
||||
'prompt-key-001',
|
||||
MATERIAL,
|
||||
),
|
||||
sourceGeneration: 4,
|
||||
sourceCatalogDigest: CATALOG_DIGEST,
|
||||
wrappingProvider: 'vault-transit',
|
||||
wrappingKeyRefDigest: WRAPPING_KEY_REF_DIGEST,
|
||||
wrappedMaterialDigest: createHash('sha256').update(WRAPPED).digest('hex'),
|
||||
wrappedMaterialBytes: WRAPPED.length,
|
||||
createdAtMs: 1_700_000_000_000,
|
||||
...overrides,
|
||||
},
|
||||
{
|
||||
publicKey,
|
||||
sign: (digest) => sign(null, digest, privateKey),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function artifact(overrides = {}) {
|
||||
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: 'bounded-model',
|
||||
text: 'private recovered answer',
|
||||
finishReason: 'stop',
|
||||
usage: {
|
||||
inputTokens: 5,
|
||||
outputTokens: 3,
|
||||
totalTokens: 8,
|
||||
costMicros: 11,
|
||||
},
|
||||
},
|
||||
retentionPolicy: {
|
||||
revision: 'retention-v1',
|
||||
retentionMs: 86_400_000,
|
||||
},
|
||||
keyId: 'prompt-key-001',
|
||||
key: Buffer.from(MATERIAL),
|
||||
sealedAtMs: 1_700_000_000_000,
|
||||
...overrides,
|
||||
},
|
||||
() => Buffer.alloc(12, 0x33),
|
||||
);
|
||||
}
|
||||
|
||||
test('binds a signed content-free custody receipt to exact wrapped bytes', () => {
|
||||
const value = receipt();
|
||||
assert.deepEqual(
|
||||
normalizePluginPackagePromptOutputExternalCustodyReceipt(value),
|
||||
value,
|
||||
);
|
||||
assert.deepEqual(
|
||||
verifyPluginPackagePromptOutputExternalCustodyReceipt(value, publicKey),
|
||||
value,
|
||||
);
|
||||
const verified = verifyPluginPackagePromptOutputWrappedBackup(
|
||||
value,
|
||||
publicKey,
|
||||
WRAPPED,
|
||||
);
|
||||
assert.equal(verified.custodyId, 'custody-001');
|
||||
assert.equal(verified.wrappedMaterialBytes, WRAPPED.length);
|
||||
const serialized = JSON.stringify(value);
|
||||
assert.equal(serialized.includes(MATERIAL.toString('base64url')), false);
|
||||
assert.equal(serialized.includes(WRAPPED.toString('base64url')), false);
|
||||
assert.equal(serialized.includes('kms-wrapped-prompt'), false);
|
||||
});
|
||||
|
||||
test('verifies recovered material against durable fact and opens an Artifact', () => {
|
||||
const custody = receipt();
|
||||
const encrypted = artifact();
|
||||
const proof = verifyPluginPackagePromptOutputRecoveredMaterial({
|
||||
recoveryId: 'recovery-001',
|
||||
requestId: 'request-001',
|
||||
receipt: custody,
|
||||
trustedPublicKey: publicKey,
|
||||
durableKeyFact: {
|
||||
keyId: custody.keyId,
|
||||
materialProof: custody.materialProof,
|
||||
catalogDigest: custody.sourceCatalogDigest,
|
||||
},
|
||||
material: MATERIAL,
|
||||
artifact: encrypted,
|
||||
verifiedAtMs: 1_700_000_001_000,
|
||||
});
|
||||
assert.equal(proof.artifactId, encrypted.artifactId);
|
||||
assert.equal(proof.artifactDigest, encrypted.artifactDigest);
|
||||
assert.equal(proof.contentDigest, encrypted.contentDigest);
|
||||
assert.equal(proof.keyId, custody.keyId);
|
||||
assert.equal(proof.proofDigest.length, 64);
|
||||
const serialized = JSON.stringify(proof);
|
||||
assert.equal(serialized.includes('private recovered answer'), false);
|
||||
assert.equal(serialized.includes(MATERIAL.toString('base64url')), false);
|
||||
});
|
||||
|
||||
test('rejects untrusted receipt, wrapped bytes, durable fact and material drift', () => {
|
||||
const custody = receipt();
|
||||
const otherKeys = generateKeyPairSync('ed25519');
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyPluginPackagePromptOutputExternalCustodyReceipt(
|
||||
custody,
|
||||
otherKeys.publicKey,
|
||||
),
|
||||
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyPluginPackagePromptOutputWrappedBackup(
|
||||
custody,
|
||||
publicKey,
|
||||
Buffer.from('different wrapped material'),
|
||||
),
|
||||
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
||||
);
|
||||
for (const candidate of [
|
||||
{
|
||||
durableKeyFact: {
|
||||
keyId: custody.keyId,
|
||||
materialProof: '3'.repeat(64),
|
||||
catalogDigest: custody.sourceCatalogDigest,
|
||||
},
|
||||
material: MATERIAL,
|
||||
},
|
||||
{
|
||||
durableKeyFact: {
|
||||
keyId: custody.keyId,
|
||||
materialProof: custody.materialProof,
|
||||
catalogDigest: custody.sourceCatalogDigest,
|
||||
},
|
||||
material: Buffer.alloc(32, 0x5b),
|
||||
},
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyPluginPackagePromptOutputRecoveredMaterial({
|
||||
recoveryId: 'recovery-001',
|
||||
requestId: 'request-001',
|
||||
receipt: custody,
|
||||
trustedPublicKey: publicKey,
|
||||
durableKeyFact: candidate.durableKeyFact,
|
||||
material: candidate.material,
|
||||
artifact: artifact(),
|
||||
verifiedAtMs: 1_700_000_001_000,
|
||||
}),
|
||||
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects a custody receipt with a tampered signature', () => {
|
||||
const custody = receipt();
|
||||
const signature = Buffer.from(custody.signature, 'base64url');
|
||||
signature[0] ^= 0x01;
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyPluginPackagePromptOutputExternalCustodyReceipt(
|
||||
{
|
||||
...custody,
|
||||
signature: signature.toString('base64url'),
|
||||
},
|
||||
publicKey,
|
||||
),
|
||||
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash, generateKeyPairSync, sign } = require('node:crypto');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
PLUGIN_PACKAGE_PROMPT_OUTPUT_EXTERNAL_CUSTODY_BUNDLE_SCHEMA,
|
||||
createPluginPackagePromptOutputExternalCustodyBundle,
|
||||
openPluginPackagePromptOutputExternalCustodyBundle,
|
||||
} = require('../dist/prompt-output/custody/pluginPackagePromptOutputExternalCustodyBundle.js');
|
||||
const {
|
||||
InvalidPluginPackagePromptOutputExternalCustodyError,
|
||||
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
||||
createPluginPackagePromptOutputExternalCustodyReceipt,
|
||||
} = require('../dist/prompt-output/custody/pluginPackagePromptOutputExternalCustody.js');
|
||||
|
||||
function fixture(byte = 0x51) {
|
||||
const keys = generateKeyPairSync('ed25519');
|
||||
const wrappedMaterial = Buffer.from(`external-provider-wrapped-${byte}`);
|
||||
const receipt = createPluginPackagePromptOutputExternalCustodyReceipt(
|
||||
{
|
||||
custodyId: 'provider-neutral-custody-001',
|
||||
keyId: 'prompt-output-key-001',
|
||||
materialProof: '1'.repeat(64),
|
||||
sourceGeneration: 3,
|
||||
sourceCatalogDigest: '2'.repeat(64),
|
||||
wrappingProvider: 'external-kms',
|
||||
wrappingKeyRefDigest: '3'.repeat(64),
|
||||
wrappedMaterialDigest: createHash('sha256')
|
||||
.update(wrappedMaterial)
|
||||
.digest('hex'),
|
||||
wrappedMaterialBytes: wrappedMaterial.byteLength,
|
||||
createdAtMs: 1_700_000_000_000,
|
||||
},
|
||||
{
|
||||
publicKey: keys.publicKey,
|
||||
sign: (message) => sign(null, message, keys.privateKey),
|
||||
},
|
||||
);
|
||||
const bundle = createPluginPackagePromptOutputExternalCustodyBundle(
|
||||
receipt,
|
||||
keys.publicKey,
|
||||
wrappedMaterial,
|
||||
);
|
||||
return { keys, wrappedMaterial, receipt, bundle };
|
||||
}
|
||||
|
||||
test('creates and opens one provider-neutral atomic custody bundle', () => {
|
||||
const value = fixture();
|
||||
const opened = openPluginPackagePromptOutputExternalCustodyBundle(
|
||||
value.bundle,
|
||||
value.keys.publicKey,
|
||||
);
|
||||
try {
|
||||
assert.equal(
|
||||
value.bundle.schema,
|
||||
PLUGIN_PACKAGE_PROMPT_OUTPUT_EXTERNAL_CUSTODY_BUNDLE_SCHEMA,
|
||||
);
|
||||
assert.equal(opened.bundleDigest, value.bundle.bundleDigest);
|
||||
assert.equal(opened.receipt.receiptDigest, value.receipt.receiptDigest);
|
||||
assert.deepEqual(opened.wrappedMaterial, value.wrappedMaterial);
|
||||
} finally {
|
||||
opened.wrappedMaterial.fill(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects split-file substitution even when the receipt remains valid', () => {
|
||||
const left = fixture(0x51);
|
||||
const right = fixture(0x52);
|
||||
assert.throws(
|
||||
() =>
|
||||
openPluginPackagePromptOutputExternalCustodyBundle(
|
||||
{ ...left.bundle, wrappedMaterial: right.bundle.wrappedMaterial },
|
||||
left.keys.publicKey,
|
||||
),
|
||||
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects bundle digest drift and extra fields', () => {
|
||||
const value = fixture();
|
||||
assert.throws(
|
||||
() =>
|
||||
openPluginPackagePromptOutputExternalCustodyBundle(
|
||||
{ ...value.bundle, bundleDigest: '4'.repeat(64) },
|
||||
value.keys.publicKey,
|
||||
),
|
||||
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
openPluginPackagePromptOutputExternalCustodyBundle(
|
||||
{ ...value.bundle, providerConfig: 'must-not-be-embedded' },
|
||||
value.keys.publicKey,
|
||||
),
|
||||
InvalidPluginPackagePromptOutputExternalCustodyError,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a valid bundle under a different custody signing authority', () => {
|
||||
const value = fixture();
|
||||
const other = generateKeyPairSync('ed25519');
|
||||
assert.throws(
|
||||
() =>
|
||||
openPluginPackagePromptOutputExternalCustodyBundle(
|
||||
value.bundle,
|
||||
other.publicKey,
|
||||
),
|
||||
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,302 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash, generateKeyPairSync, sign } = require('node:crypto');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
||||
createPluginPackagePromptOutputExternalRecoveryAuthorization,
|
||||
normalizePluginPackagePromptOutputExternalRecoveryAuthorization,
|
||||
verifyAuthorizedPluginPackagePromptOutputRecoveredMaterial,
|
||||
verifyPluginPackagePromptOutputExternalRecoveryAuthorization,
|
||||
} = require('../dist/prompt-output/custody/pluginPackagePromptOutputExternalRecoveryAuthorization.js');
|
||||
const {
|
||||
createPluginPackagePromptOutputExternalCustodyReceipt,
|
||||
} = require('../dist/prompt-output/custody/pluginPackagePromptOutputExternalCustody.js');
|
||||
const {
|
||||
createPluginPackagePromptOutputArtifact,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputArtifact.js');
|
||||
const {
|
||||
pluginPackagePromptOutputKeyRotationMaterialProof,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyRotation.js');
|
||||
|
||||
const NOW = 1_700_000_000_000;
|
||||
const MATERIAL = Buffer.alloc(32, 0x4a);
|
||||
const WRAPPED = Buffer.from('external-kms-wrapped-material');
|
||||
const CATALOG_DIGEST = '1'.repeat(64);
|
||||
const POLICY_DIGEST = '2'.repeat(64);
|
||||
const WRAPPING_KEY_REF_DIGEST = '3'.repeat(64);
|
||||
const custodyKeys = generateKeyPairSync('ed25519');
|
||||
const approverAKeys = generateKeyPairSync('ed25519');
|
||||
const approverBKeys = generateKeyPairSync('ed25519');
|
||||
|
||||
function artifact() {
|
||||
return createPluginPackagePromptOutputArtifact(
|
||||
{
|
||||
projectId: 'project-recovery',
|
||||
runId: 'run-recovery',
|
||||
stepRunId: 'step-recovery',
|
||||
invocationId: 'invocation-recovery',
|
||||
requestedBy: { type: 'user', id: 'requester-user' },
|
||||
result: {
|
||||
provider: 'openai-compatible',
|
||||
model: 'bounded-model',
|
||||
text: 'historical private answer',
|
||||
finishReason: 'stop',
|
||||
usage: {
|
||||
inputTokens: 5,
|
||||
outputTokens: 4,
|
||||
totalTokens: 9,
|
||||
costMicros: 12,
|
||||
},
|
||||
},
|
||||
retentionPolicy: {
|
||||
revision: 'recovery-retention-v1',
|
||||
retentionMs: 86_400_000,
|
||||
},
|
||||
keyId: 'prompt-key-recovery',
|
||||
key: Buffer.from(MATERIAL),
|
||||
sealedAtMs: NOW - 60_000,
|
||||
},
|
||||
() => Buffer.alloc(12, 0x31),
|
||||
);
|
||||
}
|
||||
|
||||
function receipt() {
|
||||
return createPluginPackagePromptOutputExternalCustodyReceipt(
|
||||
{
|
||||
custodyId: 'custody-recovery',
|
||||
keyId: 'prompt-key-recovery',
|
||||
materialProof: pluginPackagePromptOutputKeyRotationMaterialProof(
|
||||
'prompt-key-recovery',
|
||||
MATERIAL,
|
||||
),
|
||||
sourceGeneration: 7,
|
||||
sourceCatalogDigest: CATALOG_DIGEST,
|
||||
wrappingProvider: 'external-kms',
|
||||
wrappingKeyRefDigest: WRAPPING_KEY_REF_DIGEST,
|
||||
wrappedMaterialDigest: createHash('sha256').update(WRAPPED).digest('hex'),
|
||||
wrappedMaterialBytes: WRAPPED.length,
|
||||
createdAtMs: NOW - 120_000,
|
||||
},
|
||||
{
|
||||
publicKey: custodyKeys.publicKey,
|
||||
sign: (message) => sign(null, message, custodyKeys.privateKey),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function signer(userId, authenticationId, keys, approvedAtMs) {
|
||||
return {
|
||||
userId,
|
||||
authenticationId,
|
||||
authenticatedAtMs: approvedAtMs - 1_000,
|
||||
approvedAtMs,
|
||||
publicKey: keys.publicKey,
|
||||
sign: (message) => sign(null, message, keys.privateKey),
|
||||
};
|
||||
}
|
||||
|
||||
function authorization(encrypted, custody, overrides = {}) {
|
||||
return createPluginPackagePromptOutputExternalRecoveryAuthorization(
|
||||
{
|
||||
recoveryId: 'recovery-001',
|
||||
requestId: 'request-001',
|
||||
custodyId: custody.custodyId,
|
||||
custodyReceiptDigest: custody.receiptDigest,
|
||||
keyId: custody.keyId,
|
||||
artifactId: encrypted.artifactId,
|
||||
artifactDigest: encrypted.artifactDigest,
|
||||
policyDigest: POLICY_DIGEST,
|
||||
requestedBy: {
|
||||
userId: 'requester-user',
|
||||
authenticationId: 'requester-auth-001',
|
||||
authenticatedAtMs: NOW - 5_000,
|
||||
},
|
||||
requestedAtMs: NOW,
|
||||
expiresAtMs: NOW + 10 * 60_000,
|
||||
...overrides,
|
||||
},
|
||||
[
|
||||
signer('reviewer-b', 'reviewer-b-auth', approverBKeys, NOW + 2_000),
|
||||
signer('reviewer-a', 'reviewer-a-auth', approverAKeys, NOW + 1_000),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
function trustedApprovers() {
|
||||
return [
|
||||
{ userId: 'reviewer-a', publicKey: approverAKeys.publicKey },
|
||||
{ userId: 'reviewer-b', publicKey: approverBKeys.publicKey },
|
||||
];
|
||||
}
|
||||
|
||||
test('binds two distinct strong approvers to one exact recovery request', () => {
|
||||
const encrypted = artifact();
|
||||
const custody = receipt();
|
||||
const value = authorization(encrypted, custody);
|
||||
assert.deepEqual(
|
||||
normalizePluginPackagePromptOutputExternalRecoveryAuthorization(value),
|
||||
value,
|
||||
);
|
||||
assert.deepEqual(
|
||||
verifyPluginPackagePromptOutputExternalRecoveryAuthorization(
|
||||
value,
|
||||
trustedApprovers(),
|
||||
NOW + 3_000,
|
||||
),
|
||||
value,
|
||||
);
|
||||
assert.deepEqual(
|
||||
value.approvals.map(({ userId }) => userId),
|
||||
['reviewer-a', 'reviewer-b'],
|
||||
);
|
||||
assert.equal(value.permission, 'artifact.read');
|
||||
assert.equal(value.purpose, 'lost-key-recovery-verification');
|
||||
});
|
||||
|
||||
test('returns only an authorization-bound content-free recovery proof', () => {
|
||||
const encrypted = artifact();
|
||||
const custody = receipt();
|
||||
const proof = verifyAuthorizedPluginPackagePromptOutputRecoveredMaterial({
|
||||
authorization: authorization(encrypted, custody),
|
||||
trustedApprovers: trustedApprovers(),
|
||||
receipt: custody,
|
||||
trustedCustodyPublicKey: custodyKeys.publicKey,
|
||||
wrappedMaterial: WRAPPED,
|
||||
durableKeyFact: {
|
||||
keyId: custody.keyId,
|
||||
materialProof: custody.materialProof,
|
||||
catalogDigest: custody.sourceCatalogDigest,
|
||||
},
|
||||
material: MATERIAL,
|
||||
artifact: encrypted,
|
||||
verifiedAtMs: NOW + 3_000,
|
||||
});
|
||||
assert.equal(proof.artifactId, encrypted.artifactId);
|
||||
assert.equal(proof.authorizationDigest.length, 64);
|
||||
assert.equal(proof.recoveryProofDigest.length, 64);
|
||||
assert.equal(proof.proofDigest.length, 64);
|
||||
const serialized = JSON.stringify(proof);
|
||||
assert.equal(serialized.includes('historical private answer'), false);
|
||||
assert.equal(serialized.includes(MATERIAL.toString('base64url')), false);
|
||||
assert.equal(serialized.includes(WRAPPED.toString('base64url')), false);
|
||||
});
|
||||
|
||||
test('rejects requester self-approval, duplicate identity and stale authentication', () => {
|
||||
const encrypted = artifact();
|
||||
const custody = receipt();
|
||||
for (const signers of [
|
||||
[
|
||||
signer(
|
||||
'requester-user',
|
||||
'requester-review-auth',
|
||||
approverAKeys,
|
||||
NOW + 1_000,
|
||||
),
|
||||
signer('reviewer-b', 'reviewer-b-auth', approverBKeys, NOW + 2_000),
|
||||
],
|
||||
[
|
||||
signer('reviewer-a', 'same-auth', approverAKeys, NOW + 1_000),
|
||||
signer('reviewer-b', 'same-auth', approverBKeys, NOW + 2_000),
|
||||
],
|
||||
[
|
||||
{
|
||||
...signer('reviewer-a', 'reviewer-a-auth', approverAKeys, NOW + 1_000),
|
||||
authenticatedAtMs: NOW - 10 * 60_000,
|
||||
},
|
||||
signer('reviewer-b', 'reviewer-b-auth', approverBKeys, NOW + 2_000),
|
||||
],
|
||||
]) {
|
||||
assert.throws(() =>
|
||||
createPluginPackagePromptOutputExternalRecoveryAuthorization(
|
||||
{
|
||||
recoveryId: 'recovery-001',
|
||||
requestId: 'request-001',
|
||||
custodyId: custody.custodyId,
|
||||
custodyReceiptDigest: custody.receiptDigest,
|
||||
keyId: custody.keyId,
|
||||
artifactId: encrypted.artifactId,
|
||||
artifactDigest: encrypted.artifactDigest,
|
||||
policyDigest: POLICY_DIGEST,
|
||||
requestedBy: {
|
||||
userId: 'requester-user',
|
||||
authenticationId: 'requester-auth-001',
|
||||
authenticatedAtMs: NOW - 5_000,
|
||||
},
|
||||
requestedAtMs: NOW,
|
||||
expiresAtMs: NOW + 10 * 60_000,
|
||||
},
|
||||
signers,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects signature, trusted approver, expiry and exact fact drift', () => {
|
||||
const encrypted = artifact();
|
||||
const custody = receipt();
|
||||
const approved = authorization(encrypted, custody);
|
||||
const signature = Buffer.from(approved.approvals[0].signature, 'base64url');
|
||||
signature[0] ^= 1;
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyPluginPackagePromptOutputExternalRecoveryAuthorization(
|
||||
{
|
||||
...approved,
|
||||
approvals: [
|
||||
{
|
||||
...approved.approvals[0],
|
||||
signature: signature.toString('base64url'),
|
||||
},
|
||||
approved.approvals[1],
|
||||
],
|
||||
},
|
||||
trustedApprovers(),
|
||||
NOW + 3_000,
|
||||
),
|
||||
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyPluginPackagePromptOutputExternalRecoveryAuthorization(
|
||||
approved,
|
||||
[
|
||||
trustedApprovers()[0],
|
||||
{
|
||||
userId: 'reviewer-b',
|
||||
publicKey: generateKeyPairSync('ed25519').publicKey,
|
||||
},
|
||||
],
|
||||
NOW + 3_000,
|
||||
),
|
||||
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyPluginPackagePromptOutputExternalRecoveryAuthorization(
|
||||
approved,
|
||||
trustedApprovers(),
|
||||
NOW + 11 * 60_000,
|
||||
),
|
||||
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyAuthorizedPluginPackagePromptOutputRecoveredMaterial({
|
||||
authorization: approved,
|
||||
trustedApprovers: trustedApprovers(),
|
||||
receipt: custody,
|
||||
trustedCustodyPublicKey: custodyKeys.publicKey,
|
||||
wrappedMaterial: WRAPPED,
|
||||
durableKeyFact: {
|
||||
keyId: custody.keyId,
|
||||
materialProof: custody.materialProof,
|
||||
catalogDigest: custody.sourceCatalogDigest,
|
||||
},
|
||||
material: MATERIAL,
|
||||
artifact: { ...encrypted, artifactDigest: '4'.repeat(64) },
|
||||
verifiedAtMs: NOW + 3_000,
|
||||
}),
|
||||
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
PluginPackagePromptOutputFileKeyring,
|
||||
provisionPluginPackagePromptOutputFileKeyring,
|
||||
rotatePluginPackagePromptOutputFileKeyring,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputFileKeyring.js');
|
||||
const {
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
PluginPackagePromptOutputKeyRetirementUnavailableError,
|
||||
createPluginPackagePromptOutputKeyRetirementPreparation,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyRetirement.js');
|
||||
|
||||
function fixture(t) {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-output-keys-'));
|
||||
fs.chmodSync(directory, 0o700);
|
||||
const filePath = path.join(directory, 'prompt-output-keyring.json');
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
return { directory, filePath };
|
||||
}
|
||||
|
||||
function preparation(material, overrides = {}) {
|
||||
return createPluginPackagePromptOutputKeyRetirementPreparation({
|
||||
keyId: material.keyId,
|
||||
retirementId: 'retirement-a',
|
||||
requestId: 'request-a',
|
||||
mutationId: 'mutation-a',
|
||||
catalogDigest: material.catalogDigest,
|
||||
materialProof: material.materialProof,
|
||||
preparedAtMs: 10_000,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test('file keyring rotates then retires one inactive key with exact recovery', async (t) => {
|
||||
const { filePath } = fixture(t);
|
||||
const initial = await provisionPluginPackagePromptOutputFileKeyring(filePath);
|
||||
assert.equal(initial.generation, 1);
|
||||
assert.equal(initial.keyIds.length, 1);
|
||||
assert.deepEqual(initial.retiredKeyIds, []);
|
||||
assert.equal(fs.statSync(filePath).mode & 0o777, 0o600);
|
||||
|
||||
const keyring = new PluginPackagePromptOutputFileKeyring(filePath);
|
||||
const firstMaterial = await keyring.active();
|
||||
assert.equal(firstMaterial.keyId, initial.activeKeyId);
|
||||
assert.equal(firstMaterial.key.length, 32);
|
||||
firstMaterial.key.fill(0);
|
||||
|
||||
const rotated = await rotatePluginPackagePromptOutputFileKeyring({
|
||||
filePath,
|
||||
expectedActiveKeyId: initial.activeKeyId,
|
||||
expectedCatalogDigest: initial.catalogDigest,
|
||||
});
|
||||
assert.equal(rotated.generation, 2);
|
||||
assert.notEqual(rotated.activeKeyId, initial.activeKeyId);
|
||||
assert.deepEqual(
|
||||
[...rotated.keyIds].sort(),
|
||||
[initial.activeKeyId, rotated.activeKeyId].sort(),
|
||||
);
|
||||
const inactive = await keyring.inspect(initial.activeKeyId);
|
||||
assert.equal(inactive.state, 'inactive');
|
||||
const prepared = preparation(inactive);
|
||||
const retired = await keyring.retire({ preparation: prepared });
|
||||
assert.equal(retired.state, 'absent');
|
||||
assert.deepEqual(await keyring.inspect(initial.activeKeyId), retired);
|
||||
assert.deepEqual(await keyring.retire({ preparation: prepared }), retired);
|
||||
assert.equal(await keyring.resolve(initial.activeKeyId), null);
|
||||
const final = await keyring.summary();
|
||||
assert.equal(final.generation, 3);
|
||||
assert.deepEqual(final.keyIds, [rotated.activeKeyId]);
|
||||
assert.deepEqual(final.retiredKeyIds, [initial.activeKeyId]);
|
||||
const active = await keyring.active();
|
||||
assert.equal(active.keyId, rotated.activeKeyId);
|
||||
active.key.fill(0);
|
||||
});
|
||||
|
||||
test('file keyring rejects active, stale and corrupt retirement authority', async (t) => {
|
||||
const { directory, filePath } = fixture(t);
|
||||
const initial = await provisionPluginPackagePromptOutputFileKeyring(filePath);
|
||||
const keyring = new PluginPackagePromptOutputFileKeyring(filePath);
|
||||
const active = await keyring.inspect(initial.activeKeyId);
|
||||
assert.equal(active.state, 'active');
|
||||
await assert.rejects(
|
||||
keyring.retire({ preparation: preparation(active) }),
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
);
|
||||
|
||||
const rotated = await rotatePluginPackagePromptOutputFileKeyring({
|
||||
filePath,
|
||||
expectedActiveKeyId: initial.activeKeyId,
|
||||
expectedCatalogDigest: initial.catalogDigest,
|
||||
});
|
||||
const inactive = await keyring.inspect(initial.activeKeyId);
|
||||
await assert.rejects(
|
||||
keyring.retire({
|
||||
preparation: preparation(inactive, { catalogDigest: 'f'.repeat(64) }),
|
||||
}),
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
);
|
||||
|
||||
const lockPath = `${filePath}.lock`;
|
||||
fs.mkdirSync(lockPath, { mode: 0o700 });
|
||||
fs.writeFileSync(
|
||||
path.join(lockPath, 'owner.json'),
|
||||
`${JSON.stringify({ pid: 99_999_999, token: 'dead' })}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
const retired = await keyring.retire({ preparation: preparation(inactive) });
|
||||
assert.equal(retired.state, 'absent');
|
||||
assert.equal(fs.existsSync(lockPath), false);
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
manifest.retirements[initial.activeKeyId].absenceProof = '0'.repeat(64);
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
|
||||
await assert.rejects(
|
||||
keyring.inspect(initial.activeKeyId),
|
||||
PluginPackagePromptOutputKeyRetirementUnavailableError,
|
||||
);
|
||||
assert.equal(rotated.generation, 2);
|
||||
assert.equal(fs.statSync(directory).mode & 0o777, 0o700);
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidPluginPackagePromptOutputKeyRetirementError,
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
PluginPackagePromptOutputKeyRetirementCoordinator,
|
||||
PluginPackagePromptOutputKeyRetirementUnavailableError,
|
||||
createPluginPackagePromptOutputKeyRetirementCompletion,
|
||||
createPluginPackagePromptOutputKeyRetirementPreparation,
|
||||
normalizePluginPackagePromptOutputKeyRetirementCompletion,
|
||||
normalizePluginPackagePromptOutputKeyRetirementPreparation,
|
||||
pluginPackagePromptOutputKeyRetirementAbsenceProof,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyRetirement.js');
|
||||
|
||||
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
||||
|
||||
function request() {
|
||||
return {
|
||||
keyId: 'prompt-output-key-old',
|
||||
retirementId: 'retire-prompt-output-key-old',
|
||||
requestId: 'request-retire-prompt-output-key-old',
|
||||
mutationId: 'mutation-retire-prompt-output-key-old',
|
||||
};
|
||||
}
|
||||
|
||||
function harness(options = {}) {
|
||||
let record = null;
|
||||
let material = {
|
||||
state: options.materialState ?? 'inactive',
|
||||
keyId: request().keyId,
|
||||
catalogDigest: digest('catalog-before'),
|
||||
materialProof: digest('material-old'),
|
||||
};
|
||||
let retireCalls = 0;
|
||||
let failRetireResponse = options.failRetireResponse ?? false;
|
||||
let failCompleteResponse = options.failCompleteResponse ?? false;
|
||||
const repository = {
|
||||
async find() {
|
||||
return record;
|
||||
},
|
||||
async prepare(command) {
|
||||
if (options.liveArtifacts) {
|
||||
throw new PluginPackagePromptOutputKeyRetirementUnavailableError();
|
||||
}
|
||||
const preparation =
|
||||
createPluginPackagePromptOutputKeyRetirementPreparation({
|
||||
...command,
|
||||
preparedAtMs: 100,
|
||||
});
|
||||
if (record) {
|
||||
assert.deepEqual(record.preparation, preparation);
|
||||
return { status: 'existing', preparation: record.preparation };
|
||||
}
|
||||
record = { preparation, completion: null };
|
||||
return { status: 'created', preparation };
|
||||
},
|
||||
async complete(command) {
|
||||
const completion = createPluginPackagePromptOutputKeyRetirementCompletion(
|
||||
{
|
||||
...command,
|
||||
completedAtMs: 200,
|
||||
},
|
||||
);
|
||||
if (record.completion) {
|
||||
assert.deepEqual(record.completion, completion);
|
||||
return { status: 'existing', completion: record.completion };
|
||||
}
|
||||
record = { ...record, completion };
|
||||
if (failCompleteResponse) {
|
||||
failCompleteResponse = false;
|
||||
throw new Error('completion response lost');
|
||||
}
|
||||
return { status: 'created', completion };
|
||||
},
|
||||
};
|
||||
const materials = {
|
||||
async inspect() {
|
||||
return { ...material };
|
||||
},
|
||||
async retire(command) {
|
||||
retireCalls += 1;
|
||||
assert.equal(command.preparation.keyId, material.keyId);
|
||||
assert.equal(command.preparation.catalogDigest, material.catalogDigest);
|
||||
assert.equal(command.preparation.materialProof, material.materialProof);
|
||||
material = {
|
||||
state: 'absent',
|
||||
keyId: command.preparation.keyId,
|
||||
catalogDigest: digest('catalog-after'),
|
||||
absenceProof: pluginPackagePromptOutputKeyRetirementAbsenceProof(
|
||||
record.preparation,
|
||||
digest('catalog-after'),
|
||||
),
|
||||
};
|
||||
if (failRetireResponse) {
|
||||
failRetireResponse = false;
|
||||
throw new Error('retirement response lost');
|
||||
}
|
||||
return { ...material };
|
||||
},
|
||||
};
|
||||
return {
|
||||
coordinator: new PluginPackagePromptOutputKeyRetirementCoordinator({
|
||||
repository,
|
||||
materials,
|
||||
}),
|
||||
record: () => record,
|
||||
retireCalls: () => retireCalls,
|
||||
setMaterial(next) {
|
||||
material = next;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('normalizes exact content-free preparation and completion receipts', () => {
|
||||
const preparation = createPluginPackagePromptOutputKeyRetirementPreparation({
|
||||
...request(),
|
||||
catalogDigest: digest('catalog'),
|
||||
materialProof: digest('material'),
|
||||
preparedAtMs: 100,
|
||||
});
|
||||
const completion = createPluginPackagePromptOutputKeyRetirementCompletion({
|
||||
preparation,
|
||||
retiredCatalogDigest: digest('retired-catalog'),
|
||||
absenceProof: digest('absence'),
|
||||
completedAtMs: 200,
|
||||
});
|
||||
assert.deepEqual(
|
||||
normalizePluginPackagePromptOutputKeyRetirementPreparation(preparation),
|
||||
preparation,
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizePluginPackagePromptOutputKeyRetirementCompletion(completion),
|
||||
completion,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizePluginPackagePromptOutputKeyRetirementPreparation({
|
||||
...preparation,
|
||||
widened: true,
|
||||
}),
|
||||
InvalidPluginPackagePromptOutputKeyRetirementError,
|
||||
);
|
||||
});
|
||||
|
||||
test('prepares a durable fence before retiring inactive material', async () => {
|
||||
const value = harness();
|
||||
const retired = await value.coordinator.retire(request());
|
||||
assert.equal(retired.status, 'completed');
|
||||
assert.equal(value.retireCalls(), 1);
|
||||
assert.deepEqual(value.record(), {
|
||||
preparation: retired.preparation,
|
||||
completion: retired.completion,
|
||||
});
|
||||
assert.equal((await value.coordinator.retire(request())).status, 'existing');
|
||||
assert.equal(value.retireCalls(), 1);
|
||||
});
|
||||
|
||||
test('rejects active material and live Artifact coverage', async () => {
|
||||
const active = harness({ materialState: 'active' });
|
||||
await assert.rejects(
|
||||
active.coordinator.retire(request()),
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
);
|
||||
assert.equal(active.record(), null);
|
||||
const referenced = harness({ liveArtifacts: true });
|
||||
await assert.rejects(
|
||||
referenced.coordinator.retire(request()),
|
||||
PluginPackagePromptOutputKeyRetirementUnavailableError,
|
||||
);
|
||||
assert.equal(referenced.retireCalls(), 0);
|
||||
});
|
||||
|
||||
test('recovers when material retirement succeeded but its response was lost', async () => {
|
||||
const value = harness({ failRetireResponse: true });
|
||||
await assert.rejects(
|
||||
value.coordinator.retire(request()),
|
||||
PluginPackagePromptOutputKeyRetirementUnavailableError,
|
||||
);
|
||||
assert.equal(value.record().completion, null);
|
||||
const recovered = await value.coordinator.retire(request());
|
||||
assert.equal(recovered.status, 'completed');
|
||||
assert.equal(value.retireCalls(), 1);
|
||||
});
|
||||
|
||||
test('recovers completion response loss and rejects material drift', async () => {
|
||||
const responseLoss = harness({ failCompleteResponse: true });
|
||||
await assert.rejects(
|
||||
responseLoss.coordinator.retire(request()),
|
||||
PluginPackagePromptOutputKeyRetirementUnavailableError,
|
||||
);
|
||||
assert.equal(
|
||||
(await responseLoss.coordinator.retire(request())).status,
|
||||
'existing',
|
||||
);
|
||||
|
||||
const drift = harness({ failRetireResponse: true });
|
||||
await assert.rejects(drift.coordinator.retire(request()));
|
||||
drift.setMaterial({
|
||||
state: 'absent',
|
||||
keyId: request().keyId,
|
||||
catalogDigest: digest('drifted-catalog'),
|
||||
absenceProof: digest('drifted-absence'),
|
||||
});
|
||||
await assert.rejects(
|
||||
drift.coordinator.retire(request()),
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
PluginPackagePromptOutputKeyRetirementConflictError,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyRetirement.js');
|
||||
const {
|
||||
pluginPackagePromptOutputKeyringCatalogDigest,
|
||||
rotatePluginPackagePromptOutputKeyringManifest,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyringManifest.js');
|
||||
|
||||
function initial() {
|
||||
return Object.freeze({
|
||||
schema: 'qinglong/plugin-package-prompt-output-file-keyring@v1',
|
||||
generation: 1,
|
||||
activeKeyId: 'prompt-key-one',
|
||||
keys: Object.freeze({
|
||||
'prompt-key-one': Buffer.alloc(32, 0x11).toString('base64url'),
|
||||
}),
|
||||
retirements: Object.freeze({}),
|
||||
});
|
||||
}
|
||||
|
||||
function request(manifest, material = Buffer.alloc(32, 0x22)) {
|
||||
return {
|
||||
expectedActiveKeyId: 'prompt-key-one',
|
||||
expectedCatalogDigest:
|
||||
pluginPackagePromptOutputKeyringCatalogDigest(manifest),
|
||||
newKeyId: 'prompt-key-two',
|
||||
material,
|
||||
};
|
||||
}
|
||||
|
||||
test('rotates staged material while retaining history and exactly replays', () => {
|
||||
const before = initial();
|
||||
const staged = Buffer.alloc(32, 0x22);
|
||||
const rotated = rotatePluginPackagePromptOutputKeyringManifest(
|
||||
before,
|
||||
request(before, staged),
|
||||
);
|
||||
assert.equal(rotated.changed, true);
|
||||
assert.equal(rotated.state.generation, 2);
|
||||
assert.equal(rotated.state.previousActiveKeyId, 'prompt-key-one');
|
||||
assert.equal(rotated.state.activeKeyId, 'prompt-key-two');
|
||||
assert.deepEqual(Object.keys(rotated.manifest.keys).sort(), [
|
||||
'prompt-key-one',
|
||||
'prompt-key-two',
|
||||
]);
|
||||
assert.equal(
|
||||
rotated.manifest.keys['prompt-key-one'],
|
||||
before.keys['prompt-key-one'],
|
||||
);
|
||||
assert.deepEqual(staged, Buffer.alloc(32, 0x22));
|
||||
|
||||
const replay = rotatePluginPackagePromptOutputKeyringManifest(
|
||||
rotated.manifest,
|
||||
request(before, staged),
|
||||
);
|
||||
assert.equal(replay.changed, false);
|
||||
assert.deepEqual(replay.state, rotated.state);
|
||||
assert.equal(replay.manifest, rotated.manifest);
|
||||
});
|
||||
|
||||
test('rejects stale winners, changed material and key identity reuse', () => {
|
||||
const before = initial();
|
||||
const operation = request(before);
|
||||
const rotated = rotatePluginPackagePromptOutputKeyringManifest(
|
||||
before,
|
||||
operation,
|
||||
);
|
||||
for (const [manifest, candidate] of [
|
||||
[rotated.manifest, { ...operation, material: Buffer.alloc(32, 0x33) }],
|
||||
[before, { ...operation, expectedCatalogDigest: '0'.repeat(64) }],
|
||||
[before, { ...operation, newKeyId: 'prompt-key-one' }],
|
||||
]) {
|
||||
assert.throws(
|
||||
() => rotatePluginPackagePromptOutputKeyringManifest(manifest, candidate),
|
||||
(error) =>
|
||||
error instanceof PluginPackagePromptOutputKeyRetirementConflictError ||
|
||||
error.code === 'PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_RETIREMENT_INVALID',
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { afterEach, test } = require('node:test');
|
||||
|
||||
const {
|
||||
PluginPackagePromptOutputProjectedKeyring,
|
||||
PluginPackagePromptOutputProjectedKeyringUnavailableError,
|
||||
createPluginPackagePromptOutputProjectedKeyring,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputProjectedKeyring.js');
|
||||
const {
|
||||
PLUGIN_PACKAGE_PROMPT_OUTPUT_KEYRING_MANIFEST_SCHEMA,
|
||||
canonicalPluginPackagePromptOutputKeyringManifest,
|
||||
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyringManifest.js');
|
||||
|
||||
const roots = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
roots
|
||||
.splice(0)
|
||||
.map((root) => fs.rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
function manifest(generation, activeKeyId, keys) {
|
||||
return Object.freeze({
|
||||
schema: PLUGIN_PACKAGE_PROMPT_OUTPUT_KEYRING_MANIFEST_SCHEMA,
|
||||
generation,
|
||||
activeKeyId,
|
||||
keys: Object.freeze(keys),
|
||||
retirements: Object.freeze({}),
|
||||
});
|
||||
}
|
||||
|
||||
async function tempRoot() {
|
||||
const root = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'ql3-projected-keyring-'),
|
||||
);
|
||||
roots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
async function publish(root, generationName, bytes, mode = 0o440) {
|
||||
const generation = path.join(root, generationName);
|
||||
await fs.mkdir(generation, { mode: 0o750 });
|
||||
const target = path.join(generation, 'keyring.json');
|
||||
await fs.writeFile(target, bytes, { mode });
|
||||
await fs.chmod(target, mode);
|
||||
const next = path.join(root, '..data-next');
|
||||
await fs.symlink(generationName, next);
|
||||
await fs.rename(next, path.join(root, '..data'));
|
||||
try {
|
||||
await fs.symlink('..data/keyring.json', path.join(root, 'keyring.json'));
|
||||
} catch (error) {
|
||||
if (error.code !== 'EEXIST') throw error;
|
||||
}
|
||||
}
|
||||
|
||||
test('projected keyring follows bounded Kubernetes atomic rotation without caching', async () => {
|
||||
const root = await tempRoot();
|
||||
const keyOne = Buffer.alloc(32, 0x11).toString('base64url');
|
||||
const keyTwo = Buffer.alloc(32, 0x22).toString('base64url');
|
||||
await publish(
|
||||
root,
|
||||
'..2026_08_02_01',
|
||||
canonicalPluginPackagePromptOutputKeyringManifest(
|
||||
manifest(1, 'prompt-key-one', { 'prompt-key-one': keyOne }),
|
||||
),
|
||||
);
|
||||
|
||||
const provider = await createPluginPackagePromptOutputProjectedKeyring({
|
||||
rootDirectory: root,
|
||||
});
|
||||
const first = await provider.active();
|
||||
assert.equal(first.keyId, 'prompt-key-one');
|
||||
assert.deepEqual(Buffer.from(first.key), Buffer.alloc(32, 0x11));
|
||||
first.key.fill(0);
|
||||
|
||||
await publish(
|
||||
root,
|
||||
'..2026_08_02_02',
|
||||
canonicalPluginPackagePromptOutputKeyringManifest(
|
||||
manifest(2, 'prompt-key-two', {
|
||||
'prompt-key-one': keyOne,
|
||||
'prompt-key-two': keyTwo,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const second = await provider.active();
|
||||
assert.equal(second.keyId, 'prompt-key-two');
|
||||
assert.deepEqual(Buffer.from(second.key), Buffer.alloc(32, 0x22));
|
||||
const historical = await provider.resolve('prompt-key-one');
|
||||
assert.ok(historical);
|
||||
assert.deepEqual(Buffer.from(historical.key), Buffer.alloc(32, 0x11));
|
||||
second.key.fill(0);
|
||||
historical.key.fill(0);
|
||||
});
|
||||
|
||||
test('projected keyring rejects noncanonical, writable and escaped material', async () => {
|
||||
const key = Buffer.alloc(32, 0x33).toString('base64url');
|
||||
const value = manifest(1, 'prompt-key-one', { 'prompt-key-one': key });
|
||||
|
||||
const noncanonicalRoot = await tempRoot();
|
||||
await publish(
|
||||
noncanonicalRoot,
|
||||
'..2026_08_02_01',
|
||||
Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8'),
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
createPluginPackagePromptOutputProjectedKeyring({
|
||||
rootDirectory: noncanonicalRoot,
|
||||
}),
|
||||
PluginPackagePromptOutputProjectedKeyringUnavailableError,
|
||||
);
|
||||
|
||||
const writableRoot = await tempRoot();
|
||||
await publish(
|
||||
writableRoot,
|
||||
'..2026_08_02_01',
|
||||
canonicalPluginPackagePromptOutputKeyringManifest(value),
|
||||
0o640,
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
new PluginPackagePromptOutputProjectedKeyring({
|
||||
rootDirectory: writableRoot,
|
||||
}).active(),
|
||||
PluginPackagePromptOutputProjectedKeyringUnavailableError,
|
||||
);
|
||||
|
||||
const escapedRoot = await tempRoot();
|
||||
const externalRoot = await tempRoot();
|
||||
const external = path.join(externalRoot, 'keyring.json');
|
||||
await fs.writeFile(
|
||||
external,
|
||||
canonicalPluginPackagePromptOutputKeyringManifest(value),
|
||||
{ mode: 0o440 },
|
||||
);
|
||||
await fs.symlink(external, path.join(escapedRoot, 'keyring.json'));
|
||||
await assert.rejects(
|
||||
() =>
|
||||
new PluginPackagePromptOutputProjectedKeyring({
|
||||
rootDirectory: escapedRoot,
|
||||
}).verify(),
|
||||
PluginPackagePromptOutputProjectedKeyringUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('projected keyring rejects ambiguous roots and file names', async () => {
|
||||
const root = await tempRoot();
|
||||
const rootLink = `${root}-link`;
|
||||
roots.push(rootLink);
|
||||
await fs.symlink(root, rootLink);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
new PluginPackagePromptOutputProjectedKeyring({
|
||||
rootDirectory: rootLink,
|
||||
}).verify(),
|
||||
PluginPackagePromptOutputProjectedKeyringUnavailableError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
new PluginPackagePromptOutputProjectedKeyring({
|
||||
rootDirectory: root,
|
||||
dataFileName: '../keyring.json',
|
||||
}),
|
||||
PluginPackagePromptOutputProjectedKeyringUnavailableError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createPluginPackagePromptOutputArtifact,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputArtifact.js');
|
||||
const {
|
||||
InvalidPluginPackagePromptOutputReadError,
|
||||
PluginPackagePromptOutputReadService,
|
||||
PluginPackagePromptOutputReadUnavailableError,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputRead.js');
|
||||
|
||||
const NOW = 1_700_000_000_000;
|
||||
const KEY = Buffer.alloc(32, 7);
|
||||
const PRINCIPAL = Object.freeze({
|
||||
subject: Object.freeze({ type: 'user', id: 'user-a' }),
|
||||
authenticationId: 'auth-a',
|
||||
authenticatedAtMs: NOW - 1_000,
|
||||
expiresAtMs: NOW + 60_000,
|
||||
assurance: 'multi_factor',
|
||||
});
|
||||
const RESULT = Object.freeze({
|
||||
provider: 'openai-compatible',
|
||||
model: 'bounded-model',
|
||||
text: 'private durable output',
|
||||
finishReason: 'stop',
|
||||
usage: Object.freeze({
|
||||
inputTokens: 5,
|
||||
outputTokens: 3,
|
||||
totalTokens: 8,
|
||||
costMicros: 11,
|
||||
}),
|
||||
});
|
||||
|
||||
function artifact() {
|
||||
return createPluginPackagePromptOutputArtifact(
|
||||
{
|
||||
projectId: 'project-a',
|
||||
runId: 'run-a',
|
||||
stepRunId: 'step-a',
|
||||
invocationId: 'invocation-a',
|
||||
requestedBy: PRINCIPAL.subject,
|
||||
result: RESULT,
|
||||
retentionPolicy: { revision: 'retention-v1', retentionMs: 86_400_000 },
|
||||
keyId: 'prompt-key-1',
|
||||
key: Buffer.from(KEY),
|
||||
sealedAtMs: NOW - 1_000,
|
||||
},
|
||||
() => Buffer.alloc(12, 9),
|
||||
);
|
||||
}
|
||||
|
||||
function harness(overrides = {}) {
|
||||
const stored = artifact();
|
||||
const calls = [];
|
||||
let resolvedKey;
|
||||
const service = new PluginPackagePromptOutputReadService({
|
||||
artifacts: {
|
||||
async find(id) {
|
||||
calls.push(`find:${id}`);
|
||||
return stored;
|
||||
},
|
||||
async put() {
|
||||
throw new Error('unreachable');
|
||||
},
|
||||
},
|
||||
authorizer: {
|
||||
async authorize(request) {
|
||||
calls.push(`authorize:${request.artifactId}`);
|
||||
return { effect: 'allow' };
|
||||
},
|
||||
},
|
||||
retention: {
|
||||
async inspect(request) {
|
||||
calls.push(`retention:${request.reference.artifactId}`);
|
||||
return { state: 'retained' };
|
||||
},
|
||||
},
|
||||
keys: {
|
||||
async active() {
|
||||
throw new Error('read must not request active key');
|
||||
},
|
||||
async resolve(keyId) {
|
||||
calls.push(`key:${keyId}`);
|
||||
resolvedKey = Buffer.from(KEY);
|
||||
return { keyId, key: resolvedKey };
|
||||
},
|
||||
},
|
||||
now: () => NOW,
|
||||
...overrides,
|
||||
});
|
||||
return { service, stored, calls, resolvedKey: () => resolvedKey };
|
||||
}
|
||||
|
||||
function command(stored, overrides = {}) {
|
||||
return {
|
||||
principal: PRINCIPAL,
|
||||
projectId: stored.projectId,
|
||||
runId: stored.runId,
|
||||
artifactId: stored.artifactId,
|
||||
artifactDigest: stored.artifactDigest,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('reads only after metadata, policy and retention and wipes resolved key', async () => {
|
||||
const state = harness();
|
||||
const result = await state.service.read(command(state.stored));
|
||||
|
||||
assert.equal(result.status, 'available');
|
||||
assert.deepEqual(result.result, RESULT);
|
||||
assert.equal(result.reference.artifactDigest, state.stored.artifactDigest);
|
||||
assert.deepEqual(state.calls, [
|
||||
`find:${state.stored.artifactId}`,
|
||||
`authorize:${state.stored.artifactId}`,
|
||||
`retention:${state.stored.artifactId}`,
|
||||
'key:prompt-key-1',
|
||||
]);
|
||||
assert.equal(
|
||||
state.resolvedKey().every((byte) => byte === 0),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('masks absent and identity-drifted Artifacts before policy or key access', async () => {
|
||||
const missingCalls = [];
|
||||
const missing = harness({
|
||||
artifacts: {
|
||||
async find() {
|
||||
missingCalls.push('find');
|
||||
return null;
|
||||
},
|
||||
async put() {
|
||||
throw new Error('unreachable');
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
(await missing.service.read(command(missing.stored))).status,
|
||||
'not_found',
|
||||
);
|
||||
assert.deepEqual(missingCalls, ['find']);
|
||||
|
||||
const drift = harness();
|
||||
assert.equal(
|
||||
(
|
||||
await drift.service.read(
|
||||
command(drift.stored, { projectId: 'project-b' }),
|
||||
)
|
||||
).status,
|
||||
'not_found',
|
||||
);
|
||||
assert.deepEqual(drift.calls, [`find:${drift.stored.artifactId}`]);
|
||||
});
|
||||
|
||||
test('masks policy denial and tombstone without resolving key material', async () => {
|
||||
const denied = harness({
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return { effect: 'deny', reasonCode: 'artifact_read_denied' };
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
(await denied.service.read(command(denied.stored))).status,
|
||||
'not_found',
|
||||
);
|
||||
assert.equal(
|
||||
denied.calls.some((call) => call.startsWith('key:')),
|
||||
false,
|
||||
);
|
||||
|
||||
const tombstoned = harness({
|
||||
retention: {
|
||||
async inspect() {
|
||||
return {
|
||||
state: 'tombstoned',
|
||||
tombstonedAtMs: NOW - 1,
|
||||
tombstoneDigest: 'a'.repeat(64),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
(await tombstoned.service.read(command(tombstoned.stored))).status,
|
||||
'not_found',
|
||||
);
|
||||
assert.equal(
|
||||
tombstoned.calls.some((call) => call.startsWith('key:')),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails closed for invalid requests, corrupt decisions and missing keys', async () => {
|
||||
const invalid = harness();
|
||||
await assert.rejects(
|
||||
invalid.service.read(command(invalid.stored, { artifactDigest: 'bad' })),
|
||||
InvalidPluginPackagePromptOutputReadError,
|
||||
);
|
||||
assert.deepEqual(invalid.calls, []);
|
||||
|
||||
const corruptDecision = harness({
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return { effect: 'allow', widened: true };
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
corruptDecision.service.read(command(corruptDecision.stored)),
|
||||
PluginPackagePromptOutputReadUnavailableError,
|
||||
);
|
||||
|
||||
const missingKey = harness({
|
||||
keys: {
|
||||
async active() {
|
||||
throw new Error('unreachable');
|
||||
},
|
||||
async resolve() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
missingKey.service.read(command(missingKey.stored)),
|
||||
PluginPackagePromptOutputReadUnavailableError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidPluginPackagePromptOutputRetentionPolicyCatalogError,
|
||||
createPluginPackagePromptOutputRetentionPolicyCatalogResolver,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputRetention');
|
||||
const {
|
||||
pluginPackagePromptOutputArtifactRetentionPolicyDigest,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputArtifact');
|
||||
|
||||
function entry(projectId, revision, retentionMs) {
|
||||
const policy = { revision, retentionMs };
|
||||
return {
|
||||
projectId,
|
||||
policy,
|
||||
policyDigest:
|
||||
pluginPackagePromptOutputArtifactRetentionPolicyDigest(policy),
|
||||
};
|
||||
}
|
||||
|
||||
test('resolves one exact digest-bound Project retention policy', async () => {
|
||||
const expected = entry('project-a', 'retention-v1', 3_600_000);
|
||||
const resolver =
|
||||
createPluginPackagePromptOutputRetentionPolicyCatalogResolver({
|
||||
schemaVersion: 1,
|
||||
policies: [expected, entry('project-b', 'retention-v2', 7_200_000)],
|
||||
});
|
||||
assert.deepEqual(
|
||||
await resolver.resolve({
|
||||
projectId: expected.projectId,
|
||||
revision: expected.policy.revision,
|
||||
}),
|
||||
expected.policy,
|
||||
);
|
||||
assert.equal(
|
||||
await resolver.resolve({
|
||||
projectId: expected.projectId,
|
||||
revision: 'retention-v2',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects rewritten, duplicate, widened and unbounded policy catalogs', () => {
|
||||
const expected = entry('project-a', 'retention-v1', 3_600_000);
|
||||
assert.throws(
|
||||
() =>
|
||||
createPluginPackagePromptOutputRetentionPolicyCatalogResolver({
|
||||
schemaVersion: 1,
|
||||
policies: [{ ...expected, policyDigest: '0'.repeat(64) }],
|
||||
}),
|
||||
InvalidPluginPackagePromptOutputRetentionPolicyCatalogError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createPluginPackagePromptOutputRetentionPolicyCatalogResolver({
|
||||
schemaVersion: 1,
|
||||
policies: [expected, expected],
|
||||
}),
|
||||
InvalidPluginPackagePromptOutputRetentionPolicyCatalogError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createPluginPackagePromptOutputRetentionPolicyCatalogResolver({
|
||||
schemaVersion: 1,
|
||||
policies: [{ ...expected, enabled: true }],
|
||||
}),
|
||||
InvalidPluginPackagePromptOutputRetentionPolicyCatalogError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createPluginPackagePromptOutputRetentionPolicyCatalogResolver({
|
||||
schemaVersion: 1,
|
||||
policies: Array.from({ length: 129 }, (_, index) =>
|
||||
entry(`project-${index}`, 'retention-v1', 3_600_000),
|
||||
),
|
||||
}),
|
||||
InvalidPluginPackagePromptOutputRetentionPolicyCatalogError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,997 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { createRequire } = require('node:module');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createStepRunRecord,
|
||||
transitionStepRunMutation,
|
||||
} = require('@qinglong/runtime-core/step-run');
|
||||
const {
|
||||
ModelInvocationConflictError,
|
||||
createModelInvocationCompletionCommand,
|
||||
createModelInvocationMutationIdentity,
|
||||
createModelInvocationStartCommand,
|
||||
} = require('../dist/model-invocation/modelInvocation.js');
|
||||
const {
|
||||
migratePostgresModelInvocationFeature,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
const {
|
||||
PostgresModelInvocationRepository,
|
||||
} = require('../dist/model-invocation/postgresModelInvocationRepository.js');
|
||||
const {
|
||||
DurableModelInvocationCoordinator,
|
||||
DurableModelInvocationRecovery,
|
||||
} = require('../dist/model-invocation/durableModelInvocationCoordinator.js');
|
||||
const {
|
||||
DurableModelInvocationResolutionCoordinator,
|
||||
} = require('../dist/model-invocation/modelInvocationResolution.js');
|
||||
const {
|
||||
BoundedModelGateway,
|
||||
ModelInvocationReplayBlockedError,
|
||||
} = require('../dist/model-gateway/gateway.js');
|
||||
const {
|
||||
ModelInvocationProjectQuotaExceededError,
|
||||
createModelInvocationQuotaAdmission,
|
||||
} = require('../dist/usage/usageQuota.js');
|
||||
const {
|
||||
StaticModelPriceCatalog,
|
||||
createModelPriceCatalogEntry,
|
||||
} = require('../dist/pricing/pricing.js');
|
||||
|
||||
const migrationConnectionString =
|
||||
process.env.QL3_TEST_POSTGRES_MIGRATION_URL ??
|
||||
process.env.QL3_TEST_POSTGRES_URL;
|
||||
const runtimeConnectionString =
|
||||
process.env.QL3_TEST_POSTGRES_RUNTIME_URL ?? migrationConnectionString;
|
||||
|
||||
if (!migrationConnectionString) {
|
||||
test(
|
||||
'PostgreSQL ModelInvocation integration requires QL3_TEST_POSTGRES_URL',
|
||||
{
|
||||
skip: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const clusterRequire = createRequire(
|
||||
path.resolve(__dirname, '../../ql3-cluster-postgres/package.json'),
|
||||
);
|
||||
const { Pool } = clusterRequire('pg');
|
||||
const {
|
||||
runPostgresMigrations,
|
||||
} = require('../../ql3-cluster-postgres/dist/migration/migration.js');
|
||||
const {
|
||||
assertPostgresSchemaReady,
|
||||
} = require('../../ql3-cluster-postgres/dist/schema/schemaReadiness.js');
|
||||
|
||||
const NOW = 2_000_000;
|
||||
const RUN_ID = '51000000-0000-4000-8000-000000000001';
|
||||
const CONFLICT_RUN_ID = '51000000-0000-4000-8000-000000000002';
|
||||
const FAULT_RUN_ID = '51000000-0000-4000-8000-000000000003';
|
||||
const QUOTA_RUN_A_ID = '51000000-0000-4000-8000-000000000004';
|
||||
const QUOTA_RUN_B_ID = '51000000-0000-4000-8000-000000000005';
|
||||
const PRICING_RUN_ID = '51000000-0000-4000-8000-000000000006';
|
||||
|
||||
function pool(connectionString, applicationName) {
|
||||
return new Pool({
|
||||
connectionString,
|
||||
ssl: false,
|
||||
max: 4,
|
||||
application_name: applicationName,
|
||||
});
|
||||
}
|
||||
|
||||
function commitResponseLossPool(basePool) {
|
||||
let injected = false;
|
||||
return {
|
||||
query(text, values) {
|
||||
return basePool.query(text, values);
|
||||
},
|
||||
async connect() {
|
||||
const client = await basePool.connect();
|
||||
return {
|
||||
async query(text, values) {
|
||||
const result = await client.query(text, values);
|
||||
if (!injected && /^\s*COMMIT\s*$/i.test(text)) {
|
||||
injected = true;
|
||||
const error = new Error('injected response loss after COMMIT');
|
||||
error.code = 'ECONNRESET';
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
release() {
|
||||
client.release();
|
||||
},
|
||||
};
|
||||
},
|
||||
wasInjected() {
|
||||
return injected;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function audit(phase, overrides = {}) {
|
||||
return {
|
||||
phase,
|
||||
projectId: 'project-ai',
|
||||
runId: RUN_ID,
|
||||
stepRunId: 'model-step-ai',
|
||||
traceId: 'trace-ai',
|
||||
requestId: 'request-ai',
|
||||
provider: 'remote',
|
||||
model: 'model-ai',
|
||||
policyRevision: 'policy-1',
|
||||
requestDigest: `sha256:${'b'.repeat(64)}`,
|
||||
deadlineAtMs: NOW + 10_000,
|
||||
inputBytes: 128,
|
||||
maxOutputTokens: 64,
|
||||
outputBytes: 0,
|
||||
usage: null,
|
||||
errorCode: null,
|
||||
occurredAtMs: NOW,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function readyStep(runId, stepRunId, mutationId) {
|
||||
return createStepRunRecord({
|
||||
id: stepRunId,
|
||||
runId,
|
||||
stepKey: 'summarize',
|
||||
kind: 'model',
|
||||
definitionRef: 'prompt:summary@1',
|
||||
definitionDigest: 'a'.repeat(64),
|
||||
required: true,
|
||||
initialStatus: 'ready',
|
||||
inputRef: `artifact:${stepRunId}:input`,
|
||||
mutationId,
|
||||
createdAtMs: NOW - 1,
|
||||
});
|
||||
}
|
||||
|
||||
function startCommand(ready, auditRecord = audit('admitted')) {
|
||||
const identity = createModelInvocationMutationIdentity(
|
||||
auditRecord.requestId,
|
||||
'start',
|
||||
);
|
||||
return createModelInvocationStartCommand(
|
||||
auditRecord,
|
||||
transitionStepRunMutation(
|
||||
ready,
|
||||
{
|
||||
expectedVersion: ready.version,
|
||||
expectedDigest: ready.stepRunDigest,
|
||||
mutationId: identity.mutationId,
|
||||
to: 'running',
|
||||
atMs: auditRecord.occurredAtMs,
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 1,
|
||||
expectedRunEventSequence: 1,
|
||||
eventId: identity.eventId,
|
||||
dedupeKey: identity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function completionCommand(start) {
|
||||
const identity = createModelInvocationMutationIdentity(
|
||||
start.start.invocationId,
|
||||
'completion',
|
||||
);
|
||||
const completedAudit = audit('completed', {
|
||||
runId: start.start.runId,
|
||||
stepRunId: start.start.stepRunId,
|
||||
traceId: start.start.traceId,
|
||||
requestId: start.start.invocationId,
|
||||
occurredAtMs: NOW + 25,
|
||||
outputBytes: 12,
|
||||
usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
|
||||
});
|
||||
return createModelInvocationCompletionCommand(
|
||||
start.start,
|
||||
completedAudit,
|
||||
transitionStepRunMutation(
|
||||
start.stepRunMutation.stepRun,
|
||||
{
|
||||
expectedVersion: start.start.startedStepRunVersion,
|
||||
expectedDigest: start.start.startedStepRunDigest,
|
||||
mutationId: identity.mutationId,
|
||||
to: 'succeeded',
|
||||
atMs: completedAudit.occurredAtMs,
|
||||
outputRef: `model-invocation:${start.start.invocationId}`,
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 2,
|
||||
expectedRunEventSequence: 2,
|
||||
eventId: identity.eventId,
|
||||
dedupeKey: identity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteFixture(client, runId) {
|
||||
await client.query(
|
||||
`DELETE FROM "ql3_ai"."model_invocation_price_settlements"
|
||||
WHERE invocation_id IN (
|
||||
SELECT invocation_id
|
||||
FROM "ql3_ai"."model_invocation_starts"
|
||||
WHERE run_id = $1
|
||||
)`,
|
||||
[runId],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM "ql3_ai"."model_invocation_quota_settlements"
|
||||
WHERE invocation_id IN (
|
||||
SELECT invocation_id
|
||||
FROM "ql3_ai"."model_invocation_starts"
|
||||
WHERE run_id = $1
|
||||
)`,
|
||||
[runId],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM "ql3_ai"."model_invocation_quota_reservations"
|
||||
WHERE invocation_id IN (
|
||||
SELECT invocation_id
|
||||
FROM "ql3_ai"."model_invocation_starts"
|
||||
WHERE run_id = $1
|
||||
)`,
|
||||
[runId],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM "ql3_ai"."model_invocation_price_quotes"
|
||||
WHERE invocation_id IN (
|
||||
SELECT invocation_id
|
||||
FROM "ql3_ai"."model_invocation_starts"
|
||||
WHERE run_id = $1
|
||||
)`,
|
||||
[runId],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM "ql3_ai"."model_invocation_resolutions"
|
||||
WHERE run_id = $1`,
|
||||
[runId],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM "ql3_ai"."model_invocation_usage_ledger"
|
||||
WHERE run_id = $1`,
|
||||
[runId],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM "ql3_ai"."model_invocation_completions"
|
||||
WHERE run_id = $1`,
|
||||
[runId],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM "ql3_ai"."model_invocation_starts" WHERE run_id = $1`,
|
||||
[runId],
|
||||
);
|
||||
await client.query(
|
||||
`DELETE FROM "ql3"."step_run_mutations" WHERE run_id = $1`,
|
||||
[runId],
|
||||
);
|
||||
await client.query(`DELETE FROM "ql3"."run_events" WHERE run_id = $1`, [
|
||||
runId,
|
||||
]);
|
||||
await client.query(`DELETE FROM "ql3"."step_runs" WHERE run_id = $1`, [
|
||||
runId,
|
||||
]);
|
||||
await client.query(`DELETE FROM "ql3"."runs" WHERE id = $1`, [runId]);
|
||||
}
|
||||
|
||||
async function insertFixture(
|
||||
client,
|
||||
ready,
|
||||
runVersion = 1,
|
||||
projectId = 'project-ai',
|
||||
) {
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."runs" (
|
||||
id, project_id, task_id, task_revision, trigger_type,
|
||||
execution_origin, execution_owner, status, version,
|
||||
event_sequence, priority, created_at_ms
|
||||
) VALUES (
|
||||
$1, $4, 'task-ai', 'task-revision-ai', 'manual',
|
||||
'manual', 'runtime', 'running', $2, 1, 0, $3
|
||||
)`,
|
||||
[ready.runId, runVersion, NOW - 1, projectId],
|
||||
);
|
||||
await client.query(
|
||||
`INSERT INTO "ql3"."step_runs" (
|
||||
id, run_id, parent_step_run_id, step_key, kind, definition_ref,
|
||||
definition_digest, required, status, version, attempt_count,
|
||||
input_ref, output_ref, approval_request_id, ready_at_ms,
|
||||
started_at_ms, finished_at_ms, result_code, error_summary,
|
||||
created_at_ms, updated_at_ms, last_mutation_id, step_run_digest,
|
||||
step_run_json
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
||||
$14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24::jsonb
|
||||
)`,
|
||||
[
|
||||
ready.id,
|
||||
ready.runId,
|
||||
ready.parentStepRunId,
|
||||
ready.stepKey,
|
||||
ready.kind,
|
||||
ready.definitionRef,
|
||||
ready.definitionDigest,
|
||||
ready.required,
|
||||
ready.status,
|
||||
ready.version,
|
||||
ready.attemptCount,
|
||||
ready.inputRef,
|
||||
ready.outputRef,
|
||||
ready.approvalRequestId,
|
||||
ready.readyAtMs,
|
||||
ready.startedAtMs,
|
||||
ready.finishedAtMs,
|
||||
ready.resultCode,
|
||||
ready.errorSummary,
|
||||
ready.createdAtMs,
|
||||
ready.updatedAtMs,
|
||||
ready.lastMutationId,
|
||||
ready.stepRunDigest,
|
||||
JSON.stringify(ready),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test('PostgreSQL migration and repository preserve atomic model invocation facts', async () => {
|
||||
const migrationPool = pool(
|
||||
migrationConnectionString,
|
||||
'ql3-ai-model-invocation-migration-test',
|
||||
);
|
||||
const runtimePool = pool(
|
||||
runtimeConnectionString,
|
||||
'ql3-ai-model-invocation-runtime-test',
|
||||
);
|
||||
try {
|
||||
await runPostgresMigrations({ pool: migrationPool });
|
||||
await migratePostgresModelInvocationFeature(migrationPool);
|
||||
await runPostgresMigrations({ pool: migrationPool });
|
||||
await migratePostgresModelInvocationFeature(migrationPool);
|
||||
assert.equal((await assertPostgresSchemaReady(runtimePool)).ready, true);
|
||||
|
||||
const privileges = await runtimePool.query(
|
||||
`SELECT
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_starts', 'SELECT'
|
||||
) AS start_select,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_starts', 'INSERT'
|
||||
) AS start_insert,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_starts', 'UPDATE'
|
||||
) AS start_update,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_starts', 'DELETE'
|
||||
) AS start_delete,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_completions', 'SELECT'
|
||||
) AS completion_select,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_completions', 'INSERT'
|
||||
) AS completion_insert,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_resolutions', 'SELECT'
|
||||
) AS resolution_select,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_resolutions', 'INSERT'
|
||||
) AS resolution_insert,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_resolutions', 'UPDATE'
|
||||
) AS resolution_update,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_resolutions', 'DELETE'
|
||||
) AS resolution_delete,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_usage_ledger', 'SELECT'
|
||||
) AS usage_select,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_usage_ledger', 'INSERT'
|
||||
) AS usage_insert,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_usage_ledger', 'UPDATE'
|
||||
) AS usage_update,
|
||||
has_table_privilege(
|
||||
current_user, 'ql3_ai.model_invocation_usage_ledger', 'DELETE'
|
||||
) AS usage_delete,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_quota_reservations', 'SELECT'
|
||||
) AS quota_reservation_select,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_quota_reservations', 'INSERT'
|
||||
) AS quota_reservation_insert,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_quota_reservations', 'UPDATE'
|
||||
) AS quota_reservation_update,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_quota_reservations', 'DELETE'
|
||||
) AS quota_reservation_delete,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_quota_settlements', 'SELECT'
|
||||
) AS quota_settlement_select,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_quota_settlements', 'INSERT'
|
||||
) AS quota_settlement_insert,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_quota_settlements', 'UPDATE'
|
||||
) AS quota_settlement_update,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_quota_settlements', 'DELETE'
|
||||
) AS quota_settlement_delete,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_price_quotes', 'SELECT'
|
||||
) AS price_quote_select,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_price_quotes', 'INSERT'
|
||||
) AS price_quote_insert,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_price_quotes', 'UPDATE'
|
||||
) AS price_quote_update,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_price_quotes', 'DELETE'
|
||||
) AS price_quote_delete,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_price_settlements', 'SELECT'
|
||||
) AS price_settlement_select,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_price_settlements', 'INSERT'
|
||||
) AS price_settlement_insert,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_price_settlements', 'UPDATE'
|
||||
) AS price_settlement_update,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_price_settlements', 'DELETE'
|
||||
) AS price_settlement_delete`,
|
||||
);
|
||||
assert.deepEqual(privileges.rows[0], {
|
||||
start_select: true,
|
||||
start_insert: true,
|
||||
start_update: false,
|
||||
start_delete: false,
|
||||
completion_select: true,
|
||||
completion_insert: true,
|
||||
resolution_select: true,
|
||||
resolution_insert: true,
|
||||
resolution_update: false,
|
||||
resolution_delete: false,
|
||||
usage_select: true,
|
||||
usage_insert: true,
|
||||
usage_update: false,
|
||||
usage_delete: false,
|
||||
quota_reservation_select: true,
|
||||
quota_reservation_insert: true,
|
||||
quota_reservation_update: false,
|
||||
quota_reservation_delete: false,
|
||||
quota_settlement_select: true,
|
||||
quota_settlement_insert: true,
|
||||
quota_settlement_update: false,
|
||||
quota_settlement_delete: false,
|
||||
price_quote_select: true,
|
||||
price_quote_insert: true,
|
||||
price_quote_update: false,
|
||||
price_quote_delete: false,
|
||||
price_settlement_select: true,
|
||||
price_settlement_insert: true,
|
||||
price_settlement_update: false,
|
||||
price_settlement_delete: false,
|
||||
});
|
||||
|
||||
await deleteFixture(migrationPool, RUN_ID);
|
||||
await deleteFixture(migrationPool, CONFLICT_RUN_ID);
|
||||
await deleteFixture(migrationPool, FAULT_RUN_ID);
|
||||
await deleteFixture(migrationPool, QUOTA_RUN_A_ID);
|
||||
await deleteFixture(migrationPool, QUOTA_RUN_B_ID);
|
||||
await deleteFixture(migrationPool, PRICING_RUN_ID);
|
||||
const ready = readyStep(RUN_ID, 'model-step-ai', 'create-model-step-ai');
|
||||
await insertFixture(migrationPool, ready);
|
||||
const start = startCommand(ready);
|
||||
const completion = completionCommand(start);
|
||||
const repository = new PostgresModelInvocationRepository(runtimePool);
|
||||
|
||||
const authority = await repository.readAuthority({
|
||||
projectId: 'project-ai',
|
||||
runId: RUN_ID,
|
||||
stepRunId: 'model-step-ai',
|
||||
});
|
||||
assert.equal(authority.stepRun.status, 'ready');
|
||||
assert.equal(authority.runVersion, 1);
|
||||
assert.equal((await repository.admit(start)).status, 'created');
|
||||
assert.equal((await repository.admit(start)).status, 'existing');
|
||||
assert.deepEqual(await repository.findStart('request-ai'), start.start);
|
||||
const incomplete = await repository.listIncomplete(1);
|
||||
assert.deepEqual(
|
||||
incomplete.candidates.map((candidate) => candidate.invocationId),
|
||||
['request-ai'],
|
||||
);
|
||||
assert.equal(incomplete.hasMore, false);
|
||||
assert.equal((await repository.complete(completion)).status, 'created');
|
||||
assert.equal((await repository.complete(completion)).status, 'existing');
|
||||
assert.deepEqual(
|
||||
await repository.findCompletion('request-ai'),
|
||||
completion.completion,
|
||||
);
|
||||
const usage = await repository.findUsage('request-ai');
|
||||
assert.equal(
|
||||
usage.completionDigest,
|
||||
completion.completion.completionDigest,
|
||||
);
|
||||
assert.equal(usage.totalTokens, 7);
|
||||
assert.deepEqual(
|
||||
await repository.summarizeProjectUsage({
|
||||
projectId: 'project-ai',
|
||||
fromMsInclusive: NOW,
|
||||
toMsExclusive: NOW + 100,
|
||||
}),
|
||||
{
|
||||
invocationCount: 1,
|
||||
inputTokens: 5,
|
||||
outputTokens: 2,
|
||||
totalTokens: 7,
|
||||
knownCostMicros: 0,
|
||||
unknownCostInvocations: 1,
|
||||
},
|
||||
);
|
||||
|
||||
const facts = await migrationPool.query(
|
||||
`SELECT
|
||||
(SELECT status FROM "ql3"."step_runs"
|
||||
WHERE id = 'model-step-ai') AS step_status,
|
||||
(SELECT version FROM "ql3"."step_runs"
|
||||
WHERE id = 'model-step-ai') AS step_version,
|
||||
(SELECT version FROM "ql3"."runs" WHERE id = $1) AS run_version,
|
||||
(SELECT event_sequence FROM "ql3"."runs" WHERE id = $1)
|
||||
AS event_sequence,
|
||||
(SELECT count(*) FROM "ql3"."run_events" WHERE run_id = $1)
|
||||
AS events,
|
||||
(SELECT count(*) FROM "ql3"."step_run_mutations"
|
||||
WHERE run_id = $1) AS mutations,
|
||||
(SELECT count(*) FROM "ql3_ai"."model_invocation_starts"
|
||||
WHERE run_id = $1) AS starts,
|
||||
(SELECT count(*) FROM "ql3_ai"."model_invocation_completions"
|
||||
WHERE run_id = $1) AS completions,
|
||||
(SELECT count(*) FROM "ql3_ai"."model_invocation_usage_ledger"
|
||||
WHERE run_id = $1) AS usage`,
|
||||
[RUN_ID],
|
||||
);
|
||||
assert.deepEqual(facts.rows[0], {
|
||||
step_status: 'succeeded',
|
||||
step_version: 3,
|
||||
run_version: 3,
|
||||
event_sequence: 3,
|
||||
events: '2',
|
||||
mutations: '2',
|
||||
starts: '1',
|
||||
completions: '1',
|
||||
usage: '1',
|
||||
});
|
||||
|
||||
const quotaReadyA = readyStep(
|
||||
QUOTA_RUN_A_ID,
|
||||
'model-step-quota-a',
|
||||
'create-model-step-quota-a',
|
||||
);
|
||||
const quotaReadyB = readyStep(
|
||||
QUOTA_RUN_B_ID,
|
||||
'model-step-quota-b',
|
||||
'create-model-step-quota-b',
|
||||
);
|
||||
await insertFixture(migrationPool, quotaReadyA);
|
||||
await insertFixture(migrationPool, quotaReadyB);
|
||||
const quotaStartA = startCommand(
|
||||
quotaReadyA,
|
||||
audit('admitted', {
|
||||
runId: QUOTA_RUN_A_ID,
|
||||
stepRunId: 'model-step-quota-a',
|
||||
traceId: 'trace-quota-a',
|
||||
requestId: 'request-quota-a',
|
||||
}),
|
||||
);
|
||||
const quotaStartB = startCommand(
|
||||
quotaReadyB,
|
||||
audit('admitted', {
|
||||
runId: QUOTA_RUN_B_ID,
|
||||
stepRunId: 'model-step-quota-b',
|
||||
traceId: 'trace-quota-b',
|
||||
requestId: 'request-quota-b',
|
||||
}),
|
||||
);
|
||||
const quotaPolicy = {
|
||||
revision: 'quota-concurrency-1',
|
||||
windowMs: 3_600_000,
|
||||
maxInvocations: 1,
|
||||
maxTokens: 512,
|
||||
maxCostMicros: null,
|
||||
};
|
||||
const quotaAdmissions = [quotaStartA, quotaStartB].map((command) =>
|
||||
createModelInvocationQuotaAdmission({
|
||||
invocationId: command.start.invocationId,
|
||||
projectId: command.start.projectId,
|
||||
modelPolicyRevision: command.start.policyRevision,
|
||||
reservedTokens: 256,
|
||||
reservedCostMicros: null,
|
||||
quota: quotaPolicy,
|
||||
}),
|
||||
);
|
||||
const quotaResults = await Promise.allSettled([
|
||||
repository.admitWithQuota(quotaStartA, quotaAdmissions[0]),
|
||||
repository.admitWithQuota(quotaStartB, quotaAdmissions[1]),
|
||||
]);
|
||||
assert.equal(
|
||||
quotaResults.filter((result) => result.status === 'fulfilled').length,
|
||||
1,
|
||||
JSON.stringify(
|
||||
quotaResults.map((result) =>
|
||||
result.status === 'fulfilled'
|
||||
? { status: result.status }
|
||||
: {
|
||||
status: result.status,
|
||||
name: result.reason?.name,
|
||||
code: result.reason?.code,
|
||||
message: result.reason?.message,
|
||||
cause: result.reason?.cause?.message,
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
const quotaRejection = quotaResults.find(
|
||||
(result) => result.status === 'rejected',
|
||||
);
|
||||
assert.ok(
|
||||
quotaRejection.reason instanceof
|
||||
ModelInvocationProjectQuotaExceededError,
|
||||
);
|
||||
const winnerIndex = quotaResults.findIndex(
|
||||
(result) => result.status === 'fulfilled',
|
||||
);
|
||||
const winningStart = [quotaStartA, quotaStartB][winnerIndex];
|
||||
const losingStart = [quotaStartA, quotaStartB][1 - winnerIndex];
|
||||
assert.equal(
|
||||
await repository.findStart(losingStart.start.invocationId),
|
||||
null,
|
||||
);
|
||||
const winningReservation = await repository.findQuotaReservation(
|
||||
winningStart.start.invocationId,
|
||||
);
|
||||
assert.equal(winningReservation.reservedTokens, 256);
|
||||
assert.equal(
|
||||
(await repository.completeWithQuota(completionCommand(winningStart)))
|
||||
.status,
|
||||
'created',
|
||||
);
|
||||
const winningSettlement = await repository.findQuotaSettlement(
|
||||
winningStart.start.invocationId,
|
||||
);
|
||||
assert.equal(winningSettlement.effectiveTokens, 7);
|
||||
assert.equal(winningSettlement.retainedTokenReservation, false);
|
||||
assert.deepEqual(
|
||||
await repository.readQuotaWindowUsage(
|
||||
'project-ai',
|
||||
winningReservation.reservedAtMs,
|
||||
),
|
||||
{
|
||||
projectId: 'project-ai',
|
||||
windowStartMs: winningReservation.windowStartMs,
|
||||
windowEndMs: winningReservation.windowEndMs,
|
||||
invocationCount: 1,
|
||||
effectiveTokens: 7,
|
||||
effectiveCostMicros: 0,
|
||||
unknownCostInvocations: 1,
|
||||
},
|
||||
);
|
||||
|
||||
const pricingReady = readyStep(
|
||||
PRICING_RUN_ID,
|
||||
'model-step-pricing',
|
||||
'create-model-step-pricing',
|
||||
);
|
||||
await insertFixture(migrationPool, pricingReady, 1, 'project-pricing');
|
||||
const pricing = new StaticModelPriceCatalog([
|
||||
createModelPriceCatalogEntry({
|
||||
provider: 'remote',
|
||||
model: 'model-ai',
|
||||
priceRevision: 'price-1',
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: 150_000,
|
||||
outputMicrosPerMillionTokens: 600_000,
|
||||
publishedAtMs: NOW - 1,
|
||||
}),
|
||||
]);
|
||||
const pricedGateway = new BoundedModelGateway({
|
||||
providers: [
|
||||
{
|
||||
type: 'remote',
|
||||
async listModels() {
|
||||
return [{ id: 'model-ai' }];
|
||||
},
|
||||
async generate() {
|
||||
return {
|
||||
provider: 'remote',
|
||||
model: 'model-ai',
|
||||
text: 'priced summary',
|
||||
finishReason: 'stop',
|
||||
usage: {
|
||||
inputTokens: 5,
|
||||
outputTokens: 2,
|
||||
totalTokens: 7,
|
||||
costMicros: 99_999,
|
||||
},
|
||||
};
|
||||
},
|
||||
async *stream() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
],
|
||||
pricing,
|
||||
policies: {
|
||||
async resolve() {
|
||||
return {
|
||||
revision: 'policy-priced-1',
|
||||
allowedProviders: ['remote'],
|
||||
allowedModels: ['model-ai'],
|
||||
maxInputBytes: 4096,
|
||||
maxOutputBytes: 4096,
|
||||
maxOutputTokens: 64,
|
||||
maxTotalTokens: 256,
|
||||
maxCostMicros: 100,
|
||||
priceRevision: 'price-1',
|
||||
projectQuota: {
|
||||
revision: 'quota-priced-1',
|
||||
windowMs: 3_600_000,
|
||||
maxInvocations: 10,
|
||||
maxTokens: 10_000,
|
||||
maxCostMicros: 1_000,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
audit: new DurableModelInvocationCoordinator(repository),
|
||||
maxConcurrent: 1,
|
||||
now: () => NOW,
|
||||
});
|
||||
const pricedResult = await pricedGateway.generate(
|
||||
{
|
||||
provider: 'remote',
|
||||
model: 'model-ai',
|
||||
messages: [{ role: 'user', content: 'priced prompt' }],
|
||||
maxOutputTokens: 64,
|
||||
},
|
||||
{
|
||||
projectId: 'project-pricing',
|
||||
runId: PRICING_RUN_ID,
|
||||
stepRunId: 'model-step-pricing',
|
||||
traceId: 'trace-pricing',
|
||||
requestId: 'request-pricing',
|
||||
deadlineAtMs: NOW + 10_000,
|
||||
},
|
||||
);
|
||||
const priceQuote = await repository.findPriceQuote('request-pricing');
|
||||
const priceSettlement = await repository.findPriceSettlement(
|
||||
'request-pricing',
|
||||
);
|
||||
const pricedReservation = await repository.findQuotaReservation(
|
||||
'request-pricing',
|
||||
);
|
||||
const pricedQuotaSettlement = await repository.findQuotaSettlement(
|
||||
'request-pricing',
|
||||
);
|
||||
assert.equal(pricedResult.usage.costMicros, 3);
|
||||
assert.equal(priceQuote.reservedCostMicros, 68);
|
||||
assert.equal(priceSettlement.costMicros, 3);
|
||||
assert.equal(
|
||||
(await repository.findUsage('request-pricing')).costMicros,
|
||||
3,
|
||||
);
|
||||
assert.equal(pricedReservation.reservedCostMicros, 68);
|
||||
assert.equal(pricedQuotaSettlement.effectiveCostMicros, 3);
|
||||
|
||||
const conflictReady = readyStep(
|
||||
CONFLICT_RUN_ID,
|
||||
'model-step-conflict',
|
||||
'create-model-step-conflict',
|
||||
);
|
||||
await insertFixture(migrationPool, conflictReady, 2);
|
||||
const conflictStart = startCommand(
|
||||
conflictReady,
|
||||
audit('admitted', {
|
||||
runId: CONFLICT_RUN_ID,
|
||||
stepRunId: 'model-step-conflict',
|
||||
traceId: 'trace-conflict',
|
||||
requestId: 'request-conflict',
|
||||
}),
|
||||
);
|
||||
await assert.rejects(
|
||||
repository.admit(conflictStart),
|
||||
ModelInvocationConflictError,
|
||||
);
|
||||
const rolledBack = await migrationPool.query(
|
||||
`SELECT
|
||||
(SELECT count(*) FROM "ql3_ai"."model_invocation_starts"
|
||||
WHERE run_id = $1) AS starts,
|
||||
(SELECT count(*) FROM "ql3"."run_events"
|
||||
WHERE run_id = $1) AS events,
|
||||
(SELECT count(*) FROM "ql3"."step_run_mutations"
|
||||
WHERE run_id = $1) AS mutations`,
|
||||
[CONFLICT_RUN_ID],
|
||||
);
|
||||
assert.deepEqual(rolledBack.rows[0], {
|
||||
starts: '0',
|
||||
events: '0',
|
||||
mutations: '0',
|
||||
});
|
||||
|
||||
const faultReady = readyStep(
|
||||
FAULT_RUN_ID,
|
||||
'model-step-fault',
|
||||
'create-model-step-fault',
|
||||
);
|
||||
await insertFixture(migrationPool, faultReady);
|
||||
const faultPool = commitResponseLossPool(runtimePool);
|
||||
const faultRepository = new PostgresModelInvocationRepository(faultPool);
|
||||
const faultCoordinator = new DurableModelInvocationCoordinator(
|
||||
faultRepository,
|
||||
);
|
||||
let providerCalls = 0;
|
||||
const gateway = new BoundedModelGateway({
|
||||
providers: [
|
||||
{
|
||||
type: 'remote',
|
||||
async listModels() {
|
||||
return [{ id: 'model-ai' }];
|
||||
},
|
||||
async generate() {
|
||||
providerCalls += 1;
|
||||
throw new Error('provider must not run after ambiguous commit');
|
||||
},
|
||||
async *stream() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
],
|
||||
pricing: {
|
||||
async resolve() {
|
||||
throw new Error('pricing must remain unreachable');
|
||||
},
|
||||
},
|
||||
policies: {
|
||||
async resolve() {
|
||||
return {
|
||||
revision: 'policy-1',
|
||||
allowedProviders: ['remote'],
|
||||
allowedModels: ['model-ai'],
|
||||
maxInputBytes: 4096,
|
||||
maxOutputBytes: 4096,
|
||||
maxOutputTokens: 64,
|
||||
maxTotalTokens: 256,
|
||||
maxCostMicros: null,
|
||||
priceRevision: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
audit: faultCoordinator,
|
||||
maxConcurrent: 1,
|
||||
now: () => NOW,
|
||||
});
|
||||
await assert.rejects(
|
||||
gateway.generate(
|
||||
{
|
||||
provider: 'remote',
|
||||
model: 'model-ai',
|
||||
messages: [{ role: 'user', content: 'must not persist' }],
|
||||
maxOutputTokens: 64,
|
||||
},
|
||||
{
|
||||
projectId: 'project-ai',
|
||||
runId: FAULT_RUN_ID,
|
||||
stepRunId: 'model-step-fault',
|
||||
traceId: 'trace-fault',
|
||||
requestId: 'request-fault',
|
||||
deadlineAtMs: NOW + 10_000,
|
||||
},
|
||||
),
|
||||
ModelInvocationReplayBlockedError,
|
||||
);
|
||||
assert.equal(faultPool.wasInjected(), true);
|
||||
assert.equal(providerCalls, 0);
|
||||
const committedStart = await repository.findStart('request-fault');
|
||||
assert.equal(committedStart.invocationId, 'request-fault');
|
||||
assert.equal(
|
||||
JSON.stringify(committedStart).includes('must not persist'),
|
||||
false,
|
||||
);
|
||||
|
||||
const recovery = await new DurableModelInvocationRecovery(
|
||||
repository,
|
||||
).recover(8);
|
||||
assert.deepEqual(
|
||||
{
|
||||
recovered: recovery.recovered,
|
||||
failed: recovery.failed,
|
||||
hasMore: recovery.hasMore,
|
||||
},
|
||||
{ recovered: 1, failed: 0, hasMore: false },
|
||||
);
|
||||
const recoveredCompletion = await repository.findCompletion(
|
||||
'request-fault',
|
||||
);
|
||||
assert.equal(recoveredCompletion.outcome, 'outcome_unknown');
|
||||
assert.equal(await repository.findUsage('request-fault'), null);
|
||||
const recoveredStep = await migrationPool.query(
|
||||
`SELECT status FROM "ql3"."step_runs" WHERE id = 'model-step-fault'`,
|
||||
);
|
||||
assert.equal(recoveredStep.rows[0].status, 'lost');
|
||||
|
||||
const resolution = await new DurableModelInvocationResolutionCoordinator(
|
||||
repository,
|
||||
).resolve({
|
||||
invocationId: 'request-fault',
|
||||
decision: 'retry',
|
||||
resolvedByUserId: 'user-ai',
|
||||
resolvedAtMs: recoveredCompletion.completedAtMs + 1,
|
||||
});
|
||||
assert.equal(resolution.status, 'created');
|
||||
const retryAtMs = recoveredCompletion.completedAtMs + 2;
|
||||
assert.deepEqual(
|
||||
await new DurableModelInvocationCoordinator(repository).record(
|
||||
audit('admitted', {
|
||||
runId: FAULT_RUN_ID,
|
||||
stepRunId: 'model-step-fault',
|
||||
traceId: 'trace-fault-retry',
|
||||
requestId: 'request-fault-retry',
|
||||
occurredAtMs: retryAtMs,
|
||||
deadlineAtMs: retryAtMs + 10_000,
|
||||
}),
|
||||
),
|
||||
{ status: 'created' },
|
||||
);
|
||||
const retried = await migrationPool.query(
|
||||
`SELECT
|
||||
(SELECT status FROM "ql3"."step_runs"
|
||||
WHERE id = 'model-step-fault') AS step_status,
|
||||
(SELECT attempt_count FROM "ql3"."step_runs"
|
||||
WHERE id = 'model-step-fault') AS attempt_count,
|
||||
(SELECT count(*) FROM "ql3_ai"."model_invocation_starts"
|
||||
WHERE run_id = $1) AS starts,
|
||||
(SELECT count(*) FROM "ql3_ai"."model_invocation_completions"
|
||||
WHERE run_id = $1) AS completions,
|
||||
(SELECT count(*) FROM "ql3_ai"."model_invocation_resolutions"
|
||||
WHERE run_id = $1) AS resolutions`,
|
||||
[FAULT_RUN_ID],
|
||||
);
|
||||
assert.deepEqual(retried.rows[0], {
|
||||
step_status: 'running',
|
||||
attempt_count: 2,
|
||||
starts: '2',
|
||||
completions: '1',
|
||||
resolutions: '1',
|
||||
});
|
||||
} finally {
|
||||
await runtimePool.end();
|
||||
await migrationPool.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { randomUUID } = require('node:crypto');
|
||||
const { createRequire } = require('node:module');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
ModelPriceCatalogConflictError,
|
||||
createModelPriceCatalogPublishCommand,
|
||||
createModelPriceCatalogTransitionCommand,
|
||||
} = require('../dist/pricing/modelPriceCatalog.js');
|
||||
const {
|
||||
migratePostgresModelInvocationFeature,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
const {
|
||||
ModelPriceCatalogManagementSeparationOfDutyError,
|
||||
createModelPriceCatalogManagementService,
|
||||
createModelPriceCatalogPolicyDecision,
|
||||
} = require('../dist/pricing/modelPriceCatalogManagement.js');
|
||||
const {
|
||||
PostgresModelPriceCatalogReader,
|
||||
PostgresModelPriceCatalogRepository,
|
||||
} = require('../dist/pricing/storage/postgresModelPriceCatalogRepository.js');
|
||||
|
||||
const migrationConnectionString =
|
||||
process.env.QL3_TEST_POSTGRES_MIGRATION_URL ??
|
||||
process.env.QL3_TEST_POSTGRES_URL;
|
||||
const adminConnectionString = process.env.QL3_TEST_POSTGRES_ADMIN_URL;
|
||||
const runtimeConnectionString = process.env.QL3_TEST_POSTGRES_RUNTIME_URL;
|
||||
|
||||
if (
|
||||
!migrationConnectionString ||
|
||||
!adminConnectionString ||
|
||||
!runtimeConnectionString
|
||||
) {
|
||||
test(
|
||||
'PostgreSQL Model price catalog integration requires migration, admin and runtime URLs',
|
||||
{ skip: true },
|
||||
);
|
||||
} else {
|
||||
const clusterRequire = createRequire(
|
||||
path.resolve(__dirname, '../../ql3-cluster-postgres/package.json'),
|
||||
);
|
||||
const { Pool } = clusterRequire('pg');
|
||||
const {
|
||||
runPostgresMigrations,
|
||||
} = require('../../ql3-cluster-postgres/dist/migration/migration.js');
|
||||
|
||||
function pool(connectionString, applicationName) {
|
||||
return new Pool({
|
||||
connectionString,
|
||||
ssl: false,
|
||||
max: 4,
|
||||
application_name: applicationName,
|
||||
});
|
||||
}
|
||||
|
||||
function publish(provider, model, revision, mutationId, rate) {
|
||||
return createModelPriceCatalogPublishCommand({
|
||||
provider,
|
||||
model,
|
||||
priceRevision: revision,
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: rate,
|
||||
outputMicrosPerMillionTokens: rate * 4,
|
||||
mutationId,
|
||||
publishedByUserId: 'integration-admin',
|
||||
});
|
||||
}
|
||||
|
||||
function transition(provider, model, head, action, revision, mutationId) {
|
||||
return createModelPriceCatalogTransitionCommand({
|
||||
provider,
|
||||
model,
|
||||
expectedGeneration: head?.generation ?? 0,
|
||||
expectedHeadDigest: head?.headDigest ?? null,
|
||||
action,
|
||||
priceRevision: revision,
|
||||
mutationId,
|
||||
changedByUserId: 'integration-admin',
|
||||
});
|
||||
}
|
||||
|
||||
async function assertSqlState(operation, expected) {
|
||||
await assert.rejects(operation, (error) => {
|
||||
assert.equal(error.code, expected);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
test('PostgreSQL catalog serializes publication, activation and permanent revocation', async () => {
|
||||
const suffix = randomUUID();
|
||||
const provider = `integration-${suffix}`;
|
||||
const model = 'model-price-catalog';
|
||||
const migrationPool = pool(
|
||||
migrationConnectionString,
|
||||
'ql3-ai-price-catalog-migration-test',
|
||||
);
|
||||
const adminPool = pool(
|
||||
adminConnectionString,
|
||||
'ql3-ai-price-catalog-admin-test',
|
||||
);
|
||||
const runtimePool = pool(
|
||||
runtimeConnectionString,
|
||||
'ql3-ai-price-catalog-runtime-test',
|
||||
);
|
||||
const repository = new PostgresModelPriceCatalogRepository(adminPool);
|
||||
const reader = new PostgresModelPriceCatalogReader(runtimePool);
|
||||
|
||||
try {
|
||||
await runPostgresMigrations({ pool: migrationPool });
|
||||
await migratePostgresModelInvocationFeature(migrationPool);
|
||||
|
||||
const firstCommand = publish(
|
||||
provider,
|
||||
model,
|
||||
'price-1',
|
||||
`publish-1-${suffix}`,
|
||||
150_000,
|
||||
);
|
||||
const first = await repository.publish(firstCommand);
|
||||
assert.equal(first.status, 'created');
|
||||
assert.deepEqual(await repository.publish(firstCommand), {
|
||||
status: 'existing',
|
||||
publication: first.publication,
|
||||
});
|
||||
assert.equal(
|
||||
await reader.resolve({
|
||||
provider,
|
||||
model,
|
||||
priceRevision: 'price-1',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
|
||||
const second = await repository.publish(
|
||||
publish(provider, model, 'price-2', `publish-2-${suffix}`, 200_000),
|
||||
);
|
||||
const activated = await repository.transition(
|
||||
transition(
|
||||
provider,
|
||||
model,
|
||||
null,
|
||||
'activate',
|
||||
'price-1',
|
||||
`activate-1-${suffix}`,
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await reader.resolve({
|
||||
provider,
|
||||
model,
|
||||
priceRevision: 'price-1',
|
||||
})
|
||||
).catalogDigest,
|
||||
first.publication.entry.catalogDigest,
|
||||
);
|
||||
|
||||
const competitors = await Promise.allSettled([
|
||||
repository.transition(
|
||||
transition(
|
||||
provider,
|
||||
model,
|
||||
activated.head,
|
||||
'activate',
|
||||
'price-2',
|
||||
`activate-2-${suffix}`,
|
||||
),
|
||||
),
|
||||
repository.transition(
|
||||
transition(
|
||||
provider,
|
||||
model,
|
||||
activated.head,
|
||||
'deactivate',
|
||||
null,
|
||||
`deactivate-1-${suffix}`,
|
||||
),
|
||||
),
|
||||
]);
|
||||
assert.equal(
|
||||
competitors.filter((result) => result.status === 'fulfilled').length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
competitors.filter(
|
||||
(result) =>
|
||||
result.status === 'rejected' &&
|
||||
result.reason instanceof ModelPriceCatalogConflictError,
|
||||
).length,
|
||||
1,
|
||||
);
|
||||
|
||||
let current = await repository.findCurrent(provider, model);
|
||||
if (current.activePriceRevision !== 'price-2') {
|
||||
current = (
|
||||
await repository.transition(
|
||||
transition(
|
||||
provider,
|
||||
model,
|
||||
current,
|
||||
'activate',
|
||||
'price-2',
|
||||
`activate-2-after-race-${suffix}`,
|
||||
),
|
||||
)
|
||||
).head;
|
||||
}
|
||||
assert.equal(
|
||||
(
|
||||
await reader.resolve({
|
||||
provider,
|
||||
model,
|
||||
priceRevision: 'price-2',
|
||||
})
|
||||
).catalogDigest,
|
||||
second.publication.entry.catalogDigest,
|
||||
);
|
||||
assert.equal(
|
||||
await reader.resolve({
|
||||
provider,
|
||||
model,
|
||||
priceRevision: 'price-1',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
|
||||
current = (
|
||||
await repository.transition(
|
||||
transition(
|
||||
provider,
|
||||
model,
|
||||
current,
|
||||
'revoke',
|
||||
'price-1',
|
||||
`revoke-1-${suffix}`,
|
||||
),
|
||||
)
|
||||
).head;
|
||||
assert.equal(current.activePriceRevision, 'price-2');
|
||||
current = (
|
||||
await repository.transition(
|
||||
transition(
|
||||
provider,
|
||||
model,
|
||||
current,
|
||||
'revoke',
|
||||
'price-2',
|
||||
`revoke-2-${suffix}`,
|
||||
),
|
||||
)
|
||||
).head;
|
||||
assert.equal(current.activePriceRevision, null);
|
||||
assert.equal(
|
||||
await reader.resolve({
|
||||
provider,
|
||||
model,
|
||||
priceRevision: 'price-2',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
await assert.rejects(
|
||||
repository.transition(
|
||||
transition(
|
||||
provider,
|
||||
model,
|
||||
current,
|
||||
'activate',
|
||||
'price-2',
|
||||
`reactivate-2-${suffix}`,
|
||||
),
|
||||
),
|
||||
ModelPriceCatalogConflictError,
|
||||
);
|
||||
|
||||
const privileges = await Promise.all([
|
||||
runtimePool.query(
|
||||
`SELECT
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_publications',
|
||||
'SELECT'
|
||||
) AS publication_select,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_publications',
|
||||
'INSERT'
|
||||
) AS publication_insert,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_heads',
|
||||
'SELECT'
|
||||
) AS head_select,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_heads',
|
||||
'INSERT'
|
||||
) AS head_insert`,
|
||||
),
|
||||
adminPool.query(
|
||||
`SELECT
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_publications',
|
||||
'SELECT'
|
||||
) AND
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_publications',
|
||||
'INSERT'
|
||||
) AS publication_append,
|
||||
NOT has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_publications',
|
||||
'UPDATE'
|
||||
) AND NOT
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_publications',
|
||||
'DELETE'
|
||||
) AS publication_no_rewrite,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_heads',
|
||||
'SELECT'
|
||||
) AND
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_heads',
|
||||
'INSERT'
|
||||
) AS head_append,
|
||||
NOT has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_heads',
|
||||
'UPDATE'
|
||||
) AND NOT
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_heads',
|
||||
'DELETE'
|
||||
) AS head_no_rewrite`,
|
||||
),
|
||||
]);
|
||||
assert.deepEqual(privileges[0].rows[0], {
|
||||
publication_select: true,
|
||||
publication_insert: false,
|
||||
head_select: true,
|
||||
head_insert: false,
|
||||
});
|
||||
assert.deepEqual(privileges[1].rows[0], {
|
||||
publication_append: true,
|
||||
publication_no_rewrite: true,
|
||||
head_append: true,
|
||||
head_no_rewrite: true,
|
||||
});
|
||||
await assertSqlState(
|
||||
runtimePool.query(
|
||||
`INSERT INTO "ql3_ai"."model_price_catalog_publications"
|
||||
(provider)
|
||||
VALUES ('forbidden')`,
|
||||
),
|
||||
'42501',
|
||||
);
|
||||
await assertSqlState(
|
||||
adminPool.query(
|
||||
`UPDATE "ql3_ai"."model_price_catalog_heads"
|
||||
SET action = action
|
||||
WHERE provider = $1 AND model = $2`,
|
||||
[provider, model],
|
||||
),
|
||||
'42501',
|
||||
);
|
||||
} finally {
|
||||
await runtimePool.end();
|
||||
await adminPool.end();
|
||||
await migrationPool.end();
|
||||
}
|
||||
});
|
||||
|
||||
test('PostgreSQL atomically fences authorized catalog management and its ACL', async () => {
|
||||
const suffix = randomUUID();
|
||||
const provider = `authorized-${suffix}`;
|
||||
const model = 'managed-price-catalog';
|
||||
const now = Date.now();
|
||||
const migrationPool = pool(
|
||||
migrationConnectionString,
|
||||
'ql3-ai-price-authorization-migration-test',
|
||||
);
|
||||
const adminPool = pool(
|
||||
adminConnectionString,
|
||||
'ql3-ai-price-authorization-admin-test',
|
||||
);
|
||||
const runtimePool = pool(
|
||||
runtimeConnectionString,
|
||||
'ql3-ai-price-authorization-runtime-test',
|
||||
);
|
||||
const repository = new PostgresModelPriceCatalogRepository(adminPool);
|
||||
const service = createModelPriceCatalogManagementService(repository, {
|
||||
decisionMode: 'separation_of_duty',
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return createModelPriceCatalogPolicyDecision({
|
||||
effect: 'allow',
|
||||
revision: 'integration-platform-policy-1',
|
||||
reasons: ['catalog_operator'],
|
||||
});
|
||||
},
|
||||
},
|
||||
now: () => now,
|
||||
});
|
||||
const principal = (userId) => ({
|
||||
subject: { type: 'user', id: userId },
|
||||
authenticationId: `auth-${userId}`,
|
||||
authenticatedAtMs: now - 1_000,
|
||||
expiresAtMs: now + 60_000,
|
||||
assurance: 'multi_factor',
|
||||
});
|
||||
const publishRequest = {
|
||||
authorizationId: `authorize-publish-${suffix}`,
|
||||
requestId: `request-publish-${suffix}`,
|
||||
mutationId: `publish-${suffix}`,
|
||||
provider,
|
||||
model,
|
||||
principal: principal('integration-publisher'),
|
||||
priceRevision: 'price-1',
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: 150_000,
|
||||
outputMicrosPerMillionTokens: 600_000,
|
||||
};
|
||||
const activateRequest = {
|
||||
authorizationId: `authorize-activate-${suffix}`,
|
||||
requestId: `request-activate-${suffix}`,
|
||||
mutationId: `activate-${suffix}`,
|
||||
provider,
|
||||
model,
|
||||
principal: principal('integration-reviewer'),
|
||||
expectedGeneration: 0,
|
||||
expectedHeadDigest: null,
|
||||
action: 'activate',
|
||||
priceRevision: 'price-1',
|
||||
};
|
||||
|
||||
try {
|
||||
await runPostgresMigrations({ pool: migrationPool });
|
||||
await migratePostgresModelInvocationFeature(migrationPool);
|
||||
|
||||
const publication = await service.publish(publishRequest);
|
||||
assert.equal(publication.status, 'created');
|
||||
assert.deepEqual(await service.publish(publishRequest), {
|
||||
status: 'existing',
|
||||
publication: publication.publication,
|
||||
authorization: publication.authorization,
|
||||
});
|
||||
await assert.rejects(
|
||||
service.transition({
|
||||
...activateRequest,
|
||||
principal: principal('integration-publisher'),
|
||||
}),
|
||||
ModelPriceCatalogManagementSeparationOfDutyError,
|
||||
);
|
||||
const activation = await service.transition(activateRequest);
|
||||
assert.equal(activation.head.activePriceRevision, 'price-1');
|
||||
assert.equal(
|
||||
activation.authorization.principal.subject.id,
|
||||
'integration-reviewer',
|
||||
);
|
||||
assert.deepEqual(
|
||||
await repository.findAuthorization(activateRequest.authorizationId),
|
||||
activation.authorization,
|
||||
);
|
||||
|
||||
const runtimePrivileges = await runtimePool.query(
|
||||
`SELECT
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_authorizations',
|
||||
'SELECT'
|
||||
) AS authorization_select,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_authorizations',
|
||||
'INSERT'
|
||||
) AS authorization_insert`,
|
||||
);
|
||||
assert.deepEqual(runtimePrivileges.rows[0], {
|
||||
authorization_select: false,
|
||||
authorization_insert: false,
|
||||
});
|
||||
const adminPrivileges = await adminPool.query(
|
||||
`SELECT
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_authorizations',
|
||||
'SELECT'
|
||||
) AND
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_authorizations',
|
||||
'INSERT'
|
||||
) AS authorization_append,
|
||||
NOT has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_authorizations',
|
||||
'UPDATE'
|
||||
) AND NOT
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_price_catalog_authorizations',
|
||||
'DELETE'
|
||||
) AS authorization_no_rewrite`,
|
||||
);
|
||||
assert.deepEqual(adminPrivileges.rows[0], {
|
||||
authorization_append: true,
|
||||
authorization_no_rewrite: true,
|
||||
});
|
||||
await assertSqlState(
|
||||
runtimePool.query(
|
||||
`SELECT authorization_id
|
||||
FROM "ql3_ai"."model_price_catalog_authorizations"
|
||||
LIMIT 1`,
|
||||
),
|
||||
'42501',
|
||||
);
|
||||
await assertSqlState(
|
||||
adminPool.query(
|
||||
`UPDATE "ql3_ai"."model_price_catalog_authorizations"
|
||||
SET operation = operation
|
||||
WHERE authorization_id = $1`,
|
||||
[activation.authorization.authorizationId],
|
||||
),
|
||||
'42501',
|
||||
);
|
||||
} finally {
|
||||
await runtimePool.end();
|
||||
await adminPool.end();
|
||||
await migrationPool.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
ModelProviderCredentialManagementAuditAuthorizationFenceConflictError,
|
||||
ModelProviderCredentialManagementAuditUnavailableError,
|
||||
PostgresModelProviderCredentialManagementAuditQueryRepository,
|
||||
} = require('../dist/model-provider-credential/postgresModelProviderCredentialManagementAuditQuery.js');
|
||||
|
||||
const QUERY_ID = '219f7094-a853-4f3b-82ab-dfa08e6bd1c3';
|
||||
|
||||
function auditRow({
|
||||
eventId,
|
||||
requestId,
|
||||
operationId,
|
||||
occurredAtMs,
|
||||
authenticationId = 'authentication-1',
|
||||
}) {
|
||||
return {
|
||||
eventId,
|
||||
requestId,
|
||||
operationId,
|
||||
projectId: 'project-a',
|
||||
subjectType: 'user',
|
||||
subjectId: 'owner-a',
|
||||
authenticationId,
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_owner'],
|
||||
projectVersion: '3',
|
||||
bindingVersion: '7',
|
||||
occurredAtMs: String(occurredAtMs),
|
||||
};
|
||||
}
|
||||
|
||||
function authorized(overrides = {}) {
|
||||
return {
|
||||
query: {
|
||||
schemaVersion: 1,
|
||||
queryId: QUERY_ID,
|
||||
requestId: 'audit-request-1',
|
||||
projectId: 'project-a',
|
||||
limit: 2,
|
||||
...overrides,
|
||||
},
|
||||
actor: { type: 'user', id: 'owner-a' },
|
||||
fence: { projectVersion: 3, bindingVersion: 7 },
|
||||
audit: {
|
||||
eventId: QUERY_ID,
|
||||
requestId: 'audit-request-1',
|
||||
operationId: 'model_provider_credential.audit.list',
|
||||
projectId: 'project-a',
|
||||
subject: { type: 'user', id: 'owner-a' },
|
||||
authenticationId: 'authentication-1',
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_owner'],
|
||||
fence: { projectVersion: 3, bindingVersion: 7 },
|
||||
occurredAtMs: 2_000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(options = {}) {
|
||||
const state = {
|
||||
queries: [],
|
||||
accessAudit: options.accessAudit ?? null,
|
||||
accessAuditInserts: 0,
|
||||
commitResponseLost: false,
|
||||
};
|
||||
const records = [
|
||||
auditRow({
|
||||
eventId: '319f7094-a853-4f3b-82ab-dfa08e6bd1c4',
|
||||
requestId: 'request-revoke-1',
|
||||
operationId: 'model_provider_credential.revoke',
|
||||
occurredAtMs: 1_003,
|
||||
}),
|
||||
auditRow({
|
||||
eventId: '119f7094-a853-4f3b-82ab-dfa08e6bd1c2',
|
||||
requestId: 'request-bind-2',
|
||||
operationId: 'model_provider_credential.bind',
|
||||
occurredAtMs: 1_002,
|
||||
}),
|
||||
auditRow({
|
||||
eventId: '019f7094-a853-4f3b-82ab-dfa08e6bd1c1',
|
||||
requestId: 'request-bind-1',
|
||||
operationId: 'model_provider_credential.bind',
|
||||
occurredAtMs: 1_001,
|
||||
}),
|
||||
];
|
||||
const client = {
|
||||
async query(statement, values = []) {
|
||||
state.queries.push({ statement, values });
|
||||
if (statement.includes('FROM "ql3"."projects"')) {
|
||||
return {
|
||||
rows: [
|
||||
{ status: 'active', version: String(options.projectVersion ?? 3) },
|
||||
],
|
||||
};
|
||||
}
|
||||
if (statement.includes('FROM "ql3"."project_role_bindings"')) {
|
||||
return { rows: [{ state: 'active', version: '7' }] };
|
||||
}
|
||||
if (
|
||||
statement.includes('FROM "ql3"."security_audit_events"') &&
|
||||
statement.includes('WHERE event_id = $1')
|
||||
) {
|
||||
return { rows: state.accessAudit ? [state.accessAudit] : [] };
|
||||
}
|
||||
if (statement.includes('operation_id IN')) {
|
||||
return { rows: records };
|
||||
}
|
||||
if (statement.includes('INSERT INTO "ql3"."security_audit_events"')) {
|
||||
state.accessAuditInserts += 1;
|
||||
state.accessAudit = auditRow({
|
||||
eventId: values[0],
|
||||
requestId: values[1],
|
||||
operationId: values[2],
|
||||
occurredAtMs: values[11],
|
||||
});
|
||||
return { rows: [] };
|
||||
}
|
||||
if (
|
||||
statement === 'COMMIT' &&
|
||||
options.loseCommitResponse &&
|
||||
!state.commitResponseLost
|
||||
) {
|
||||
state.commitResponseLost = true;
|
||||
const error = new Error('injected commit response loss');
|
||||
error.code = 'ECONNRESET';
|
||||
throw error;
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
release() {},
|
||||
};
|
||||
return {
|
||||
state,
|
||||
repository:
|
||||
new PostgresModelProviderCredentialManagementAuditQueryRepository({
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
test('atomically audits and pages only content-free credential management events', async () => {
|
||||
const { repository, state } = fixture();
|
||||
const page = await repository.listAuthorized(authorized());
|
||||
assert.equal(page.projectId, 'project-a');
|
||||
assert.equal(page.records.length, 2);
|
||||
assert.deepEqual(page.nextCursor, {
|
||||
occurredAtMs: 1_002,
|
||||
eventId: '119f7094-a853-4f3b-82ab-dfa08e6bd1c2',
|
||||
});
|
||||
assert.equal(state.accessAuditInserts, 1);
|
||||
assert.match(
|
||||
state.queries.find(({ statement }) => statement.includes('operation_id IN'))
|
||||
.statement,
|
||||
/model_provider_credential\.bind[\s\S]+model_provider_credential\.revoke/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(page),
|
||||
/secretRef|bindingDigest|transitionDigest|authenticationId|openai/i,
|
||||
);
|
||||
});
|
||||
|
||||
test('converges an audit COMMIT response loss without duplicate access audit', async () => {
|
||||
const { repository, state } = fixture({ loseCommitResponse: true });
|
||||
await assert.rejects(
|
||||
repository.listAuthorized(authorized()),
|
||||
ModelProviderCredentialManagementAuditUnavailableError,
|
||||
);
|
||||
const replay = await repository.listAuthorized(authorized());
|
||||
assert.equal(replay.records.length, 2);
|
||||
assert.equal(state.accessAuditInserts, 1);
|
||||
assert.equal(state.commitResponseLost, true);
|
||||
});
|
||||
|
||||
test('rejects a stale Project fence before reading or auditing events', async () => {
|
||||
const { repository, state } = fixture({ projectVersion: 4 });
|
||||
await assert.rejects(
|
||||
repository.listAuthorized(authorized()),
|
||||
ModelProviderCredentialManagementAuditAuthorizationFenceConflictError,
|
||||
);
|
||||
assert.equal(
|
||||
state.queries.some(({ statement }) =>
|
||||
statement.includes('operation_id IN'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(state.accessAuditInserts, 0);
|
||||
});
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
PostgresModelProviderCredentialManagementIdentityLedgerConflictError,
|
||||
PostgresModelProviderCredentialManagementIdentityLedgerRepository,
|
||||
PostgresModelProviderCredentialManagementIdentityLedgerUnavailableError,
|
||||
PostgresModelProviderCredentialManagerNotReadyError,
|
||||
assertPostgresModelProviderCredentialManagerReady,
|
||||
} = require('../dist/model-provider-credential/postgresModelProviderCredentialManagementIdentityLedger.js');
|
||||
const {
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
postgresModelInvocationMigrationDefinition,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
|
||||
function snapshot(generation, overrides = {}) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
generation,
|
||||
digest: String.fromCharCode(64 + generation).repeat(43),
|
||||
issuer: 'https://identity.example.test/',
|
||||
audience: 'qinglong3-model-provider-credential-management',
|
||||
activeKeyIds: [`issuer-key-${generation}`],
|
||||
revokedKeyIds:
|
||||
generation === 1
|
||||
? []
|
||||
: Array.from(
|
||||
{ length: generation - 1 },
|
||||
(_, index) => `issuer-key-${index + 1}`,
|
||||
),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
let state;
|
||||
let loseCommitResponse = false;
|
||||
const queries = [];
|
||||
let releases = 0;
|
||||
const client = {
|
||||
async query(text, values = []) {
|
||||
queries.push({ text, values });
|
||||
if (text.startsWith('INSERT')) {
|
||||
state ??= {
|
||||
generation: values[1],
|
||||
digest: values[2],
|
||||
issuer: values[3],
|
||||
audience: values[4],
|
||||
activeKeyIds: JSON.parse(values[5]),
|
||||
revokedKeyIds: JSON.parse(values[6]),
|
||||
};
|
||||
} else if (text.startsWith('SELECT')) {
|
||||
return { rows: state ? [{ ...state }] : [] };
|
||||
} else if (text.startsWith('UPDATE')) {
|
||||
state = {
|
||||
...state,
|
||||
generation: values[1],
|
||||
digest: values[2],
|
||||
activeKeyIds: JSON.parse(values[3]),
|
||||
revokedKeyIds: JSON.parse(values[4]),
|
||||
};
|
||||
} else if (text === 'COMMIT' && loseCommitResponse) {
|
||||
loseCommitResponse = false;
|
||||
throw new Error('response lost after commit');
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
release() {
|
||||
releases += 1;
|
||||
},
|
||||
};
|
||||
return {
|
||||
repository:
|
||||
new PostgresModelProviderCredentialManagementIdentityLedgerRepository({
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
async query() {
|
||||
throw new Error('pool query must not bypass transaction client');
|
||||
},
|
||||
}),
|
||||
queries,
|
||||
state: () => state,
|
||||
releases: () => releases,
|
||||
loseNextCommitResponse() {
|
||||
loseCommitResponse = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('serializes exact identity observation and forward-only rotation', async () => {
|
||||
const value = fixture();
|
||||
await value.repository.observe(snapshot(1));
|
||||
await value.repository.observe(snapshot(1));
|
||||
await value.repository.observe(snapshot(2));
|
||||
assert.deepEqual(value.state(), {
|
||||
generation: 2,
|
||||
digest: 'B'.repeat(43),
|
||||
issuer: 'https://identity.example.test/',
|
||||
audience: 'qinglong3-model-provider-credential-management',
|
||||
activeKeyIds: ['issuer-key-2'],
|
||||
revokedKeyIds: ['issuer-key-1'],
|
||||
});
|
||||
const insert = value.queries.find(({ text }) => text.startsWith('INSERT'));
|
||||
assert.equal(insert.values[0], 'model-provider-credential-management');
|
||||
assert.match(insert.text, /clock_timestamp\(\)/);
|
||||
assert.equal(value.releases(), 3);
|
||||
});
|
||||
|
||||
test('rejects rollback, trust-domain drift and implicit key removal', async () => {
|
||||
const value = fixture();
|
||||
await value.repository.observe(snapshot(2));
|
||||
await assert.rejects(
|
||||
value.repository.observe(snapshot(1)),
|
||||
PostgresModelProviderCredentialManagementIdentityLedgerConflictError,
|
||||
);
|
||||
await assert.rejects(
|
||||
value.repository.observe(
|
||||
snapshot(3, { issuer: 'https://other.example.test/' }),
|
||||
),
|
||||
PostgresModelProviderCredentialManagementIdentityLedgerConflictError,
|
||||
);
|
||||
await assert.rejects(
|
||||
value.repository.observe(
|
||||
snapshot(3, { activeKeyIds: ['issuer-key-3'], revokedKeyIds: [] }),
|
||||
),
|
||||
PostgresModelProviderCredentialManagementIdentityLedgerConflictError,
|
||||
);
|
||||
});
|
||||
|
||||
test('converges a lost commit response without accepting malformed keys', async () => {
|
||||
const value = fixture();
|
||||
value.loseNextCommitResponse();
|
||||
await assert.rejects(
|
||||
value.repository.observe(snapshot(1)),
|
||||
PostgresModelProviderCredentialManagementIdentityLedgerUnavailableError,
|
||||
);
|
||||
await value.repository.observe(snapshot(1));
|
||||
assert.equal(value.state().generation, 1);
|
||||
await assert.rejects(
|
||||
value.repository.observe(
|
||||
snapshot(2, { activeKeyIds: ['issuer-key-2', 'issuer-key-2'] }),
|
||||
),
|
||||
TypeError,
|
||||
);
|
||||
});
|
||||
|
||||
test('readiness binds exact migration history and least-privilege primary authority', async () => {
|
||||
const history = postgresModelInvocationMigrationDefinition.migrations.map(
|
||||
({ id, checksum }) => ({
|
||||
migrationId: id,
|
||||
streamId: POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
dialect: 'postgresql',
|
||||
checksum,
|
||||
}),
|
||||
);
|
||||
const queries = [];
|
||||
const report = await assertPostgresModelProviderCredentialManagerReady({
|
||||
async query(text) {
|
||||
queries.push(text);
|
||||
return queries.length === 1
|
||||
? { rows: history }
|
||||
: {
|
||||
rows: [
|
||||
{
|
||||
currentUser: 'ql3_ai_credential_manager',
|
||||
writablePrimary: true,
|
||||
managerAuthority: true,
|
||||
leastPrivilege: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.equal(report.ready, true);
|
||||
assert.equal(report.migrationIds.at(-1).startsWith('pg-9017-'), true);
|
||||
assert.match(
|
||||
queries[1],
|
||||
/model_provider_credential_management_identity_keyset_ledger/,
|
||||
);
|
||||
assert.match(queries[1], /model_provider_credential_test_plans/);
|
||||
assert.match(queries[1], /model_provider_credential_test_quota_buckets/);
|
||||
assert.match(queries[1], /model_invocation_prompt_output_artifacts/);
|
||||
await assert.rejects(
|
||||
assertPostgresModelProviderCredentialManagerReady({
|
||||
async query() {
|
||||
return { rows: [] };
|
||||
},
|
||||
}),
|
||||
PostgresModelProviderCredentialManagerNotReadyError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { randomUUID } = require('node:crypto');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createModelProviderCredentialTestAllowlist,
|
||||
createModelProviderCredentialTestPlan,
|
||||
} = require('../dist/model-provider-credential/modelProviderCredentialTestConnection.js');
|
||||
const {
|
||||
MODEL_PROVIDER_CREDENTIAL_TEST_PLAN_OPERATION_ID,
|
||||
ModelProviderCredentialTestPlanAuthorizationFenceConflictError,
|
||||
ModelProviderCredentialTestPlanQuotaExceededError,
|
||||
ModelProviderCredentialTestPlanUnavailableError,
|
||||
PostgresModelProviderCredentialTestPlanRepository,
|
||||
} = require('../dist/model-provider-credential/postgresModelProviderCredentialTestConnection.js');
|
||||
|
||||
function plan(overrides = {}) {
|
||||
const endpoint = createModelProviderCredentialTestAllowlist({
|
||||
revision: 'catalog-v1',
|
||||
providers: [
|
||||
{
|
||||
provider: 'openai-compatible',
|
||||
adapter: 'openai-compatible',
|
||||
baseUrl: 'https://provider.example.test/v1/',
|
||||
revision: 'endpoint-v1',
|
||||
deadlineMs: 5_000,
|
||||
maxResponseBytes: 64 * 1_024,
|
||||
maxModels: 64,
|
||||
maxCostMicrousd: 0,
|
||||
retryLimit: 0,
|
||||
},
|
||||
],
|
||||
}).providers[0];
|
||||
return createModelProviderCredentialTestPlan({
|
||||
testId: randomUUID(),
|
||||
requestId: `request-${randomUUID()}`,
|
||||
projectId: 'project-a',
|
||||
provider: endpoint.provider,
|
||||
endpoint,
|
||||
requestedBy: { type: 'user', id: 'owner-a' },
|
||||
fence: { projectVersion: 1, bindingVersion: 1 },
|
||||
plannedAtMs: 100,
|
||||
expiresAtMs: 60_100,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function authorized(value) {
|
||||
return {
|
||||
plan: value,
|
||||
audit: {
|
||||
eventId: value.testId,
|
||||
requestId: value.requestId,
|
||||
operationId: MODEL_PROVIDER_CREDENTIAL_TEST_PLAN_OPERATION_ID,
|
||||
projectId: value.projectId,
|
||||
subject: value.requestedBy,
|
||||
authenticationId: 'strong-authentication-1',
|
||||
outcome: 'allowed',
|
||||
reasons: ['project_owner'],
|
||||
fence: value.fence,
|
||||
occurredAtMs: value.plannedAtMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fixture({ projectVersion = 1, bindingVersion = 1 } = {}) {
|
||||
const state = {
|
||||
plans: new Map(),
|
||||
audits: new Map(),
|
||||
quota: { consumed: 0, receipts: new Set() },
|
||||
};
|
||||
let snapshot;
|
||||
let loseCommit = false;
|
||||
const client = {
|
||||
async query(text, values = []) {
|
||||
if (text.startsWith('BEGIN')) {
|
||||
snapshot = structuredClone(state);
|
||||
return { rows: [] };
|
||||
}
|
||||
if (text.includes('pg_advisory_xact_lock')) return { rows: [{}] };
|
||||
if (text.includes('FROM "ql3"."projects"')) {
|
||||
return { rows: [{ status: 'active', version: projectVersion }] };
|
||||
}
|
||||
if (text.includes('FROM "ql3"."project_role_bindings"')) {
|
||||
return { rows: [{ state: 'active', version: bindingVersion }] };
|
||||
}
|
||||
if (
|
||||
text.includes('FROM "ql3_ai"."model_provider_credential_test_plans"')
|
||||
) {
|
||||
const [testId, requestedProjectId, requestId] = values;
|
||||
const rows = [...state.plans.values()]
|
||||
.filter(
|
||||
(stored) =>
|
||||
stored.testId === testId ||
|
||||
(stored.projectId === requestedProjectId &&
|
||||
stored.requestId === requestId),
|
||||
)
|
||||
.map((stored) => ({ planJson: stored }));
|
||||
return { rows };
|
||||
}
|
||||
if (text.includes('FROM "ql3"."security_audit_events"')) {
|
||||
const stored = state.audits.get(values[0]);
|
||||
return { rows: stored ? [stored] : [] };
|
||||
}
|
||||
if (text.includes('receipt_ids ? $3::text AS "hasReceipt"')) {
|
||||
return {
|
||||
rows:
|
||||
state.quota.consumed === 0
|
||||
? []
|
||||
: [{ hasReceipt: state.quota.receipts.has(values[2]) }],
|
||||
};
|
||||
}
|
||||
if (
|
||||
text.startsWith('WITH database_clock AS (') &&
|
||||
text.includes('INSERT INTO')
|
||||
) {
|
||||
const receipt = values[2];
|
||||
const limit = values[4];
|
||||
if (
|
||||
!state.quota.receipts.has(receipt) &&
|
||||
state.quota.consumed >= limit
|
||||
) {
|
||||
return { rows: [] };
|
||||
}
|
||||
if (!state.quota.receipts.has(receipt)) {
|
||||
state.quota.receipts.add(receipt);
|
||||
state.quota.consumed += 1;
|
||||
}
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
consumedCount: state.quota.consumed,
|
||||
resetAtMs: 60_100,
|
||||
observedAtMs: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (
|
||||
text.startsWith('WITH database_clock AS (') &&
|
||||
text.includes('SELECT consumed_count')
|
||||
) {
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
consumedCount: state.quota.consumed,
|
||||
resetAtMs: 60_100,
|
||||
observedAtMs: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (
|
||||
text.startsWith(
|
||||
'INSERT INTO "ql3_ai"."model_provider_credential_test_plans"',
|
||||
)
|
||||
) {
|
||||
state.plans.set(values[0], JSON.parse(values[20]));
|
||||
return { rows: [] };
|
||||
}
|
||||
if (text.startsWith('INSERT INTO "ql3"."security_audit_events"')) {
|
||||
state.audits.set(values[0], {
|
||||
eventId: values[0],
|
||||
requestId: values[1],
|
||||
operationId: values[2],
|
||||
projectId: values[3],
|
||||
subjectType: values[4],
|
||||
subjectId: values[5],
|
||||
authenticationId: values[6],
|
||||
outcome: values[7],
|
||||
reasons: JSON.parse(values[8]),
|
||||
projectVersion: values[9],
|
||||
bindingVersion: values[10],
|
||||
occurredAtMs: values[11],
|
||||
});
|
||||
return { rows: [] };
|
||||
}
|
||||
if (text === 'COMMIT') {
|
||||
snapshot = undefined;
|
||||
if (loseCommit) {
|
||||
loseCommit = false;
|
||||
const error = new Error('lost commit response');
|
||||
error.code = 'ECONNRESET';
|
||||
throw error;
|
||||
}
|
||||
return { rows: [] };
|
||||
}
|
||||
if (text === 'ROLLBACK') {
|
||||
if (snapshot) {
|
||||
state.plans = snapshot.plans;
|
||||
state.audits = snapshot.audits;
|
||||
state.quota = snapshot.quota;
|
||||
}
|
||||
snapshot = undefined;
|
||||
return { rows: [] };
|
||||
}
|
||||
throw new Error(`unexpected query: ${text}`);
|
||||
},
|
||||
release() {},
|
||||
};
|
||||
return {
|
||||
repository: new PostgresModelProviderCredentialTestPlanRepository(
|
||||
{
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
},
|
||||
{ quotaWindowMs: 60_000, quotaLimit: 2 },
|
||||
),
|
||||
state: () => state,
|
||||
loseNextCommitResponse() {
|
||||
loseCommit = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('atomically consumes quota, stores a plan and writes allowed audit', async () => {
|
||||
const value = fixture();
|
||||
const candidate = plan();
|
||||
const created = await value.repository.createAuthorized(
|
||||
authorized(candidate),
|
||||
);
|
||||
assert.equal(created.status, 'created');
|
||||
assert.equal(value.state().plans.size, 1);
|
||||
assert.equal(value.state().audits.size, 1);
|
||||
assert.equal(value.state().quota.consumed, 1);
|
||||
assert.equal(value.state().quota.receipts.has(candidate.testId), true);
|
||||
});
|
||||
|
||||
test('converges a COMMIT response loss without consuming quota twice', async () => {
|
||||
const value = fixture();
|
||||
const candidate = plan();
|
||||
value.loseNextCommitResponse();
|
||||
await assert.rejects(
|
||||
value.repository.createAuthorized(authorized(candidate)),
|
||||
ModelProviderCredentialTestPlanUnavailableError,
|
||||
);
|
||||
const replay = await value.repository.createAuthorized(authorized(candidate));
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.equal(value.state().quota.consumed, 1);
|
||||
});
|
||||
|
||||
test('replays the stored plan when API retry observes a later clock', async () => {
|
||||
const value = fixture();
|
||||
const candidate = plan();
|
||||
await value.repository.createAuthorized(authorized(candidate));
|
||||
const retried = plan({
|
||||
testId: candidate.testId,
|
||||
requestId: candidate.requestId,
|
||||
projectId: candidate.projectId,
|
||||
provider: candidate.provider,
|
||||
endpoint: candidate.endpoint,
|
||||
requestedBy: candidate.requestedBy,
|
||||
fence: candidate.fence,
|
||||
plannedAtMs: candidate.plannedAtMs + 25,
|
||||
expiresAtMs: candidate.expiresAtMs + 25,
|
||||
});
|
||||
const replay = await value.repository.createAuthorized(authorized(retried));
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.deepEqual(replay.plan, candidate);
|
||||
assert.equal(value.state().quota.consumed, 1);
|
||||
});
|
||||
|
||||
test('fails a stale fence and quota excess before creating another plan', async () => {
|
||||
const stale = fixture({ projectVersion: 2 });
|
||||
await assert.rejects(
|
||||
stale.repository.createAuthorized(authorized(plan())),
|
||||
ModelProviderCredentialTestPlanAuthorizationFenceConflictError,
|
||||
);
|
||||
assert.equal(stale.state().quota.consumed, 0);
|
||||
|
||||
const limited = fixture();
|
||||
await limited.repository.createAuthorized(authorized(plan()));
|
||||
await limited.repository.createAuthorized(authorized(plan()));
|
||||
await assert.rejects(
|
||||
limited.repository.createAuthorized(authorized(plan())),
|
||||
ModelProviderCredentialTestPlanQuotaExceededError,
|
||||
);
|
||||
assert.equal(limited.state().plans.size, 2);
|
||||
});
|
||||
@@ -0,0 +1,303 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { randomUUID } = require('node:crypto');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createModelProviderCredentialTestAllowlist,
|
||||
createModelProviderCredentialTestPlan,
|
||||
createModelProviderCredentialTestResult,
|
||||
} = require('../dist/model-provider-credential/modelProviderCredentialTestConnection.js');
|
||||
const {
|
||||
POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
postgresModelInvocationMigrationDefinition,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
const {
|
||||
ModelProviderCredentialTestExecutionRejectedError,
|
||||
ModelProviderCredentialTestExecutionUnavailableError,
|
||||
PostgresModelProviderCredentialTestExecutionRepository,
|
||||
PostgresModelProviderCredentialTesterNotReadyError,
|
||||
assertPostgresModelProviderCredentialTesterReady,
|
||||
} = require('../dist/model-provider-credential/postgresModelProviderCredentialTestConnection.js');
|
||||
|
||||
function contractFixture({ observedAtMs = 200 } = {}) {
|
||||
const allowlist = createModelProviderCredentialTestAllowlist({
|
||||
revision: 'catalog-v1',
|
||||
providers: [
|
||||
{
|
||||
provider: 'openai-compatible',
|
||||
adapter: 'openai-compatible',
|
||||
baseUrl: 'https://provider.example.test/v1/',
|
||||
revision: 'endpoint-v1',
|
||||
deadlineMs: 5_000,
|
||||
maxResponseBytes: 64 * 1_024,
|
||||
maxModels: 64,
|
||||
maxCostMicrousd: 0,
|
||||
retryLimit: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
const plan = createModelProviderCredentialTestPlan({
|
||||
testId: randomUUID(),
|
||||
requestId: `request-${randomUUID()}`,
|
||||
projectId: 'project-a',
|
||||
provider: 'openai-compatible',
|
||||
endpoint: allowlist.providers[0],
|
||||
requestedBy: { type: 'user', id: 'owner-a' },
|
||||
fence: { projectVersion: 1, bindingVersion: 1 },
|
||||
plannedAtMs: 100,
|
||||
expiresAtMs: 60_100,
|
||||
});
|
||||
const state = {
|
||||
executions: new Map(),
|
||||
results: new Map(),
|
||||
executionInserts: 0,
|
||||
resultInserts: 0,
|
||||
};
|
||||
let snapshot;
|
||||
let loseCommit = false;
|
||||
const client = {
|
||||
async query(text, values = []) {
|
||||
if (text.startsWith('BEGIN')) {
|
||||
snapshot = structuredClone(state);
|
||||
return { rows: [] };
|
||||
}
|
||||
if (text.includes('pg_advisory_xact_lock')) return { rows: [{}] };
|
||||
if (
|
||||
text.startsWith('WITH database_clock AS (') &&
|
||||
text.includes('model_provider_credential_test_plans')
|
||||
) {
|
||||
return values[0] === plan.testId
|
||||
? { rows: [{ planJson: plan, observedAtMs }] }
|
||||
: { rows: [] };
|
||||
}
|
||||
if (
|
||||
text.startsWith('SELECT execution.execution_json') &&
|
||||
text.includes('model_provider_credential_test_executions')
|
||||
) {
|
||||
const execution = state.executions.get(values[0]);
|
||||
if (!execution) return { rows: [] };
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
executionJson: execution,
|
||||
resultJson: state.results.get(execution.executionId) ?? null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (
|
||||
text.startsWith(
|
||||
'INSERT INTO "ql3_ai"."model_provider_credential_test_executions"',
|
||||
)
|
||||
) {
|
||||
const execution = JSON.parse(values[5]);
|
||||
state.executions.set(execution.testId, execution);
|
||||
state.executionInserts += 1;
|
||||
return { rows: [] };
|
||||
}
|
||||
if (
|
||||
text.startsWith(
|
||||
'INSERT INTO "ql3_ai"."model_provider_credential_test_results"',
|
||||
)
|
||||
) {
|
||||
const result = JSON.parse(values[8]);
|
||||
state.results.set(result.executionId, result);
|
||||
state.resultInserts += 1;
|
||||
return { rows: [] };
|
||||
}
|
||||
if (text === 'COMMIT') {
|
||||
snapshot = undefined;
|
||||
if (loseCommit) {
|
||||
loseCommit = false;
|
||||
const error = new Error('lost commit response');
|
||||
error.code = 'ECONNRESET';
|
||||
throw error;
|
||||
}
|
||||
return { rows: [] };
|
||||
}
|
||||
if (text === 'ROLLBACK') {
|
||||
if (snapshot) {
|
||||
state.executions = snapshot.executions;
|
||||
state.results = snapshot.results;
|
||||
state.executionInserts = snapshot.executionInserts;
|
||||
state.resultInserts = snapshot.resultInserts;
|
||||
}
|
||||
snapshot = undefined;
|
||||
return { rows: [] };
|
||||
}
|
||||
throw new Error(`unexpected query: ${text}`);
|
||||
},
|
||||
release() {},
|
||||
};
|
||||
return {
|
||||
allowlist,
|
||||
plan,
|
||||
repository: new PostgresModelProviderCredentialTestExecutionRepository({
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
}),
|
||||
state: () => state,
|
||||
loseNextCommitResponse() {
|
||||
loseCommit = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('commits an immutable execution intent before provider I/O is allowed', async () => {
|
||||
const value = contractFixture();
|
||||
const input = {
|
||||
executionId: randomUUID(),
|
||||
testId: value.plan.testId,
|
||||
allowlist: value.allowlist,
|
||||
};
|
||||
const created = await value.repository.beginExecution(input);
|
||||
assert.equal(created.status, 'created');
|
||||
assert.equal(created.result, null);
|
||||
assert.equal(value.state().executionInserts, 1);
|
||||
|
||||
const replay = await value.repository.beginExecution(input);
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.equal(replay.result, null);
|
||||
assert.equal(value.state().executionInserts, 1);
|
||||
});
|
||||
|
||||
test('returns existing after a begin COMMIT response loss so callers cannot reexecute', async () => {
|
||||
const value = contractFixture();
|
||||
const input = {
|
||||
executionId: randomUUID(),
|
||||
testId: value.plan.testId,
|
||||
allowlist: value.allowlist,
|
||||
};
|
||||
value.loseNextCommitResponse();
|
||||
await assert.rejects(
|
||||
value.repository.beginExecution(input),
|
||||
ModelProviderCredentialTestExecutionUnavailableError,
|
||||
);
|
||||
const recovery = await value.repository.beginExecution(input);
|
||||
assert.equal(recovery.status, 'existing');
|
||||
assert.equal(recovery.result, null);
|
||||
assert.equal(value.state().executionInserts, 1);
|
||||
});
|
||||
|
||||
test('rejects expired plans and exact allowlist drift before inserting intent', async () => {
|
||||
const expired = contractFixture({ observedAtMs: 60_100 });
|
||||
await assert.rejects(
|
||||
expired.repository.beginExecution({
|
||||
executionId: randomUUID(),
|
||||
testId: expired.plan.testId,
|
||||
allowlist: expired.allowlist,
|
||||
}),
|
||||
ModelProviderCredentialTestExecutionRejectedError,
|
||||
);
|
||||
assert.equal(expired.state().executionInserts, 0);
|
||||
|
||||
const drifted = contractFixture();
|
||||
const changedAllowlist = createModelProviderCredentialTestAllowlist({
|
||||
revision: 'catalog-v2',
|
||||
providers: [
|
||||
{
|
||||
provider: 'openai-compatible',
|
||||
adapter: 'openai-compatible',
|
||||
baseUrl: 'https://provider.example.test/v2/',
|
||||
revision: 'endpoint-v2',
|
||||
deadlineMs: 5_000,
|
||||
maxResponseBytes: 64 * 1_024,
|
||||
maxModels: 64,
|
||||
maxCostMicrousd: 0,
|
||||
retryLimit: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
await assert.rejects(
|
||||
drifted.repository.beginExecution({
|
||||
executionId: randomUUID(),
|
||||
testId: drifted.plan.testId,
|
||||
allowlist: changedAllowlist,
|
||||
}),
|
||||
ModelProviderCredentialTestExecutionRejectedError,
|
||||
);
|
||||
assert.equal(drifted.state().executionInserts, 0);
|
||||
});
|
||||
|
||||
test('recovers a result COMMIT response loss by exact durable replay', async () => {
|
||||
const value = contractFixture();
|
||||
const executionId = randomUUID();
|
||||
const started = await value.repository.beginExecution({
|
||||
executionId,
|
||||
testId: value.plan.testId,
|
||||
allowlist: value.allowlist,
|
||||
});
|
||||
const result = createModelProviderCredentialTestResult({
|
||||
executionId,
|
||||
testId: value.plan.testId,
|
||||
planDigest: started.plan.planDigest,
|
||||
outcome: 'reachable',
|
||||
modelCount: 3,
|
||||
durationMs: 40,
|
||||
completedAtMs: 240,
|
||||
});
|
||||
value.loseNextCommitResponse();
|
||||
await assert.rejects(
|
||||
value.repository.complete(result),
|
||||
ModelProviderCredentialTestExecutionUnavailableError,
|
||||
);
|
||||
const recovery = await value.repository.complete(result);
|
||||
assert.equal(recovery.status, 'existing');
|
||||
assert.deepEqual(recovery.result, result);
|
||||
assert.equal(value.state().resultInserts, 1);
|
||||
});
|
||||
|
||||
test('tester readiness freezes migration history and least privilege', async () => {
|
||||
const history = postgresModelInvocationMigrationDefinition.migrations.map(
|
||||
({ id, checksum }) => ({
|
||||
migrationId: id,
|
||||
streamId: POSTGRES_MODEL_INVOCATION_MIGRATION_STREAM_ID,
|
||||
dialect: 'postgresql',
|
||||
checksum,
|
||||
}),
|
||||
);
|
||||
const ready = await assertPostgresModelProviderCredentialTesterReady({
|
||||
async query(text) {
|
||||
if (text.includes('FROM "ql3_ai"."ai_schema_migrations"')) {
|
||||
return { rows: history };
|
||||
}
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
currentUser: 'ql3_ai_credential_tester',
|
||||
writablePrimary: true,
|
||||
testerAuthority: true,
|
||||
leastPrivilege: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.equal(ready.ready, true);
|
||||
assert.equal(
|
||||
ready.migrationIds.at(-1),
|
||||
'pg-9017-ai-plugin-package-prompt-product-authorization',
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
assertPostgresModelProviderCredentialTesterReady({
|
||||
async query(text) {
|
||||
if (text.includes('FROM "ql3_ai"."ai_schema_migrations"')) {
|
||||
return { rows: history };
|
||||
}
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
currentUser: 'ql3_ai_credential_tester',
|
||||
writablePrimary: true,
|
||||
testerAuthority: true,
|
||||
leastPrivilege: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
}),
|
||||
PostgresModelProviderCredentialTesterNotReadyError,
|
||||
);
|
||||
});
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { generateKeyPairSync, randomUUID } = require('node:crypto');
|
||||
const { createRequire } = require('node:module');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createInitialPluginPackageAutomationPublication,
|
||||
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
|
||||
const {
|
||||
createPluginPackagePublisherTrustSnapshot,
|
||||
} = require('@qinglong/runtime-core/plugin-package-publisher-trust');
|
||||
const {
|
||||
activateInstall,
|
||||
pluginPackageTaskReconciliationFixture,
|
||||
publisherProvenanceInstallRepository,
|
||||
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
|
||||
const {
|
||||
migratePostgresModelInvocationFeature,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
const {
|
||||
bootstrapPostgresPluginPackagePromptApplication,
|
||||
} = require('../dist/prompt/postgresPluginPackagePromptApplication.js');
|
||||
|
||||
const migrationConnectionString =
|
||||
process.env.QL3_TEST_POSTGRES_MIGRATION_URL ??
|
||||
process.env.QL3_TEST_POSTGRES_URL;
|
||||
const runtimeConnectionString =
|
||||
process.env.QL3_TEST_POSTGRES_RUNTIME_URL ?? migrationConnectionString;
|
||||
const packageExecutorConnectionString =
|
||||
process.env.QL3_TEST_POSTGRES_PACKAGE_EXECUTOR_URL ??
|
||||
migrationConnectionString;
|
||||
|
||||
if (!migrationConnectionString) {
|
||||
test('PostgreSQL Package Prompt integration requires QL3_TEST_POSTGRES_URL', {
|
||||
skip: true,
|
||||
});
|
||||
} else {
|
||||
const clusterRequire = createRequire(
|
||||
path.resolve(__dirname, '../../ql3-cluster-postgres/package.json'),
|
||||
);
|
||||
const { Pool } = clusterRequire('pg');
|
||||
const {
|
||||
runPostgresMigrations,
|
||||
} = require('../../ql3-cluster-postgres/dist/migration/migration.js');
|
||||
const {
|
||||
PostgresPluginPackageInstallRepository,
|
||||
} = require('../../ql3-cluster-postgres/dist/plugin-package/installation/pluginPackageInstallRepository.js');
|
||||
const {
|
||||
PostgresPluginPackagePublisherProvenanceRepository,
|
||||
} = require('../../ql3-cluster-postgres/dist/plugin-package/publisher/pluginPackagePublisherProvenanceRepository.js');
|
||||
const {
|
||||
PostgresPluginPackagePublisherTrustAuthorityRepository,
|
||||
} = require('../../ql3-cluster-postgres/dist/plugin-package/publisher/pluginPackagePublisherTrustAuthorityRepository.js');
|
||||
const {
|
||||
PostgresPluginPackageMaterializedRevisionRepository,
|
||||
} = require('../../ql3-cluster-postgres/dist/plugin-package/installation/pluginPackageMaterializedRevisionRepository.js');
|
||||
const {
|
||||
PostgresPluginPackageAutomationPublicationRepository,
|
||||
} = require('../../ql3-cluster-postgres/dist/plugin-package/publication/pluginPackageAutomationPublicationRepository.js');
|
||||
|
||||
function pool(connectionString, applicationName) {
|
||||
return new Pool({
|
||||
connectionString,
|
||||
ssl: false,
|
||||
max: 4,
|
||||
application_name: applicationName,
|
||||
});
|
||||
}
|
||||
|
||||
test('PostgreSQL executes and replays one content-free Package Prompt', async () => {
|
||||
const suffix = `${process.pid}-${Date.now()}`;
|
||||
const credentialId = `prompt-${suffix}`;
|
||||
const fixture = pluginPackageTaskReconciliationFixture(
|
||||
`ai-prompt-${suffix}`,
|
||||
{
|
||||
profile: 'cluster-control',
|
||||
prompts: [
|
||||
{
|
||||
schema: 'qinglong/plugin-package-prompt-resource@v1',
|
||||
id: 'summary',
|
||||
name: 'Summary',
|
||||
template: 'Summarize {{subject}} for {{audience}}.',
|
||||
parameters: [
|
||||
{ name: 'audience', required: false },
|
||||
{ name: 'subject', required: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
const publication = createInitialPluginPackageAutomationPublication(
|
||||
fixture.revision,
|
||||
fixture.registry,
|
||||
20_000,
|
||||
);
|
||||
const migrationPool = pool(
|
||||
migrationConnectionString,
|
||||
'ql3-ai-prompt-migration-test',
|
||||
);
|
||||
const executorPool = pool(
|
||||
packageExecutorConnectionString,
|
||||
'ql3-ai-prompt-package-executor-test',
|
||||
);
|
||||
const runtimePool = pool(
|
||||
runtimeConnectionString,
|
||||
'ql3-ai-prompt-runtime-test',
|
||||
);
|
||||
let application;
|
||||
try {
|
||||
await runPostgresMigrations({ pool: migrationPool });
|
||||
await migratePostgresModelInvocationFeature(migrationPool);
|
||||
const keyPair = generateKeyPairSync('ed25519');
|
||||
const trust = createPluginPackagePublisherTrustSnapshot([
|
||||
{
|
||||
publisher: 'packages.contract.qinglong.dev',
|
||||
keyId: 'contract-key-1',
|
||||
publicKeyPem: keyPair.publicKey.export({
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
}),
|
||||
notBeforeMs: 0,
|
||||
notAfterMs: 100_000,
|
||||
},
|
||||
]);
|
||||
await new PostgresPluginPackagePublisherTrustAuthorityRepository(
|
||||
migrationPool,
|
||||
).observeSnapshot({
|
||||
authorityId: 'cluster',
|
||||
observedBy: `ai-prompt-${suffix}`,
|
||||
observedAtMs: 1,
|
||||
snapshot: trust,
|
||||
});
|
||||
await migrationPool.query(
|
||||
`INSERT INTO "ql3"."projects" (
|
||||
id, name, slug, status, version, created_at_ms, updated_at_ms
|
||||
) VALUES ($1, $1, $1, 'active', 1, 1, 1)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
[fixture.projectId],
|
||||
);
|
||||
await migrationPool.query(
|
||||
`INSERT INTO "ql3"."identity_subjects" (
|
||||
subject_type, subject_id, status, version, created_at_ms, updated_at_ms
|
||||
) VALUES ('user', 'user-a', 'active', 1, 1, 1)
|
||||
ON CONFLICT (subject_type, subject_id) DO UPDATE
|
||||
SET status = 'active', version = "ql3"."identity_subjects".version + 1,
|
||||
updated_at_ms = 1`,
|
||||
);
|
||||
await migrationPool.query(
|
||||
`INSERT INTO "ql3"."api_credentials" (
|
||||
credential_id, version, state, subject_type, subject_id,
|
||||
pepper_key_id, secret_digest, created_at_ms, not_before_at_ms,
|
||||
expires_at_ms
|
||||
) VALUES ($1, 1, 'active', 'user', 'user-a', 'integration-v1',
|
||||
$2, 1, 1, 9999999999999)`,
|
||||
[credentialId, 'a'.repeat(64)],
|
||||
);
|
||||
await migrationPool.query(
|
||||
`INSERT INTO "ql3"."project_role_bindings" (
|
||||
project_id, subject_type, subject_id, version, state, role,
|
||||
mutation_id, changed_by_type, changed_by_id, created_at_ms
|
||||
) VALUES ($1, 'user', 'user-a', 1, 'active', 'operator',
|
||||
$2, 'system', 'integration-test', 1)
|
||||
ON CONFLICT (project_id, subject_type, subject_id, version)
|
||||
DO NOTHING`,
|
||||
[fixture.projectId, `ai-prompt-user-${suffix}`],
|
||||
);
|
||||
|
||||
await activateInstall(
|
||||
publisherProvenanceInstallRepository(
|
||||
new PostgresPluginPackageInstallRepository(executorPool),
|
||||
new PostgresPluginPackagePublisherProvenanceRepository(executorPool),
|
||||
),
|
||||
fixture,
|
||||
);
|
||||
await new PostgresPluginPackageMaterializedRevisionRepository(
|
||||
executorPool,
|
||||
fixture.registry,
|
||||
).publish(fixture.revision);
|
||||
await new PostgresPluginPackageAutomationPublicationRepository(
|
||||
executorPool,
|
||||
).publish(publication);
|
||||
|
||||
const privileges = await runtimePool.query(
|
||||
`SELECT
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_prompt_admissions', 'SELECT,INSERT'
|
||||
) AS admission_write,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_prompt_admissions', 'UPDATE'
|
||||
) AS admission_update,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_prompt_finalizations', 'SELECT,INSERT'
|
||||
) AS finalization_write,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_prompt_finalizations', 'DELETE'
|
||||
) AS finalization_delete,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_prompt_output_artifacts', 'SELECT,INSERT'
|
||||
) AS output_artifact_write,
|
||||
has_table_privilege(
|
||||
current_user,
|
||||
'ql3_ai.model_invocation_prompt_output_artifacts', 'UPDATE,DELETE'
|
||||
) AS output_artifact_mutation,
|
||||
has_function_privilege(
|
||||
current_user,
|
||||
'ql3_ai.plugin_package_prompt_admission_snapshot(varchar,varchar,character,varchar,varchar,integer,integer)',
|
||||
'EXECUTE'
|
||||
) AS snapshot_execute`,
|
||||
);
|
||||
assert.deepEqual(privileges.rows[0], {
|
||||
admission_write: true,
|
||||
admission_update: false,
|
||||
finalization_write: true,
|
||||
finalization_delete: false,
|
||||
output_artifact_write: true,
|
||||
output_artifact_mutation: false,
|
||||
snapshot_execute: true,
|
||||
});
|
||||
|
||||
let providerCalls = 0;
|
||||
application = await bootstrapPostgresPluginPackagePromptApplication({
|
||||
enabled: true,
|
||||
async openDatabase() {
|
||||
return { pool: runtimePool, async close() {} };
|
||||
},
|
||||
async loadProviders() {
|
||||
return {
|
||||
providers: [
|
||||
{
|
||||
type: 'openai-compatible',
|
||||
async listModels() {
|
||||
return [{ id: 'vendor/model-a' }];
|
||||
},
|
||||
async generate() {
|
||||
providerCalls += 1;
|
||||
return {
|
||||
provider: 'openai-compatible',
|
||||
model: 'vendor/model-a',
|
||||
text: 'one live PostgreSQL response',
|
||||
finishReason: 'stop',
|
||||
usage: {
|
||||
inputTokens: 7,
|
||||
outputTokens: 3,
|
||||
totalTokens: 10,
|
||||
},
|
||||
};
|
||||
},
|
||||
async *stream() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
return {
|
||||
revision: 'policy-1',
|
||||
allowedProviders: ['openai-compatible'],
|
||||
allowedModels: ['vendor/model-a'],
|
||||
maxInputBytes: 4096,
|
||||
maxOutputBytes: 4096,
|
||||
maxOutputTokens: 512,
|
||||
maxTotalTokens: 1024,
|
||||
maxCostMicros: null,
|
||||
priceRevision: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
async audit() {},
|
||||
promptOutputKeys: {
|
||||
async active() {
|
||||
return {
|
||||
keyId: 'cluster-prompt-output-key-1',
|
||||
key: Buffer.alloc(32, 11),
|
||||
};
|
||||
},
|
||||
async resolve(keyId) {
|
||||
return keyId === 'cluster-prompt-output-key-1'
|
||||
? { keyId, key: Buffer.alloc(32, 11) }
|
||||
: null;
|
||||
},
|
||||
},
|
||||
promptOutputRead: {
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
return { effect: 'allow' };
|
||||
},
|
||||
},
|
||||
},
|
||||
maxConcurrent: 1,
|
||||
recoveryLimit: 8,
|
||||
now: () => 31_000,
|
||||
});
|
||||
assert.equal(application.status, 'active');
|
||||
assert.equal(application.readiness.currentUser, 'ql3_runtime');
|
||||
const executor = application.promptExecutions;
|
||||
const input = {
|
||||
projectId: publication.target.projectId,
|
||||
packageName: publication.target.packageName,
|
||||
promptId: 'summary',
|
||||
requestId: `prompt-request-${suffix}`,
|
||||
traceId: `prompt-trace-${suffix}`,
|
||||
auditEventId: randomUUID(),
|
||||
principal: {
|
||||
subject: { type: 'user', id: 'user-a' },
|
||||
authenticationId: `api_credential:${credentialId}:1`,
|
||||
authenticatedAtMs: 1,
|
||||
expiresAtMs: 9999999999999,
|
||||
assurance: 'single_factor',
|
||||
},
|
||||
policyFence: { projectVersion: 1, bindingVersion: 1 },
|
||||
parameters: { subject: 'private PostgreSQL input' },
|
||||
provider: 'openai-compatible',
|
||||
model: 'vendor/model-a',
|
||||
maxOutputTokens: 512,
|
||||
temperature: 0.2,
|
||||
plannedAtMs: 30_000,
|
||||
deadlineAtMs: 90_000,
|
||||
};
|
||||
const first = await executor.execute(input);
|
||||
assert.equal(first.status, 'executed');
|
||||
assert.equal(first.result.text, 'one live PostgreSQL response');
|
||||
assert.equal(first.finalization.runStatus, 'succeeded');
|
||||
const replay = await executor.execute(input);
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.equal(replay.result, null);
|
||||
assert.deepEqual(replay.admission, first.admission);
|
||||
assert.deepEqual(replay.finalization, first.finalization);
|
||||
assert.equal(providerCalls, 1);
|
||||
|
||||
const durableInput = {
|
||||
...input,
|
||||
requestId: `prompt-request-durable-${suffix}`,
|
||||
traceId: `prompt-trace-durable-${suffix}`,
|
||||
auditEventId: randomUUID(),
|
||||
output: {
|
||||
mode: 'durable_artifact',
|
||||
retentionPolicy: {
|
||||
revision: 'cluster-output-v1',
|
||||
retentionMs: 86_400_000,
|
||||
},
|
||||
},
|
||||
};
|
||||
const durableFirst = await executor.execute(durableInput);
|
||||
assert.equal(durableFirst.status, 'executed');
|
||||
assert.equal(durableFirst.result.text, 'one live PostgreSQL response');
|
||||
assert.equal(
|
||||
durableFirst.outputArtifact.artifactId.startsWith('pao:'),
|
||||
true,
|
||||
);
|
||||
const durableReplay = await executor.execute(durableInput);
|
||||
assert.equal(durableReplay.status, 'existing');
|
||||
assert.equal(durableReplay.result, null);
|
||||
assert.deepEqual(
|
||||
durableReplay.outputArtifact,
|
||||
durableFirst.outputArtifact,
|
||||
);
|
||||
assert.equal(providerCalls, 2);
|
||||
assert.ok(application.promptExecutionOutputs);
|
||||
const recoveredOutput = await application.promptExecutionOutputs.read({
|
||||
principal: input.principal,
|
||||
projectId: durableInput.projectId,
|
||||
packageName: durableInput.packageName,
|
||||
promptId: durableInput.promptId,
|
||||
executionRequestId: durableInput.requestId,
|
||||
});
|
||||
assert.equal(recoveredOutput.status, 'available');
|
||||
assert.equal(recoveredOutput.result.text, 'one live PostgreSQL response');
|
||||
assert.deepEqual(recoveredOutput.reference, durableFirst.outputArtifact);
|
||||
assert.equal(
|
||||
(
|
||||
await application.promptExecutionOutputs.read({
|
||||
principal: input.principal,
|
||||
projectId: durableInput.projectId,
|
||||
packageName: durableInput.packageName,
|
||||
promptId: 'cross-target',
|
||||
executionRequestId: durableInput.requestId,
|
||||
})
|
||||
).status,
|
||||
'not_found',
|
||||
);
|
||||
const outputEvidence = await runtimePool.query(
|
||||
`SELECT artifact.artifact_json::text AS "artifactJson",
|
||||
step.output_ref AS "outputRef"
|
||||
FROM "ql3_ai"."model_invocation_prompt_output_artifacts" AS artifact
|
||||
JOIN "ql3"."step_runs" AS step
|
||||
ON step.id = artifact.step_run_id
|
||||
AND step.run_id = artifact.run_id
|
||||
WHERE artifact.invocation_id = $1`,
|
||||
[durableFirst.admission.invocationId],
|
||||
);
|
||||
assert.equal(outputEvidence.rows.length, 1);
|
||||
assert.equal(
|
||||
outputEvidence.rows[0].outputRef,
|
||||
durableFirst.outputArtifact.artifactId,
|
||||
);
|
||||
assert.equal(
|
||||
outputEvidence.rows[0].artifactJson.includes(
|
||||
'one live PostgreSQL response',
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
const durable = await runtimePool.query(
|
||||
`SELECT admission.plan_json, admission.receipt_json,
|
||||
finalization.receipt_json, run.status, run.version,
|
||||
run.event_sequence AS "eventSequence"
|
||||
FROM "ql3_ai"."model_invocation_prompt_admissions" AS admission
|
||||
JOIN "ql3_ai"."model_invocation_prompt_finalizations"
|
||||
AS finalization USING (request_id)
|
||||
JOIN "ql3"."runs" AS run ON run.id = admission.run_id
|
||||
WHERE admission.request_id = $1`,
|
||||
[input.requestId],
|
||||
);
|
||||
assert.equal(durable.rows.length, 1);
|
||||
assert.deepEqual(
|
||||
{
|
||||
status: durable.rows[0].status,
|
||||
version: durable.rows[0].version,
|
||||
eventSequence: durable.rows[0].eventSequence,
|
||||
},
|
||||
{ status: 'succeeded', version: 5, eventSequence: 5 },
|
||||
);
|
||||
const durableJson = JSON.stringify(durable.rows[0]);
|
||||
assert.equal(durableJson.includes('private PostgreSQL input'), false);
|
||||
assert.equal(durableJson.includes('one live PostgreSQL response'), false);
|
||||
} finally {
|
||||
await application?.stop();
|
||||
await Promise.all([
|
||||
runtimePool.end(),
|
||||
executorPool.end(),
|
||||
migrationPool.end(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
postgresModelInvocationMigrationDefinition,
|
||||
} = require('@qinglong/ai/model-invocation-migration');
|
||||
const {
|
||||
PostgresPluginPackagePromptApplicationUnavailableError,
|
||||
PostgresPluginPackagePromptExecutionService,
|
||||
assertPostgresPluginPackagePromptApplicationReady,
|
||||
bootstrapPostgresPluginPackagePromptApplication,
|
||||
} = require('../dist/prompt/postgresPluginPackagePromptApplication.js');
|
||||
const {
|
||||
createInitialPluginPackageAutomationPublication,
|
||||
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
|
||||
const {
|
||||
pluginPackageTaskReconciliationFixture,
|
||||
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
|
||||
|
||||
function providers(onDispose) {
|
||||
return {
|
||||
providers: [
|
||||
{
|
||||
type: 'openai-compatible',
|
||||
async listModels() {
|
||||
return [{ id: 'test/model-a' }];
|
||||
},
|
||||
async generate() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
async *stream() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
dispose: onDispose,
|
||||
};
|
||||
}
|
||||
|
||||
function recoveryPool() {
|
||||
return {
|
||||
async query(sql) {
|
||||
if (sql.includes('statement_timestamp()')) {
|
||||
return { rows: [{ observedAtMs: '1000' }], rowCount: 1 };
|
||||
}
|
||||
if (sql.includes('model_invocation_starts')) {
|
||||
return { rows: [], rowCount: 0 };
|
||||
}
|
||||
throw new Error(`unexpected SQL: ${sql}`);
|
||||
},
|
||||
async connect() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function readiness() {
|
||||
return Object.freeze({
|
||||
schema: 'ql3_ai',
|
||||
migrationStreamId: postgresModelInvocationMigrationDefinition.id,
|
||||
migrationCount:
|
||||
postgresModelInvocationMigrationDefinition.migrations.length,
|
||||
currentUser: 'ql3_runtime',
|
||||
runtimeAuthority: true,
|
||||
appendOnly: true,
|
||||
});
|
||||
}
|
||||
|
||||
function promptProductFixture() {
|
||||
const source = pluginPackageTaskReconciliationFixture('prompt-product', {
|
||||
profile: 'cluster-control',
|
||||
prompts: [
|
||||
{
|
||||
schema: 'qinglong/plugin-package-prompt-resource@v1',
|
||||
id: 'summary',
|
||||
name: 'Summary',
|
||||
template: 'Summarize {{subject}}.',
|
||||
parameters: [{ name: 'subject', required: true }],
|
||||
},
|
||||
],
|
||||
});
|
||||
return createInitialPluginPackageAutomationPublication(
|
||||
source.revision,
|
||||
source.registry,
|
||||
20_000,
|
||||
);
|
||||
}
|
||||
|
||||
test('Cluster Prompt product derives current publication and guards admission atomically', async () => {
|
||||
const publication = promptProductFixture();
|
||||
const productQueries = [];
|
||||
const pool = {
|
||||
async query(sql) {
|
||||
productQueries.push(sql);
|
||||
if (sql.includes('model_invocation_prompt_admissions'))
|
||||
return { rows: [] };
|
||||
if (sql.includes('plugin_package_automation_publication_heads')) {
|
||||
return { rows: [{ publicationJson: publication }] };
|
||||
}
|
||||
throw new Error(`unexpected product query: ${sql}`);
|
||||
},
|
||||
async connect() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
};
|
||||
const transactionQueries = [];
|
||||
let credentialState = 'active';
|
||||
const client = {
|
||||
async query(sql, parameters = []) {
|
||||
transactionQueries.push({ sql, parameters });
|
||||
if (sql.includes('plugin_package_prompt_authorize_admission')) {
|
||||
return { rows: [{ authorized: credentialState === 'active' }] };
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
};
|
||||
let executorInput;
|
||||
let providerCalls = 0;
|
||||
const service = new PostgresPluginPackagePromptExecutionService(
|
||||
pool,
|
||||
(guard) => ({
|
||||
async execute(input) {
|
||||
executorInput = input;
|
||||
await guard.confirm({
|
||||
client,
|
||||
replay: false,
|
||||
plan: {
|
||||
requestId: input.requestId,
|
||||
plannedAtMs: input.plannedAtMs,
|
||||
requestedBySubject: input.requestedBySubject,
|
||||
policyFence: input.policyFence,
|
||||
target: {
|
||||
...publication.target,
|
||||
publicationDigest: publication.publicationDigest,
|
||||
promptId: input.promptId,
|
||||
},
|
||||
},
|
||||
});
|
||||
providerCalls += 1;
|
||||
return {
|
||||
status: 'executed',
|
||||
admission: { requestId: input.requestId },
|
||||
finalization: { runStatus: 'succeeded' },
|
||||
result: { text: 'live result' },
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
const command = {
|
||||
projectId: publication.target.projectId,
|
||||
packageName: publication.target.packageName,
|
||||
promptId: 'summary',
|
||||
requestId: 'prompt-request-1',
|
||||
traceId: 'prompt-trace-1',
|
||||
auditEventId: '00000000-0000-4000-8000-000000000001',
|
||||
principal: {
|
||||
subject: { type: 'user', id: 'user-a' },
|
||||
authenticationId: 'api_credential:prompt-credential:1',
|
||||
authenticatedAtMs: 1,
|
||||
expiresAtMs: 100_000,
|
||||
assurance: 'single_factor',
|
||||
},
|
||||
policyFence: { projectVersion: 1, bindingVersion: 1 },
|
||||
parameters: { subject: 'private input' },
|
||||
provider: 'openai-compatible',
|
||||
model: 'vendor/model-a',
|
||||
maxOutputTokens: 128,
|
||||
plannedAtMs: 30_000,
|
||||
deadlineAtMs: 90_000,
|
||||
};
|
||||
const result = await service.execute(command);
|
||||
assert.equal(result.result.text, 'live result');
|
||||
assert.equal(
|
||||
executorInput.expectedPublicationDigest,
|
||||
publication.publicationDigest,
|
||||
);
|
||||
assert.equal('publicationDigest' in command, false);
|
||||
assert.equal(
|
||||
productQueries.some((sql) =>
|
||||
sql.includes('plugin_package_automation_publication_heads'),
|
||||
),
|
||||
true,
|
||||
);
|
||||
const authorization = transactionQueries.find(({ sql }) =>
|
||||
sql.includes('plugin_package_prompt_authorize_admission'),
|
||||
);
|
||||
assert.equal(authorization.parameters[7], command.auditEventId);
|
||||
assert.equal(authorization.parameters[8], command.requestId);
|
||||
assert.equal(authorization.parameters[10], false);
|
||||
assert.equal(authorization.parameters.includes('private input'), false);
|
||||
assert.equal(providerCalls, 1);
|
||||
credentialState = 'revoked';
|
||||
await assert.rejects(
|
||||
service.execute({
|
||||
...command,
|
||||
requestId: 'prompt-request-revoked',
|
||||
auditEventId: '00000000-0000-4000-8000-000000000002',
|
||||
}),
|
||||
(error) => error?.code === 'PLUGIN_PACKAGE_PROMPT_ADMISSION_NOT_ALLOWED',
|
||||
);
|
||||
assert.equal(providerCalls, 1);
|
||||
});
|
||||
|
||||
test('disabled Cluster Package Prompt application is database/provider loader-free', async () => {
|
||||
const audits = [];
|
||||
const result = await bootstrapPostgresPluginPackagePromptApplication({
|
||||
async audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
});
|
||||
assert.equal(result.status, 'disabled');
|
||||
assert.equal(result.profile, 'cluster');
|
||||
assert.equal(await result.stop(), 'stopped');
|
||||
assert.deepEqual(audits, [{ profile: 'cluster', state: 'disabled' }]);
|
||||
});
|
||||
|
||||
test('PostgreSQL readiness binds the exact migration stream and runtime authority', async () => {
|
||||
let queries = 0;
|
||||
const pool = {
|
||||
async query(sql) {
|
||||
queries += 1;
|
||||
if (sql.includes('ai_schema_migrations')) {
|
||||
return {
|
||||
rows: postgresModelInvocationMigrationDefinition.migrations.map(
|
||||
(migration) => ({
|
||||
migrationId: migration.id,
|
||||
streamId: postgresModelInvocationMigrationDefinition.id,
|
||||
dialect: postgresModelInvocationMigrationDefinition.dialect,
|
||||
checksum: migration.checksum,
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
currentUser: 'ql3_runtime',
|
||||
runtimeAuthority: true,
|
||||
schemaUsage: true,
|
||||
invocationAppendOnly: true,
|
||||
promptAppendOnly: true,
|
||||
catalogReadable: true,
|
||||
promptSnapshotExecutable: true,
|
||||
promptAuthorizationExecutable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
assert.deepEqual(
|
||||
await assertPostgresPluginPackagePromptApplicationReady(pool),
|
||||
readiness(),
|
||||
);
|
||||
assert.equal(queries, 2);
|
||||
});
|
||||
|
||||
test('active Cluster Package Prompt application recovers before provider load and owns shutdown', async () => {
|
||||
const events = [];
|
||||
let databaseCloses = 0;
|
||||
let providerLoads = 0;
|
||||
let providerDisposals = 0;
|
||||
const result = await bootstrapPostgresPluginPackagePromptApplication({
|
||||
enabled: true,
|
||||
async openDatabase() {
|
||||
events.push('database_open');
|
||||
return {
|
||||
pool: recoveryPool(),
|
||||
async close() {
|
||||
databaseCloses += 1;
|
||||
events.push('database_close');
|
||||
},
|
||||
};
|
||||
},
|
||||
async assertReady() {
|
||||
events.push('readiness');
|
||||
return readiness();
|
||||
},
|
||||
async loadProviders() {
|
||||
providerLoads += 1;
|
||||
events.push('providers_load');
|
||||
return providers(async () => {
|
||||
providerDisposals += 1;
|
||||
events.push('providers_dispose');
|
||||
});
|
||||
},
|
||||
async audit(record) {
|
||||
events.push(record.state);
|
||||
},
|
||||
maxConcurrent: 2,
|
||||
recoveryLimit: 4,
|
||||
});
|
||||
assert.equal(result.status, 'active');
|
||||
assert.equal(result.profile, 'cluster');
|
||||
assert.deepEqual(result.readiness, readiness());
|
||||
assert.equal(typeof result.prompts.execute, 'function');
|
||||
assert.equal(typeof result.promptExecutions.execute, 'function');
|
||||
assert.equal(providerLoads, 1);
|
||||
assert.ok(events.indexOf('readiness') < events.indexOf('storage_ready'));
|
||||
assert.ok(
|
||||
events.indexOf('recovery_ready') < events.indexOf('providers_load'),
|
||||
);
|
||||
assert.ok(events.indexOf('providers_load') < events.indexOf('active'));
|
||||
assert.equal(await result.stop(), 'stopped');
|
||||
assert.equal(await result.stop(), 'stopped');
|
||||
assert.equal(providerDisposals, 1);
|
||||
assert.equal(databaseCloses, 1);
|
||||
assert.ok(
|
||||
events.indexOf('providers_dispose') < events.indexOf('database_close'),
|
||||
);
|
||||
assert.equal(events.at(-1), 'stopped');
|
||||
});
|
||||
|
||||
test('readiness failure closes PostgreSQL before provider credentials are reachable', async () => {
|
||||
let databaseCloses = 0;
|
||||
let providerLoads = 0;
|
||||
await assert.rejects(
|
||||
bootstrapPostgresPluginPackagePromptApplication({
|
||||
enabled: true,
|
||||
async openDatabase() {
|
||||
return {
|
||||
pool: recoveryPool(),
|
||||
async close() {
|
||||
databaseCloses += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
async assertReady() {
|
||||
throw new Error('schema drift');
|
||||
},
|
||||
async loadProviders() {
|
||||
providerLoads += 1;
|
||||
return providers(async () => {});
|
||||
},
|
||||
async audit() {},
|
||||
}),
|
||||
PostgresPluginPackagePromptApplicationUnavailableError,
|
||||
);
|
||||
assert.equal(databaseCloses, 1);
|
||||
assert.equal(providerLoads, 0);
|
||||
});
|
||||
|
||||
test('readiness rejects migration checksum drift', async () => {
|
||||
const migrations = postgresModelInvocationMigrationDefinition.migrations.map(
|
||||
(migration, index) => ({
|
||||
migrationId: migration.id,
|
||||
streamId: postgresModelInvocationMigrationDefinition.id,
|
||||
dialect: postgresModelInvocationMigrationDefinition.dialect,
|
||||
checksum: index === 7 ? '0'.repeat(64) : migration.checksum,
|
||||
}),
|
||||
);
|
||||
await assert.rejects(
|
||||
assertPostgresPluginPackagePromptApplicationReady({
|
||||
async query() {
|
||||
return { rows: migrations };
|
||||
},
|
||||
}),
|
||||
PostgresPluginPackagePromptApplicationUnavailableError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
pluginPackageAutomationPublicationDigest,
|
||||
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
|
||||
const {
|
||||
preparePluginPackagePromptExecution,
|
||||
} = require('../dist/prompt/pluginPackagePromptExecution.js');
|
||||
const {
|
||||
createPluginPackagePromptOutputArtifact,
|
||||
} = require('../dist/prompt-output/pluginPackagePromptOutputArtifact.js');
|
||||
const {
|
||||
PostgresPluginPackagePromptOutputArtifactRepository,
|
||||
} = require('../dist/prompt-output/storage/postgresPluginPackagePromptOutputArtifactRepository.js');
|
||||
|
||||
function publication() {
|
||||
const unsigned = {
|
||||
schema: 'qinglong/plugin-package-automation-publication@v1',
|
||||
target: {
|
||||
projectId: 'project-a',
|
||||
packageName: 'package-a',
|
||||
installationId: 'installation-a',
|
||||
lockDigest: '1'.repeat(64),
|
||||
generation: 1,
|
||||
generationDigest: '2'.repeat(64),
|
||||
materializedRevisionDigest: '3'.repeat(64),
|
||||
},
|
||||
state: 'active',
|
||||
version: 1,
|
||||
previousPublicationDigest: null,
|
||||
lifecycleEventDigest: null,
|
||||
definitions: {
|
||||
workflows: [],
|
||||
prompts: [
|
||||
{
|
||||
schema: 'qinglong/plugin-package-prompt-resource@v1',
|
||||
id: 'summary',
|
||||
name: 'Summary',
|
||||
template: 'Summarize {{subject}}.',
|
||||
parameters: [{ name: 'subject', required: true }],
|
||||
},
|
||||
],
|
||||
},
|
||||
publishedAtMs: 1_000,
|
||||
};
|
||||
return {
|
||||
...unsigned,
|
||||
publicationDigest: pluginPackageAutomationPublicationDigest(unsigned),
|
||||
};
|
||||
}
|
||||
|
||||
function prepared(output) {
|
||||
const active = publication();
|
||||
return preparePluginPackagePromptExecution({
|
||||
publication: active,
|
||||
expectedPublicationDigest: active.publicationDigest,
|
||||
promptId: 'summary',
|
||||
requestId: 'prompt-request-a',
|
||||
traceId: 'trace-a',
|
||||
requestedBySubject: { type: 'user', id: 'user-a' },
|
||||
policyFence: { projectVersion: 1, bindingVersion: 1 },
|
||||
parameters: { subject: 'private input' },
|
||||
provider: 'openai-compatible',
|
||||
model: 'model-a',
|
||||
maxOutputTokens: 64,
|
||||
plannedAtMs: 2_000,
|
||||
deadlineAtMs: 62_000,
|
||||
...(output === undefined ? {} : { output }),
|
||||
});
|
||||
}
|
||||
|
||||
function row(artifact) {
|
||||
return {
|
||||
artifactId: artifact.artifactId,
|
||||
projectId: artifact.projectId,
|
||||
runId: artifact.runId,
|
||||
stepRunId: artifact.stepRunId,
|
||||
invocationId: artifact.invocationId,
|
||||
requestedByType: artifact.requestedBy.type,
|
||||
requestedById: artifact.requestedBy.id,
|
||||
provider: artifact.provider,
|
||||
model: artifact.model,
|
||||
contentDigest: artifact.contentDigest,
|
||||
outputBytes: artifact.outputBytes,
|
||||
retentionPolicyRevision: artifact.retentionPolicy.revision,
|
||||
retentionMs: String(artifact.retentionPolicy.retentionMs),
|
||||
retentionPolicyDigest: artifact.retentionPolicyDigest,
|
||||
retentionEligibleAtMs: String(artifact.retentionEligibleAtMs),
|
||||
keyId: artifact.keyId,
|
||||
algorithm: artifact.algorithm,
|
||||
plaintextBytes: artifact.plaintextBytes,
|
||||
sealedAtMs: String(artifact.sealedAtMs),
|
||||
artifactDigest: artifact.artifactDigest,
|
||||
artifactJson: artifact,
|
||||
};
|
||||
}
|
||||
|
||||
function fakePool(plan) {
|
||||
const queries = [];
|
||||
let stored = null;
|
||||
const client = {
|
||||
async query(sql, parameters = []) {
|
||||
queries.push(sql);
|
||||
if (sql.includes('model_invocation_prompt_admissions')) {
|
||||
return { rows: [{ planJson: plan }] };
|
||||
}
|
||||
if (
|
||||
sql.startsWith('SELECT artifact_id') &&
|
||||
sql.includes('model_invocation_prompt_output_artifacts')
|
||||
) {
|
||||
return {
|
||||
rows: stored && stored.artifactId === parameters[0] ? [stored] : [],
|
||||
};
|
||||
}
|
||||
if (
|
||||
sql.includes('INSERT INTO') &&
|
||||
sql.includes('prompt_output_artifacts')
|
||||
) {
|
||||
const artifact = JSON.parse(parameters[20]);
|
||||
stored = row(artifact);
|
||||
return { rows: [] };
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
release() {},
|
||||
};
|
||||
return {
|
||||
queries,
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
async query(sql, parameters) {
|
||||
return client.query(sql, parameters);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('PostgreSQL Prompt output Artifact is immutable and plan-bound', async () => {
|
||||
const durable = prepared({
|
||||
mode: 'durable_artifact',
|
||||
retentionPolicy: { revision: 'cluster-v1', retentionMs: 86_400_000 },
|
||||
});
|
||||
const artifact = createPluginPackagePromptOutputArtifact(
|
||||
{
|
||||
projectId: durable.plan.target.projectId,
|
||||
runId: durable.plan.runId,
|
||||
stepRunId: durable.plan.stepRunId,
|
||||
invocationId: durable.plan.invocationId,
|
||||
requestedBy: durable.plan.requestedBySubject,
|
||||
result: {
|
||||
provider: durable.plan.provider,
|
||||
model: durable.plan.model,
|
||||
text: 'private PostgreSQL Artifact output',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 2, outputTokens: 3, totalTokens: 5 },
|
||||
},
|
||||
retentionPolicy: durable.plan.output.retentionPolicy,
|
||||
keyId: 'cluster-key-1',
|
||||
key: Buffer.alloc(32, 7),
|
||||
sealedAtMs: 3_000,
|
||||
},
|
||||
() => Buffer.alloc(12, 9),
|
||||
);
|
||||
const pool = fakePool(durable.plan);
|
||||
const repository = new PostgresPluginPackagePromptOutputArtifactRepository(
|
||||
pool,
|
||||
);
|
||||
|
||||
assert.deepEqual(await repository.put(artifact), { status: 'inserted' });
|
||||
assert.deepEqual(await repository.put(artifact), { status: 'existing' });
|
||||
assert.deepEqual(await repository.find(artifact.artifactId), artifact);
|
||||
assert.equal(pool.queries.includes('BEGIN'), true);
|
||||
assert.equal(
|
||||
pool.queries.includes('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
pool.queries.some((sql) => /FOR (?:UPDATE|SHARE)/.test(sql)),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(row(artifact)).includes(artifact.ciphertext),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(row(artifact)).includes('private PostgreSQL'),
|
||||
false,
|
||||
);
|
||||
|
||||
const live = prepared();
|
||||
const livePool = fakePool(live.plan);
|
||||
const liveRepository =
|
||||
new PostgresPluginPackagePromptOutputArtifactRepository(livePool);
|
||||
const liveArtifact = createPluginPackagePromptOutputArtifact(
|
||||
{
|
||||
projectId: live.plan.target.projectId,
|
||||
runId: live.plan.runId,
|
||||
stepRunId: live.plan.stepRunId,
|
||||
invocationId: live.plan.invocationId,
|
||||
requestedBy: live.plan.requestedBySubject,
|
||||
result: {
|
||||
provider: live.plan.provider,
|
||||
model: live.plan.model,
|
||||
text: 'must remain live only',
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
||||
},
|
||||
retentionPolicy: { revision: 'cluster-v1', retentionMs: 86_400_000 },
|
||||
keyId: 'cluster-key-1',
|
||||
key: Buffer.alloc(32, 7),
|
||||
sealedAtMs: 3_000,
|
||||
},
|
||||
() => Buffer.alloc(12, 9),
|
||||
);
|
||||
await assert.rejects(liveRepository.put(liveArtifact), {
|
||||
code: 'PLUGIN_PACKAGE_PROMPT_OUTPUT_ARTIFACT_CONFLICT',
|
||||
});
|
||||
assert.equal(
|
||||
livePool.queries.some((sql) => sql.includes('INSERT INTO')),
|
||||
false,
|
||||
);
|
||||
});
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
PostgresPluginPackagePromptOutputKeyRetirementRepository,
|
||||
assertPostgresPluginPackagePromptOutputKeyNotRetiring,
|
||||
} = require('../dist/prompt-output/storage/postgresPluginPackagePromptOutputKeyRetirementRepository.js');
|
||||
|
||||
function preparationRow(preparation) {
|
||||
return {
|
||||
keyId: preparation.keyId,
|
||||
retirementId: preparation.retirementId,
|
||||
requestId: preparation.requestId,
|
||||
mutationId: preparation.mutationId,
|
||||
catalogDigest: preparation.catalogDigest,
|
||||
materialProof: preparation.materialProof,
|
||||
preparedAtMs: String(preparation.preparedAtMs),
|
||||
preparationDigest: preparation.preparationDigest,
|
||||
preparationJson: preparation,
|
||||
};
|
||||
}
|
||||
|
||||
function completionRow(completion) {
|
||||
return {
|
||||
keyId: completion.keyId,
|
||||
retirementId: completion.retirementId,
|
||||
requestId: completion.requestId,
|
||||
mutationId: completion.mutationId,
|
||||
preparationDigest: completion.preparationDigest,
|
||||
retiredCatalogDigest: completion.retiredCatalogDigest,
|
||||
absenceProof: completion.absenceProof,
|
||||
completedAtMs: String(completion.completedAtMs),
|
||||
completionDigest: completion.completionDigest,
|
||||
completionJson: completion,
|
||||
};
|
||||
}
|
||||
|
||||
function storagePool(options = {}) {
|
||||
const queries = [];
|
||||
const preparations = new Map();
|
||||
const completions = new Map();
|
||||
const liveKeys = new Set(options.liveKeys ?? []);
|
||||
const client = {
|
||||
async query(sql, parameters = []) {
|
||||
queries.push({ sql, parameters });
|
||||
if (
|
||||
sql.includes('key_retirement_preparations') &&
|
||||
sql.includes('SELECT')
|
||||
) {
|
||||
const value = preparations.get(parameters[0]);
|
||||
return { rows: value ? [preparationRow(value)] : [] };
|
||||
}
|
||||
if (
|
||||
sql.includes('key_retirement_completions') &&
|
||||
sql.includes('SELECT')
|
||||
) {
|
||||
const value = completions.get(parameters[0]);
|
||||
return { rows: value ? [completionRow(value)] : [] };
|
||||
}
|
||||
if (sql.includes('count(*)::text')) {
|
||||
return { rows: [{ count: liveKeys.has(parameters[0]) ? '1' : '0' }] };
|
||||
}
|
||||
if (
|
||||
sql.includes('INSERT INTO') &&
|
||||
sql.includes('key_retirement_preparations')
|
||||
) {
|
||||
const value = JSON.parse(parameters[8]);
|
||||
preparations.set(value.keyId, value);
|
||||
return { rows: [], rowCount: 1 };
|
||||
}
|
||||
if (
|
||||
sql.includes('INSERT INTO') &&
|
||||
sql.includes('key_retirement_completions')
|
||||
) {
|
||||
const value = JSON.parse(parameters[9]);
|
||||
completions.set(value.keyId, value);
|
||||
return { rows: [], rowCount: 1 };
|
||||
}
|
||||
return { rows: [], rowCount: 0 };
|
||||
},
|
||||
release() {},
|
||||
};
|
||||
return {
|
||||
queries,
|
||||
preparations,
|
||||
completions,
|
||||
client,
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
async query(sql, parameters) {
|
||||
return client.query(sql, parameters);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function command() {
|
||||
return {
|
||||
keyId: 'cluster-key-retired',
|
||||
retirementId: 'retirement-a',
|
||||
requestId: 'request-a',
|
||||
mutationId: 'mutation-a',
|
||||
catalogDigest: '1'.repeat(64),
|
||||
materialProof: '2'.repeat(64),
|
||||
};
|
||||
}
|
||||
|
||||
test('PostgreSQL appends key retirement facts behind a shared key fence', async () => {
|
||||
const pool = storagePool();
|
||||
const times = [10_000, 20_000, 30_000, 40_000];
|
||||
const repository =
|
||||
new PostgresPluginPackagePromptOutputKeyRetirementRepository({
|
||||
pool,
|
||||
now: () => times.shift(),
|
||||
});
|
||||
|
||||
const prepared = await repository.prepare(command());
|
||||
assert.equal(prepared.status, 'created');
|
||||
assert.equal(prepared.preparation.preparedAtMs, 10_000);
|
||||
const replay = await repository.prepare(command());
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.deepEqual(replay.preparation, prepared.preparation);
|
||||
|
||||
const completed = await repository.complete({
|
||||
preparation: prepared.preparation,
|
||||
retiredCatalogDigest: '3'.repeat(64),
|
||||
absenceProof: '4'.repeat(64),
|
||||
});
|
||||
assert.equal(completed.status, 'created');
|
||||
assert.equal(completed.completion.completedAtMs, 30_000);
|
||||
const completionReplay = await repository.complete({
|
||||
preparation: prepared.preparation,
|
||||
retiredCatalogDigest: '3'.repeat(64),
|
||||
absenceProof: '4'.repeat(64),
|
||||
});
|
||||
assert.equal(completionReplay.status, 'existing');
|
||||
assert.deepEqual(completionReplay.completion, completed.completion);
|
||||
assert.deepEqual(await repository.find(command().keyId), {
|
||||
preparation: prepared.preparation,
|
||||
completion: completed.completion,
|
||||
});
|
||||
|
||||
const lockIndex = pool.queries.findIndex(({ sql }) =>
|
||||
sql.includes('pg_advisory_xact_lock'),
|
||||
);
|
||||
const countIndex = pool.queries.findIndex(({ sql }) =>
|
||||
sql.includes('count(*)::text'),
|
||||
);
|
||||
const insertIndex = pool.queries.findIndex(
|
||||
({ sql }) =>
|
||||
sql.includes('INSERT INTO') &&
|
||||
sql.includes('key_retirement_preparations'),
|
||||
);
|
||||
assert.equal(lockIndex >= 0, true);
|
||||
assert.equal(lockIndex < countIndex && countIndex < insertIndex, true);
|
||||
await assert.rejects(
|
||||
assertPostgresPluginPackagePromptOutputKeyNotRetiring(
|
||||
pool.client,
|
||||
command().keyId,
|
||||
),
|
||||
{ code: 'PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_RETIREMENT_CONFLICT' },
|
||||
);
|
||||
});
|
||||
|
||||
test('PostgreSQL refuses retirement while live ciphertext exists', async () => {
|
||||
const pool = storagePool({ liveKeys: [command().keyId] });
|
||||
const repository =
|
||||
new PostgresPluginPackagePromptOutputKeyRetirementRepository({
|
||||
pool,
|
||||
now: () => 10_000,
|
||||
});
|
||||
await assert.rejects(repository.prepare(command()), {
|
||||
code: 'PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_RETIREMENT_CONFLICT',
|
||||
});
|
||||
assert.equal(pool.preparations.size, 0);
|
||||
assert.equal(
|
||||
pool.queries.some(
|
||||
({ sql }) =>
|
||||
sql.includes('INSERT INTO') &&
|
||||
sql.includes('key_retirement_preparations'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
PostgresPluginPackagePromptOutputKeyRotationRepository,
|
||||
} = require('../dist/prompt-output/storage/postgresPluginPackagePromptOutputKeyRotationRepository.js');
|
||||
|
||||
function preparationRow(value) {
|
||||
return {
|
||||
rotationId: value.rotationId,
|
||||
requestId: value.requestId,
|
||||
mutationId: value.mutationId,
|
||||
expectedSecretUid: value.expectedSecretUid,
|
||||
expectedActiveKeyId: value.expectedActiveKeyId,
|
||||
expectedCatalogDigest: value.expectedCatalogDigest,
|
||||
newKeyId: value.newKeyId,
|
||||
materialProof: value.materialProof,
|
||||
preparedAtMs: String(value.preparedAtMs),
|
||||
preparationDigest: value.preparationDigest,
|
||||
preparationJson: value,
|
||||
};
|
||||
}
|
||||
|
||||
function completionRow(value) {
|
||||
return {
|
||||
rotationId: value.rotationId,
|
||||
requestId: value.requestId,
|
||||
mutationId: value.mutationId,
|
||||
preparationDigest: value.preparationDigest,
|
||||
generation: String(value.generation),
|
||||
previousActiveKeyId: value.previousActiveKeyId,
|
||||
activeKeyId: value.activeKeyId,
|
||||
catalogDigest: value.catalogDigest,
|
||||
materialProof: value.materialProof,
|
||||
completedAtMs: String(value.completedAtMs),
|
||||
completionDigest: value.completionDigest,
|
||||
completionJson: value,
|
||||
};
|
||||
}
|
||||
|
||||
function storagePool(options = {}) {
|
||||
const queries = [];
|
||||
const preparations = new Map();
|
||||
const completions = new Map();
|
||||
let inserts = 0;
|
||||
let commitFailures = options.commitFailures ?? 0;
|
||||
const client = {
|
||||
async query(sql, parameters = []) {
|
||||
queries.push({ sql, parameters });
|
||||
if (sql.includes('key_rotation_preparations') && sql.includes('SELECT')) {
|
||||
const value = preparations.get(parameters[0]);
|
||||
return { rows: value ? [preparationRow(value)] : [] };
|
||||
}
|
||||
if (sql.includes('key_rotation_completions') && sql.includes('SELECT')) {
|
||||
const value = completions.get(parameters[0]);
|
||||
return { rows: value ? [completionRow(value)] : [] };
|
||||
}
|
||||
if (
|
||||
sql.includes('INSERT INTO') &&
|
||||
sql.includes('key_rotation_preparations')
|
||||
) {
|
||||
if (options.conflictOnSecondSource && preparations.size > 0) {
|
||||
const error = new Error('unique source');
|
||||
error.code = '23505';
|
||||
throw error;
|
||||
}
|
||||
const value = JSON.parse(parameters[10]);
|
||||
preparations.set(value.rotationId, value);
|
||||
inserts += 1;
|
||||
return { rows: [], rowCount: 1 };
|
||||
}
|
||||
if (
|
||||
sql.includes('INSERT INTO') &&
|
||||
sql.includes('key_rotation_completions')
|
||||
) {
|
||||
const value = JSON.parse(parameters[11]);
|
||||
completions.set(value.rotationId, value);
|
||||
inserts += 1;
|
||||
return { rows: [], rowCount: 1 };
|
||||
}
|
||||
if (sql === 'COMMIT' && commitFailures > 0 && inserts > 0) {
|
||||
commitFailures -= 1;
|
||||
throw new Error('connection reset after durable COMMIT');
|
||||
}
|
||||
return { rows: [], rowCount: 0 };
|
||||
},
|
||||
release() {},
|
||||
};
|
||||
return {
|
||||
queries,
|
||||
preparations,
|
||||
completions,
|
||||
client,
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
async query(sql, parameters) {
|
||||
return client.query(sql, parameters);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function request(overrides = {}) {
|
||||
return {
|
||||
rotationId: 'rotation-a',
|
||||
requestId: 'request-a',
|
||||
mutationId: 'mutation-a',
|
||||
expectedSecretUid: 'secret-uid-a',
|
||||
expectedActiveKeyId: 'key-a',
|
||||
expectedCatalogDigest: '1'.repeat(64),
|
||||
newKeyId: 'key-b',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function state() {
|
||||
return {
|
||||
generation: 2,
|
||||
previousActiveKeyId: 'key-a',
|
||||
activeKeyId: 'key-b',
|
||||
catalogDigest: '2'.repeat(64),
|
||||
materialProof: '3'.repeat(64),
|
||||
};
|
||||
}
|
||||
|
||||
test('PostgreSQL appends rotation prepare and completion behind one source fence', async () => {
|
||||
const pool = storagePool();
|
||||
const times = [10_000, 20_000, 30_000, 40_000];
|
||||
const repository = new PostgresPluginPackagePromptOutputKeyRotationRepository(
|
||||
{
|
||||
pool,
|
||||
now: () => times.shift(),
|
||||
},
|
||||
);
|
||||
const prepared = await repository.prepare({
|
||||
request: request(),
|
||||
materialProof: '3'.repeat(64),
|
||||
});
|
||||
assert.equal(prepared.status, 'created');
|
||||
assert.equal(prepared.preparation.preparedAtMs, 10_000);
|
||||
const prepareReplay = await repository.prepare({
|
||||
request: request(),
|
||||
materialProof: '3'.repeat(64),
|
||||
});
|
||||
assert.equal(prepareReplay.status, 'existing');
|
||||
|
||||
const completed = await repository.complete({
|
||||
preparation: prepared.preparation,
|
||||
state: state(),
|
||||
});
|
||||
assert.equal(completed.status, 'created');
|
||||
assert.equal(completed.completion.completedAtMs, 30_000);
|
||||
const completionReplay = await repository.complete({
|
||||
preparation: prepared.preparation,
|
||||
state: state(),
|
||||
});
|
||||
assert.equal(completionReplay.status, 'existing');
|
||||
assert.deepEqual(await repository.find('rotation-a'), {
|
||||
preparation: prepared.preparation,
|
||||
completion: completed.completion,
|
||||
});
|
||||
|
||||
const lockIndex = pool.queries.findIndex(({ sql }) =>
|
||||
sql.includes('pg_advisory_xact_lock'),
|
||||
);
|
||||
const insertIndex = pool.queries.findIndex(
|
||||
({ sql }) =>
|
||||
sql.includes('INSERT INTO') && sql.includes('key_rotation_preparations'),
|
||||
);
|
||||
assert.equal(lockIndex >= 0 && lockIndex < insertIndex, true);
|
||||
assert.equal(
|
||||
JSON.stringify([
|
||||
...pool.preparations.values(),
|
||||
...pool.completions.values(),
|
||||
]).includes(Buffer.alloc(32, 0x42).toString('base64url')),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('PostgreSQL durable facts resolve COMMIT response loss and source conflicts', async () => {
|
||||
const lost = storagePool({ commitFailures: 1 });
|
||||
const repository = new PostgresPluginPackagePromptOutputKeyRotationRepository(
|
||||
{
|
||||
pool: lost,
|
||||
now: () => 10_000,
|
||||
},
|
||||
);
|
||||
await assert.rejects(
|
||||
repository.prepare({ request: request(), materialProof: '3'.repeat(64) }),
|
||||
{ code: 'PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_ROTATION_UNAVAILABLE' },
|
||||
);
|
||||
const durable = await repository.find('rotation-a');
|
||||
assert.equal(durable.preparation.rotationId, 'rotation-a');
|
||||
|
||||
const competing = storagePool({ conflictOnSecondSource: true });
|
||||
const competingRepository =
|
||||
new PostgresPluginPackagePromptOutputKeyRotationRepository({
|
||||
pool: competing,
|
||||
now: () => 10_000,
|
||||
});
|
||||
await competingRepository.prepare({
|
||||
request: request(),
|
||||
materialProof: '3'.repeat(64),
|
||||
});
|
||||
await assert.rejects(
|
||||
competingRepository.prepare({
|
||||
request: request({
|
||||
rotationId: 'rotation-b',
|
||||
requestId: 'request-b',
|
||||
mutationId: 'mutation-b',
|
||||
newKeyId: 'key-c',
|
||||
}),
|
||||
materialProof: '4'.repeat(64),
|
||||
}),
|
||||
{ code: 'PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_ROTATION_CONFLICT' },
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidModelPricingError,
|
||||
MODEL_INVOCATION_PRICE_QUOTE_SCHEMA,
|
||||
MODEL_INVOCATION_PRICE_SETTLEMENT_SCHEMA,
|
||||
MODEL_PRICE_CATALOG_ENTRY_SCHEMA,
|
||||
StaticModelPriceCatalog,
|
||||
createModelInvocationPriceQuote,
|
||||
createModelInvocationPriceSettlement,
|
||||
createModelPriceCatalogEntry,
|
||||
normalizeModelInvocationPriceQuote,
|
||||
normalizeModelInvocationPriceSettlement,
|
||||
normalizeModelPriceCatalogEntry,
|
||||
priceModelUsage,
|
||||
} = require('../dist/pricing/pricing.js');
|
||||
|
||||
const {
|
||||
createModelInvocationCompletionCommand,
|
||||
createModelInvocationMutationIdentity,
|
||||
createModelInvocationStartCommand,
|
||||
} = require('../dist/model-invocation/modelInvocation.js');
|
||||
const {
|
||||
createStepRunRecord,
|
||||
transitionStepRunMutation,
|
||||
} = require('@qinglong/runtime-core/step-run');
|
||||
|
||||
const NOW = 1_000_000;
|
||||
|
||||
function price(overrides = {}) {
|
||||
return createModelPriceCatalogEntry({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-2026-07',
|
||||
currency: 'USD',
|
||||
inputMicrosPerMillionTokens: 150_000,
|
||||
outputMicrosPerMillionTokens: 600_000,
|
||||
publishedAtMs: NOW - 1,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function audit(phase, overrides = {}) {
|
||||
return {
|
||||
phase,
|
||||
projectId: 'project-a',
|
||||
runId: 'run-a',
|
||||
stepRunId: 'step-a',
|
||||
traceId: 'trace-a',
|
||||
requestId: 'request-a',
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
policyRevision: 'policy-1',
|
||||
requestDigest: `sha256:${'b'.repeat(64)}`,
|
||||
deadlineAtMs: NOW + 10_000,
|
||||
inputBytes: 128,
|
||||
maxOutputTokens: 64,
|
||||
outputBytes: 0,
|
||||
usage: null,
|
||||
errorCode: null,
|
||||
occurredAtMs: NOW,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function commands(usage) {
|
||||
const failed = usage === null;
|
||||
const ready = createStepRunRecord({
|
||||
id: 'step-a',
|
||||
runId: 'run-a',
|
||||
stepKey: 'model',
|
||||
kind: 'model',
|
||||
definitionRef: 'prompt:a@1',
|
||||
definitionDigest: 'a'.repeat(64),
|
||||
required: true,
|
||||
initialStatus: 'ready',
|
||||
inputRef: 'artifact:a',
|
||||
mutationId: 'create-step-a',
|
||||
createdAtMs: NOW - 1,
|
||||
});
|
||||
const startIdentity = createModelInvocationMutationIdentity(
|
||||
'request-a',
|
||||
'start',
|
||||
);
|
||||
const start = createModelInvocationStartCommand(
|
||||
audit('admitted'),
|
||||
transitionStepRunMutation(
|
||||
ready,
|
||||
{
|
||||
expectedVersion: ready.version,
|
||||
expectedDigest: ready.stepRunDigest,
|
||||
mutationId: startIdentity.mutationId,
|
||||
to: 'running',
|
||||
atMs: NOW,
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 1,
|
||||
expectedRunEventSequence: 1,
|
||||
eventId: startIdentity.eventId,
|
||||
dedupeKey: startIdentity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
),
|
||||
);
|
||||
const completionIdentity = createModelInvocationMutationIdentity(
|
||||
'request-a',
|
||||
'completion',
|
||||
);
|
||||
const completion = createModelInvocationCompletionCommand(
|
||||
start.start,
|
||||
audit(failed ? 'failed' : 'completed', {
|
||||
outputBytes: failed ? 0 : 12,
|
||||
usage,
|
||||
errorCode: failed ? 'MODEL_PROVIDER_FAILED' : null,
|
||||
occurredAtMs: NOW + 25,
|
||||
}),
|
||||
transitionStepRunMutation(
|
||||
start.stepRunMutation.stepRun,
|
||||
{
|
||||
expectedVersion: start.start.startedStepRunVersion,
|
||||
expectedDigest: start.start.startedStepRunDigest,
|
||||
mutationId: completionIdentity.mutationId,
|
||||
to: failed ? 'failed' : 'succeeded',
|
||||
...(failed
|
||||
? {
|
||||
resultCode: 'model_provider_failed',
|
||||
errorSummary: 'Model invocation failed',
|
||||
}
|
||||
: { outputRef: 'model-invocation:request-a' }),
|
||||
atMs: NOW + 25,
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 2,
|
||||
expectedRunEventSequence: 2,
|
||||
eventId: completionIdentity.eventId,
|
||||
dedupeKey: completionIdentity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
),
|
||||
);
|
||||
return { start, completion };
|
||||
}
|
||||
|
||||
test('catalog keeps exact immutable provider/model/revision identities', async () => {
|
||||
const entry = price();
|
||||
const catalog = new StaticModelPriceCatalog([entry]);
|
||||
|
||||
assert.equal(entry.schema, MODEL_PRICE_CATALOG_ENTRY_SCHEMA);
|
||||
assert.deepEqual(normalizeModelPriceCatalogEntry(entry), entry);
|
||||
assert.deepEqual(
|
||||
await catalog.resolve({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-2026-07',
|
||||
}),
|
||||
entry,
|
||||
);
|
||||
assert.equal(
|
||||
await catalog.resolve({
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
priceRevision: 'price-older',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
assert.throws(
|
||||
() => new StaticModelPriceCatalog([entry, entry]),
|
||||
InvalidModelPricingError,
|
||||
);
|
||||
});
|
||||
|
||||
test('quote reserves the worst valid token allocation without overflow', () => {
|
||||
const quote = createModelInvocationPriceQuote(price(), {
|
||||
invocationId: 'request-a',
|
||||
projectId: 'project-a',
|
||||
modelPolicyRevision: 'policy-1',
|
||||
maxTotalTokens: 256,
|
||||
maxOutputTokens: 64,
|
||||
});
|
||||
|
||||
assert.equal(quote.schema, MODEL_INVOCATION_PRICE_QUOTE_SCHEMA);
|
||||
assert.equal(quote.reservedCostMicros, 68);
|
||||
assert.deepEqual(normalizeModelInvocationPriceQuote(quote), quote);
|
||||
assert.equal(Object.isFrozen(quote), true);
|
||||
});
|
||||
|
||||
test('settlement deterministically prices exact usage and ignores provider cost', () => {
|
||||
const quote = createModelInvocationPriceQuote(price(), {
|
||||
invocationId: 'request-a',
|
||||
projectId: 'project-a',
|
||||
modelPolicyRevision: 'policy-1',
|
||||
maxTotalTokens: 256,
|
||||
maxOutputTokens: 64,
|
||||
});
|
||||
const providerUsage = {
|
||||
inputTokens: 5,
|
||||
outputTokens: 2,
|
||||
totalTokens: 7,
|
||||
costMicros: 99_999,
|
||||
};
|
||||
const canonicalUsage = priceModelUsage(quote, providerUsage);
|
||||
const { completion } = commands(canonicalUsage);
|
||||
const settlement = createModelInvocationPriceSettlement(
|
||||
quote,
|
||||
completion.completion,
|
||||
);
|
||||
|
||||
assert.ok(settlement);
|
||||
assert.equal(canonicalUsage.costMicros, 3);
|
||||
assert.equal(settlement.schema, MODEL_INVOCATION_PRICE_SETTLEMENT_SCHEMA);
|
||||
assert.equal(settlement.costMicros, 3);
|
||||
assert.deepEqual(
|
||||
normalizeModelInvocationPriceSettlement(
|
||||
settlement,
|
||||
quote,
|
||||
completion.completion,
|
||||
),
|
||||
settlement,
|
||||
);
|
||||
});
|
||||
|
||||
test('unknown usage remains unpriced and quote tampering fails closed', () => {
|
||||
const quote = createModelInvocationPriceQuote(price(), {
|
||||
invocationId: 'request-a',
|
||||
projectId: 'project-a',
|
||||
modelPolicyRevision: 'policy-1',
|
||||
maxTotalTokens: 256,
|
||||
maxOutputTokens: 64,
|
||||
});
|
||||
const failed = commands(null);
|
||||
|
||||
assert.equal(
|
||||
createModelInvocationPriceSettlement(quote, failed.completion.completion),
|
||||
null,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelInvocationPriceQuote({
|
||||
...quote,
|
||||
reservedCostMicros: quote.reservedCostMicros + 1,
|
||||
}),
|
||||
InvalidModelPricingError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,555 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
ModelGatewayProfileDrainingError,
|
||||
ModelGatewayProfileUnavailableError,
|
||||
ModelPriceCatalogManagementProfileUnavailableError,
|
||||
bootstrapModelPriceCatalogManagementProfile,
|
||||
bootstrapModelGatewayProfile,
|
||||
} = require('@qinglong/ai/profile');
|
||||
|
||||
function repository(overrides = {}) {
|
||||
return {
|
||||
async findStart() {
|
||||
return null;
|
||||
},
|
||||
async findCompletion() {
|
||||
return null;
|
||||
},
|
||||
async findResolution() {
|
||||
return null;
|
||||
},
|
||||
async findUsage() {
|
||||
return null;
|
||||
},
|
||||
async findPriceQuote() {
|
||||
return null;
|
||||
},
|
||||
async findPriceSettlement() {
|
||||
return null;
|
||||
},
|
||||
async listProjectUsage() {
|
||||
return { records: [], hasMore: false };
|
||||
},
|
||||
async summarizeProjectUsage() {
|
||||
return {
|
||||
invocationCount: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0,
|
||||
knownCostMicros: 0,
|
||||
unknownCostInvocations: 0,
|
||||
};
|
||||
},
|
||||
async findQuotaReservation() {
|
||||
return null;
|
||||
},
|
||||
async findQuotaSettlement() {
|
||||
return null;
|
||||
},
|
||||
async readQuotaWindowUsage() {
|
||||
return null;
|
||||
},
|
||||
async readAuthority() {
|
||||
return null;
|
||||
},
|
||||
async listIncomplete() {
|
||||
return {
|
||||
observedAtMs: 1,
|
||||
candidates: [],
|
||||
hasMore: false,
|
||||
};
|
||||
},
|
||||
async admit() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
async complete() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
async resolve() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function providerAuthority(dispose) {
|
||||
return {
|
||||
providers: [
|
||||
{
|
||||
type: 'remote',
|
||||
async listModels() {
|
||||
return [{ id: 'model-a' }];
|
||||
},
|
||||
async generate() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
async *stream() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
],
|
||||
policies: {
|
||||
async resolve() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
dispose,
|
||||
};
|
||||
}
|
||||
|
||||
function pricingAuthority(overrides = {}) {
|
||||
return {
|
||||
async resolve() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('disabled AI Profile never reaches storage or provider credential loaders', async () => {
|
||||
let storageLoads = 0;
|
||||
let providerLoads = 0;
|
||||
const audits = [];
|
||||
const result = await bootstrapModelGatewayProfile({
|
||||
enabled: false,
|
||||
profile: 'edge',
|
||||
async loadStorage() {
|
||||
storageLoads += 1;
|
||||
throw new Error('must remain unreachable');
|
||||
},
|
||||
async loadProviders() {
|
||||
providerLoads += 1;
|
||||
throw new Error('must remain unreachable');
|
||||
},
|
||||
audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'disabled');
|
||||
assert.equal(await result.stop(), 'stopped');
|
||||
assert.equal(storageLoads, 0);
|
||||
assert.equal(providerLoads, 0);
|
||||
assert.deepEqual(audits, [{ profile: 'edge', state: 'disabled' }]);
|
||||
});
|
||||
|
||||
test('enabled Edge AI Profile proves storage and recovery before providers', async () => {
|
||||
const order = [];
|
||||
const audits = [];
|
||||
const result = await bootstrapModelGatewayProfile({
|
||||
enabled: true,
|
||||
profile: 'edge',
|
||||
async loadStorage() {
|
||||
order.push('storage');
|
||||
return {
|
||||
repository: repository({
|
||||
async listIncomplete(limit) {
|
||||
order.push(`recovery:${limit}`);
|
||||
return {
|
||||
observedAtMs: 2,
|
||||
candidates: [],
|
||||
hasMore: false,
|
||||
};
|
||||
},
|
||||
}),
|
||||
pricing: pricingAuthority(),
|
||||
close() {
|
||||
order.push('storage.close');
|
||||
},
|
||||
};
|
||||
},
|
||||
async loadProviders() {
|
||||
order.push('providers');
|
||||
return providerAuthority(() => {
|
||||
order.push('providers.dispose');
|
||||
});
|
||||
},
|
||||
audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'active');
|
||||
assert.equal(result.capability.maxConcurrent, 1);
|
||||
assert.equal(result.capability.recoveryLimit, 4);
|
||||
assert.equal(result.capability.accepting, true);
|
||||
assert.deepEqual(order.slice(0, 3), ['storage', 'recovery:4', 'providers']);
|
||||
assert.deepEqual(
|
||||
audits.map(({ state }) => state),
|
||||
['storage_ready', 'recovery_ready', 'active'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
await result.capability.listProjectUsage({
|
||||
projectId: 'project-a',
|
||||
fromMsInclusive: 0,
|
||||
toMsExclusive: 1,
|
||||
limit: 1,
|
||||
}),
|
||||
{ records: [], hasMore: false },
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await result.capability.summarizeProjectUsage({
|
||||
projectId: 'project-a',
|
||||
fromMsInclusive: 0,
|
||||
toMsExclusive: 1,
|
||||
})
|
||||
).invocationCount,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
await result.capability.readQuotaWindowUsage('project-a', 0),
|
||||
null,
|
||||
);
|
||||
assert.equal(await result.capability.findPriceQuote('request-a'), null);
|
||||
assert.equal(await result.capability.findPriceSettlement('request-a'), null);
|
||||
assert.equal(await result.capability.stop(), 'stopped');
|
||||
assert.equal(result.capability.accepting, false);
|
||||
assert.deepEqual(order, [
|
||||
'storage',
|
||||
'recovery:4',
|
||||
'providers',
|
||||
'providers.dispose',
|
||||
'storage.close',
|
||||
]);
|
||||
assert.equal(await result.capability.stop(), 'stopped');
|
||||
assert.equal(order.filter((item) => item.endsWith('close')).length, 1);
|
||||
assert.equal(order.filter((item) => item.endsWith('dispose')).length, 1);
|
||||
assert.equal(audits.at(-1).state, 'stopped');
|
||||
});
|
||||
|
||||
test('durable activation fence drains and releases authorities before a rejected operation', async () => {
|
||||
const order = [];
|
||||
const audits = [];
|
||||
let active = true;
|
||||
const result = await bootstrapModelGatewayProfile({
|
||||
enabled: true,
|
||||
profile: 'edge',
|
||||
async loadStorage() {
|
||||
return {
|
||||
repository: repository(),
|
||||
pricing: pricingAuthority(),
|
||||
close() {
|
||||
order.push('storage.close');
|
||||
},
|
||||
};
|
||||
},
|
||||
async loadProviders() {
|
||||
return providerAuthority(() => {
|
||||
order.push('providers.dispose');
|
||||
});
|
||||
},
|
||||
confirmActive() {
|
||||
if (!active) throw new Error('feature generation changed');
|
||||
},
|
||||
audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'active');
|
||||
active = false;
|
||||
await assert.rejects(
|
||||
result.capability.listProjectUsage({
|
||||
projectId: 'project-a',
|
||||
fromMsInclusive: 0,
|
||||
toMsExclusive: 1,
|
||||
limit: 1,
|
||||
}),
|
||||
ModelGatewayProfileDrainingError,
|
||||
);
|
||||
assert.equal(result.capability.accepting, false);
|
||||
assert.equal(result.capability.activeOperations, 0);
|
||||
assert.deepEqual(order, ['providers.dispose', 'storage.close']);
|
||||
assert.deepEqual(
|
||||
audits.map(({ state }) => state),
|
||||
['storage_ready', 'recovery_ready', 'active', 'draining', 'stopped'],
|
||||
);
|
||||
assert.equal(await result.capability.stop(), 'stopped');
|
||||
});
|
||||
|
||||
test('an explicit stop automatically releases authorities when the final operation drains', async () => {
|
||||
const order = [];
|
||||
let release;
|
||||
let started;
|
||||
const operationStarted = new Promise((resolve) => {
|
||||
started = resolve;
|
||||
});
|
||||
const operationRelease = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const result = await bootstrapModelGatewayProfile({
|
||||
enabled: true,
|
||||
profile: 'standalone',
|
||||
async loadStorage() {
|
||||
return {
|
||||
repository: repository({
|
||||
async listProjectUsage() {
|
||||
started();
|
||||
await operationRelease;
|
||||
return { records: [], hasMore: false };
|
||||
},
|
||||
}),
|
||||
pricing: pricingAuthority(),
|
||||
close() {
|
||||
order.push('storage.close');
|
||||
},
|
||||
};
|
||||
},
|
||||
async loadProviders() {
|
||||
return providerAuthority(() => {
|
||||
order.push('providers.dispose');
|
||||
});
|
||||
},
|
||||
audit() {},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'active');
|
||||
const operation = result.capability.listProjectUsage({
|
||||
projectId: 'project-a',
|
||||
fromMsInclusive: 0,
|
||||
toMsExclusive: 1,
|
||||
limit: 1,
|
||||
});
|
||||
await operationStarted;
|
||||
assert.equal(await result.capability.stop(), 'draining');
|
||||
assert.equal(result.capability.accepting, false);
|
||||
release();
|
||||
await operation;
|
||||
assert.equal(await result.capability.stop(), 'stopped');
|
||||
assert.deepEqual(order, ['providers.dispose', 'storage.close']);
|
||||
});
|
||||
|
||||
test('AI Profile fails closed before provider credentials when recovery is truncated', async () => {
|
||||
const order = [];
|
||||
const audits = [];
|
||||
await assert.rejects(
|
||||
bootstrapModelGatewayProfile({
|
||||
enabled: true,
|
||||
profile: 'cluster',
|
||||
async loadStorage() {
|
||||
order.push('storage');
|
||||
return {
|
||||
repository: repository({
|
||||
async listIncomplete(limit) {
|
||||
order.push(`recovery:${limit}`);
|
||||
return {
|
||||
observedAtMs: 3,
|
||||
candidates: [],
|
||||
hasMore: true,
|
||||
};
|
||||
},
|
||||
}),
|
||||
pricing: pricingAuthority(),
|
||||
close() {
|
||||
order.push('storage.close');
|
||||
},
|
||||
};
|
||||
},
|
||||
async loadProviders() {
|
||||
order.push('providers');
|
||||
return providerAuthority();
|
||||
},
|
||||
audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
}),
|
||||
ModelGatewayProfileUnavailableError,
|
||||
);
|
||||
assert.deepEqual(order, ['storage', 'recovery:128', 'storage.close']);
|
||||
assert.deepEqual(
|
||||
audits.map(({ state }) => state),
|
||||
['storage_ready', 'failed'],
|
||||
);
|
||||
});
|
||||
|
||||
test('AI Profile closes incomplete storage authority before provider credentials', async () => {
|
||||
const order = [];
|
||||
const audits = [];
|
||||
await assert.rejects(
|
||||
bootstrapModelGatewayProfile({
|
||||
enabled: true,
|
||||
profile: 'edge',
|
||||
async loadStorage() {
|
||||
order.push('storage');
|
||||
return {
|
||||
repository: repository(),
|
||||
close() {
|
||||
order.push('storage.close');
|
||||
},
|
||||
};
|
||||
},
|
||||
async loadProviders() {
|
||||
order.push('providers');
|
||||
return providerAuthority();
|
||||
},
|
||||
audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
}),
|
||||
ModelGatewayProfileUnavailableError,
|
||||
);
|
||||
assert.deepEqual(order, ['storage', 'storage.close']);
|
||||
assert.deepEqual(
|
||||
audits.map(({ state }) => state),
|
||||
['failed'],
|
||||
);
|
||||
});
|
||||
|
||||
function modelPriceCatalogManagementAuthority(overrides = {}) {
|
||||
return {
|
||||
repository: {
|
||||
async findPublication() {
|
||||
return null;
|
||||
},
|
||||
async findCurrent() {
|
||||
return null;
|
||||
},
|
||||
async findAuthorization() {
|
||||
return null;
|
||||
},
|
||||
async publishAuthorized() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
async transitionAuthorized() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
authorizer: {
|
||||
async authorize() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('disabled price catalog management is loader-free on constrained Edge', async () => {
|
||||
let authorityLoads = 0;
|
||||
const audits = [];
|
||||
const result = await bootstrapModelPriceCatalogManagementProfile({
|
||||
enabled: false,
|
||||
profile: 'edge',
|
||||
async loadAuthority() {
|
||||
authorityLoads += 1;
|
||||
throw new Error('must remain unreachable');
|
||||
},
|
||||
audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'disabled');
|
||||
assert.equal(result.decisionMode, 'human_confirmation');
|
||||
assert.equal(await result.stop(), 'stopped');
|
||||
assert.equal(authorityLoads, 0);
|
||||
assert.deepEqual(audits, [
|
||||
{
|
||||
profile: 'edge',
|
||||
state: 'disabled',
|
||||
decisionMode: 'human_confirmation',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('standalone price catalog management lazily activates human confirmation authority', async () => {
|
||||
const order = [];
|
||||
const audits = [];
|
||||
const result = await bootstrapModelPriceCatalogManagementProfile({
|
||||
enabled: true,
|
||||
profile: 'standalone',
|
||||
async loadAuthority() {
|
||||
order.push('authority');
|
||||
return modelPriceCatalogManagementAuthority({
|
||||
close() {
|
||||
order.push('authority.close');
|
||||
},
|
||||
});
|
||||
},
|
||||
audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'active');
|
||||
assert.equal(result.decisionMode, 'human_confirmation');
|
||||
assert.equal(result.capability.accepting, true);
|
||||
assert.equal(result.capability.activeOperations, 0);
|
||||
assert.equal(await result.capability.stop(), 'stopped');
|
||||
assert.equal(result.capability.accepting, false);
|
||||
assert.equal(await result.capability.stop(), 'stopped');
|
||||
assert.deepEqual(order, ['authority', 'authority.close']);
|
||||
assert.deepEqual(
|
||||
audits.map(({ state }) => state),
|
||||
['authority_ready', 'active', 'stopped'],
|
||||
);
|
||||
});
|
||||
|
||||
test('cluster price catalog management fails closed without quota authority', async () => {
|
||||
const order = [];
|
||||
const audits = [];
|
||||
await assert.rejects(
|
||||
bootstrapModelPriceCatalogManagementProfile({
|
||||
enabled: true,
|
||||
profile: 'cluster',
|
||||
async loadAuthority() {
|
||||
order.push('authority');
|
||||
return modelPriceCatalogManagementAuthority({
|
||||
close() {
|
||||
order.push('authority.close');
|
||||
},
|
||||
});
|
||||
},
|
||||
audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
}),
|
||||
ModelPriceCatalogManagementProfileUnavailableError,
|
||||
);
|
||||
|
||||
assert.deepEqual(order, ['authority', 'authority.close']);
|
||||
assert.deepEqual(audits, [
|
||||
{
|
||||
profile: 'cluster',
|
||||
state: 'failed',
|
||||
decisionMode: 'separation_of_duty',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('cluster price catalog management requires separation of duty and quota', async () => {
|
||||
const order = [];
|
||||
const audits = [];
|
||||
const result = await bootstrapModelPriceCatalogManagementProfile({
|
||||
enabled: true,
|
||||
profile: 'cluster',
|
||||
async loadAuthority() {
|
||||
order.push('authority');
|
||||
return modelPriceCatalogManagementAuthority({
|
||||
quota: {
|
||||
async consume() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
close() {
|
||||
order.push('authority.close');
|
||||
},
|
||||
});
|
||||
},
|
||||
audit(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'active');
|
||||
assert.equal(result.decisionMode, 'separation_of_duty');
|
||||
assert.equal(result.capability.decisionMode, 'separation_of_duty');
|
||||
assert.equal(await result.capability.stop(), 'stopped');
|
||||
assert.deepEqual(order, ['authority', 'authority.close']);
|
||||
assert.deepEqual(
|
||||
audits.map(({ state }) => state),
|
||||
['authority_ready', 'active', 'stopped'],
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { chmod, mkdtemp, writeFile } = require('node:fs/promises');
|
||||
const { tmpdir } = require('node:os');
|
||||
const { join } = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
PROJECTED_MODEL_GATEWAY_AUTHORITY_SCHEMA,
|
||||
ProjectedModelGatewayAuthorityUnavailableError,
|
||||
canonicalProjectedModelGatewayAuthorityManifest,
|
||||
loadProjectedModelGatewayProviderAuthority,
|
||||
} = require('../dist/model-gateway/projectedModelGatewayAuthority.js');
|
||||
|
||||
function manifest(overrides = {}) {
|
||||
return {
|
||||
schema: PROJECTED_MODEL_GATEWAY_AUTHORITY_SCHEMA,
|
||||
providers: [
|
||||
{
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'https://models.example.test/v1/',
|
||||
allowPlaintextLoopback: false,
|
||||
maxResponseBytes: 1048576,
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
projectId: 'project-a',
|
||||
policy: {
|
||||
revision: 'policy-v1',
|
||||
allowedProviders: ['openai-compatible'],
|
||||
allowedModels: ['vendor/model-a'],
|
||||
maxInputBytes: 4096,
|
||||
maxOutputBytes: 4096,
|
||||
maxOutputTokens: 256,
|
||||
maxTotalTokens: 1024,
|
||||
maxCostMicros: null,
|
||||
priceRevision: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function authorityFile(value, mode = 0o440) {
|
||||
const root = await mkdtemp(join(tmpdir(), 'ql3-ai-authority-'));
|
||||
const file = join(root, 'authority.json');
|
||||
await writeFile(
|
||||
file,
|
||||
canonicalProjectedModelGatewayAuthorityManifest(value),
|
||||
{ mode },
|
||||
);
|
||||
await chmod(file, mode);
|
||||
return file;
|
||||
}
|
||||
|
||||
const credentials = Object.freeze({
|
||||
async authorizationHeader() {
|
||||
throw new Error('not invoked while loading authority');
|
||||
},
|
||||
});
|
||||
|
||||
test('projected authority loads one canonical provider and Project policy', async () => {
|
||||
const configFile = await authorityFile(manifest());
|
||||
const authority = await loadProjectedModelGatewayProviderAuthority({
|
||||
configFile,
|
||||
credentials,
|
||||
});
|
||||
assert.deepEqual(
|
||||
authority.providers.map(({ type }) => type),
|
||||
['openai-compatible'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
await authority.policies.resolve({ projectId: 'project-a' }),
|
||||
manifest().projects[0].policy,
|
||||
);
|
||||
await assert.rejects(
|
||||
authority.policies.resolve({ projectId: 'project-b' }),
|
||||
ProjectedModelGatewayAuthorityUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('projected authority rejects provider/policy drift before network access', async () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
canonicalProjectedModelGatewayAuthorityManifest(
|
||||
manifest({
|
||||
projects: [
|
||||
{
|
||||
...manifest().projects[0],
|
||||
policy: {
|
||||
...manifest().projects[0].policy,
|
||||
allowedProviders: ['missing-provider'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
ProjectedModelGatewayAuthorityUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('projected authority rejects writable or noncanonical JSON', async () => {
|
||||
const writable = await authorityFile(manifest(), 0o640);
|
||||
await assert.rejects(
|
||||
loadProjectedModelGatewayProviderAuthority({
|
||||
configFile: writable,
|
||||
credentials,
|
||||
}),
|
||||
ProjectedModelGatewayAuthorityUnavailableError,
|
||||
);
|
||||
const root = await mkdtemp(join(tmpdir(), 'ql3-ai-authority-'));
|
||||
const noncanonical = join(root, 'authority.json');
|
||||
await writeFile(noncanonical, JSON.stringify(manifest(), null, 2), {
|
||||
mode: 0o440,
|
||||
});
|
||||
await chmod(noncanonical, 0o440);
|
||||
await assert.rejects(
|
||||
loadProjectedModelGatewayProviderAuthority({
|
||||
configFile: noncanonical,
|
||||
credentials,
|
||||
}),
|
||||
ProjectedModelGatewayAuthorityUnavailableError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
chmod,
|
||||
symlink,
|
||||
unlink,
|
||||
writeFile,
|
||||
} = require('node:fs/promises');
|
||||
const { tmpdir } = require('node:os');
|
||||
const { join } = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
ProjectedModelProviderSecretMaterialUnavailableError,
|
||||
createProjectedModelProviderSecretMaterialProvider,
|
||||
projectedModelProviderSecretFileName,
|
||||
} = require('../dist/model-provider-credential/projectedModelProviderSecretMaterial.js');
|
||||
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||
|
||||
const SECRET_REF = createSecretRef({
|
||||
projectId: 'project-a',
|
||||
name: 'openai-token',
|
||||
});
|
||||
|
||||
async function generation(root, name, value, mode = 0o440) {
|
||||
const directory = join(root, name);
|
||||
await mkdir(directory);
|
||||
const path = join(
|
||||
directory,
|
||||
projectedModelProviderSecretFileName(SECRET_REF),
|
||||
);
|
||||
await writeFile(path, value, { mode });
|
||||
await chmod(path, mode);
|
||||
}
|
||||
|
||||
async function atomicProjection(root, activeGeneration) {
|
||||
const fileName = projectedModelProviderSecretFileName(SECRET_REF);
|
||||
await symlink(activeGeneration, join(root, '..data'));
|
||||
await symlink(join('..data', fileName), join(root, fileName));
|
||||
}
|
||||
|
||||
test('projected credential material follows atomic rotation and wipes owned bytes', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'ql3-ai-credential-'));
|
||||
await generation(root, '..data-one', 'token-one');
|
||||
await generation(root, '..data-two', 'token-two');
|
||||
await atomicProjection(root, '..data-one');
|
||||
const provider = await createProjectedModelProviderSecretMaterialProvider({
|
||||
rootDirectory: root,
|
||||
});
|
||||
|
||||
const first = await provider.resolveProjectSecretMaterial({
|
||||
projectId: 'project-a',
|
||||
secretRef: SECRET_REF,
|
||||
});
|
||||
assert.equal(Buffer.from(first.bytes).toString('utf8'), 'token-one');
|
||||
const owned = first.bytes;
|
||||
await first.dispose();
|
||||
assert.deepEqual([...owned], Array(9).fill(0));
|
||||
|
||||
await unlink(join(root, '..data'));
|
||||
await symlink('..data-two', join(root, '..data'));
|
||||
const second = await provider.resolveProjectSecretMaterial({
|
||||
projectId: 'project-a',
|
||||
secretRef: SECRET_REF,
|
||||
});
|
||||
assert.equal(Buffer.from(second.bytes).toString('utf8'), 'token-two');
|
||||
await second.dispose();
|
||||
});
|
||||
|
||||
test('projected credential material rejects Project drift and writable material', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'ql3-ai-credential-'));
|
||||
await generation(root, '..data-one', 'unsafe-token', 0o640);
|
||||
await atomicProjection(root, '..data-one');
|
||||
const provider = await createProjectedModelProviderSecretMaterialProvider({
|
||||
rootDirectory: root,
|
||||
});
|
||||
await assert.rejects(
|
||||
provider.resolveProjectSecretMaterial({
|
||||
projectId: 'project-b',
|
||||
secretRef: SECRET_REF,
|
||||
}),
|
||||
ProjectedModelProviderSecretMaterialUnavailableError,
|
||||
);
|
||||
await assert.rejects(
|
||||
provider.resolveProjectSecretMaterial({
|
||||
projectId: 'project-a',
|
||||
secretRef: SECRET_REF,
|
||||
}),
|
||||
ProjectedModelProviderSecretMaterialUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('projected credential material rejects symlink root', async () => {
|
||||
const direct = await mkdtemp(join(tmpdir(), 'ql3-ai-credential-'));
|
||||
const parent = await mkdtemp(join(tmpdir(), 'ql3-ai-credential-link-'));
|
||||
const linked = join(parent, 'root');
|
||||
await symlink(direct, linked);
|
||||
await assert.rejects(
|
||||
createProjectedModelProviderSecretMaterialProvider({
|
||||
rootDirectory: linked,
|
||||
}),
|
||||
ProjectedModelProviderSecretMaterialUnavailableError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,365 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||
const {
|
||||
BoundModelProviderCredentialProvider,
|
||||
InvalidModelProviderCredentialBindingError,
|
||||
MODEL_PROVIDER_CREDENTIAL_AUDIT_SCHEMA,
|
||||
MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
ModelProviderCredentialUnavailableError,
|
||||
digestModelProviderCredentialBinding,
|
||||
normalizeModelProviderCredentialBinding,
|
||||
} = require('../dist/model-provider-credential/providerCredential.js');
|
||||
const {
|
||||
OpenAiCompatibleProvider,
|
||||
} = require('../dist/model-gateway/openAiCompatibleProvider.js');
|
||||
const credentialSubpath = require('@qinglong/ai/provider-credential');
|
||||
|
||||
const SECRET_REF = createSecretRef({
|
||||
projectId: 'project-a',
|
||||
name: 'OPENAI_API_KEY',
|
||||
});
|
||||
|
||||
function binding(overrides = {}) {
|
||||
return {
|
||||
schema: MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
projectId: 'project-a',
|
||||
provider: 'openai-compatible',
|
||||
revision: 'credential-binding-v1',
|
||||
secretRef: SECRET_REF,
|
||||
scheme: 'bearer',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function authorizationRequest(overrides = {}) {
|
||||
return {
|
||||
operation: 'generate',
|
||||
projectId: 'project-a',
|
||||
provider: 'openai-compatible',
|
||||
requestId: 'request-a',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('publishes credential binding through one explicit AI package subpath', () => {
|
||||
assert.equal(
|
||||
credentialSubpath.BoundModelProviderCredentialProvider,
|
||||
BoundModelProviderCredentialProvider,
|
||||
);
|
||||
assert.equal(
|
||||
credentialSubpath.MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
MODEL_PROVIDER_CREDENTIAL_BINDING_SCHEMA,
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizes one canonical Project-bound binding and derives a stable content digest', () => {
|
||||
const normalized = normalizeModelProviderCredentialBinding(binding());
|
||||
assert.equal(Object.isFrozen(normalized), true);
|
||||
assert.deepEqual(normalized, binding());
|
||||
assert.match(
|
||||
digestModelProviderCredentialBinding(normalized),
|
||||
/^sha256:[a-f0-9]{64}$/,
|
||||
);
|
||||
assert.equal(
|
||||
digestModelProviderCredentialBinding(normalized),
|
||||
digestModelProviderCredentialBinding(binding()),
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelProviderCredentialBinding(
|
||||
binding({
|
||||
secretRef: createSecretRef({
|
||||
projectId: 'project-b',
|
||||
name: 'OPENAI_API_KEY',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
InvalidModelProviderCredentialBindingError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelProviderCredentialBinding({ ...binding(), extra: true }),
|
||||
InvalidModelProviderCredentialBindingError,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolves, audits and disposes one short-lived bearer credential without content leakage', async () => {
|
||||
const lookups = [];
|
||||
const resolutions = [];
|
||||
const audits = [];
|
||||
let sourceDisposed = 0;
|
||||
let sourceBytes;
|
||||
const credentials = new BoundModelProviderCredentialProvider({
|
||||
bindings: {
|
||||
async resolveModelProviderCredentialBinding(lookup) {
|
||||
lookups.push(lookup);
|
||||
return binding();
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
async resolveProjectSecretMaterial(request) {
|
||||
resolutions.push(request);
|
||||
sourceBytes = Buffer.from('sk-ephemeral_123', 'ascii');
|
||||
return {
|
||||
secretRef: request.secretRef,
|
||||
bytes: sourceBytes,
|
||||
dispose() {
|
||||
sourceDisposed += 1;
|
||||
sourceBytes.fill(0);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
audit: {
|
||||
async record(record) {
|
||||
audits.push(record);
|
||||
},
|
||||
},
|
||||
now: () => 1234,
|
||||
});
|
||||
|
||||
const lease = await credentials.authorizationHeader(authorizationRequest());
|
||||
assert.equal(lease.value, 'Bearer sk-ephemeral_123');
|
||||
assert.equal(sourceDisposed, 1);
|
||||
assert.deepEqual([...sourceBytes], new Array(sourceBytes.length).fill(0));
|
||||
assert.deepEqual(lookups, [
|
||||
{ projectId: 'project-a', provider: 'openai-compatible' },
|
||||
]);
|
||||
assert.deepEqual(resolutions, [
|
||||
{ projectId: 'project-a', secretRef: SECRET_REF },
|
||||
]);
|
||||
assert.equal(audits.length, 1);
|
||||
assert.deepEqual(audits[0], {
|
||||
schema: MODEL_PROVIDER_CREDENTIAL_AUDIT_SCHEMA,
|
||||
operation: 'generate',
|
||||
projectId: 'project-a',
|
||||
provider: 'openai-compatible',
|
||||
requestId: 'request-a',
|
||||
bindingRevision: 'credential-binding-v1',
|
||||
bindingDigest: digestModelProviderCredentialBinding(binding()),
|
||||
occurredAtMs: 1234,
|
||||
});
|
||||
assert.equal(JSON.stringify(audits).includes('ephemeral'), false);
|
||||
await lease.dispose();
|
||||
assert.throws(() => lease.value, ModelProviderCredentialUnavailableError);
|
||||
await lease.dispose();
|
||||
});
|
||||
|
||||
test('re-resolves an unversioned SecretRef on every operation without a cache or watcher', async () => {
|
||||
const values = ['token-a', 'token-b'];
|
||||
let resolved = 0;
|
||||
const credentials = new BoundModelProviderCredentialProvider({
|
||||
bindings: {
|
||||
async resolveModelProviderCredentialBinding() {
|
||||
return binding();
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
async resolveProjectSecretMaterial(request) {
|
||||
const bytes = Buffer.from(values[resolved++], 'ascii');
|
||||
return {
|
||||
secretRef: request.secretRef,
|
||||
bytes,
|
||||
dispose() {
|
||||
bytes.fill(0);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
audit: { async record() {} },
|
||||
});
|
||||
|
||||
const first = await credentials.authorizationHeader(authorizationRequest());
|
||||
const second = await credentials.authorizationHeader(
|
||||
authorizationRequest({ operation: 'stream', requestId: 'request-b' }),
|
||||
);
|
||||
assert.equal(first.value, 'Bearer token-a');
|
||||
assert.equal(second.value, 'Bearer token-b');
|
||||
assert.equal(resolved, 2);
|
||||
await first.dispose();
|
||||
await second.dispose();
|
||||
});
|
||||
|
||||
test('fails closed on missing or drifted bindings and disposes material when audit fails', async () => {
|
||||
let secretCalls = 0;
|
||||
const missing = new BoundModelProviderCredentialProvider({
|
||||
bindings: {
|
||||
async resolveModelProviderCredentialBinding() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
async resolveProjectSecretMaterial() {
|
||||
secretCalls += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
},
|
||||
audit: { async record() {} },
|
||||
});
|
||||
await assert.rejects(
|
||||
missing.authorizationHeader(authorizationRequest()),
|
||||
ModelProviderCredentialUnavailableError,
|
||||
);
|
||||
assert.equal(secretCalls, 0);
|
||||
|
||||
const drifted = new BoundModelProviderCredentialProvider({
|
||||
bindings: {
|
||||
async resolveModelProviderCredentialBinding() {
|
||||
return binding({ provider: 'another-provider' });
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
async resolveProjectSecretMaterial() {
|
||||
secretCalls += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
},
|
||||
audit: { async record() {} },
|
||||
});
|
||||
await assert.rejects(
|
||||
drifted.authorizationHeader(authorizationRequest()),
|
||||
ModelProviderCredentialUnavailableError,
|
||||
);
|
||||
assert.equal(secretCalls, 0);
|
||||
|
||||
let disposed = 0;
|
||||
const bytes = Buffer.from('token-a', 'ascii');
|
||||
const unavailableAudit = new BoundModelProviderCredentialProvider({
|
||||
bindings: {
|
||||
async resolveModelProviderCredentialBinding() {
|
||||
return binding();
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
async resolveProjectSecretMaterial(request) {
|
||||
return {
|
||||
secretRef: request.secretRef,
|
||||
bytes,
|
||||
dispose() {
|
||||
disposed += 1;
|
||||
bytes.fill(0);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
audit: {
|
||||
async record() {
|
||||
throw new Error('audit unavailable');
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
unavailableAudit.authorizationHeader(authorizationRequest()),
|
||||
ModelProviderCredentialUnavailableError,
|
||||
);
|
||||
assert.equal(disposed, 1);
|
||||
assert.deepEqual([...bytes], new Array(bytes.length).fill(0));
|
||||
});
|
||||
|
||||
test('credential audit failure prevents OpenAI-compatible network access', async () => {
|
||||
let fetchCalls = 0;
|
||||
let disposed = 0;
|
||||
const credentials = new BoundModelProviderCredentialProvider({
|
||||
bindings: {
|
||||
async resolveModelProviderCredentialBinding() {
|
||||
return binding();
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
async resolveProjectSecretMaterial(request) {
|
||||
const bytes = Buffer.from('token-a', 'ascii');
|
||||
return {
|
||||
secretRef: request.secretRef,
|
||||
bytes,
|
||||
dispose() {
|
||||
disposed += 1;
|
||||
bytes.fill(0);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
audit: {
|
||||
async record() {
|
||||
throw new Error('audit unavailable');
|
||||
},
|
||||
},
|
||||
});
|
||||
const provider = new OpenAiCompatibleProvider({
|
||||
type: 'openai-compatible',
|
||||
baseUrl: 'https://models.example.test/v1/',
|
||||
credentials,
|
||||
async fetch() {
|
||||
fetchCalls += 1;
|
||||
throw new Error('must not run');
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
provider.generate(
|
||||
{
|
||||
provider: 'openai-compatible',
|
||||
model: 'model-a',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
maxOutputTokens: 16,
|
||||
},
|
||||
{
|
||||
projectId: 'project-a',
|
||||
runId: 'run-a',
|
||||
stepRunId: 'step-a',
|
||||
traceId: 'trace-a',
|
||||
requestId: 'request-a',
|
||||
deadlineAtMs: Date.now() + 10_000,
|
||||
},
|
||||
),
|
||||
ModelProviderCredentialUnavailableError,
|
||||
);
|
||||
assert.equal(fetchCalls, 0);
|
||||
assert.equal(disposed, 1);
|
||||
});
|
||||
|
||||
test('rejects unscoped model listing and malformed or oversized Secret material', async () => {
|
||||
const materials = [
|
||||
Buffer.from('token with spaces', 'ascii'),
|
||||
Buffer.alloc(4096, 0x61),
|
||||
Buffer.from([0xff, 0xfe]),
|
||||
];
|
||||
let disposed = 0;
|
||||
const credentials = new BoundModelProviderCredentialProvider({
|
||||
bindings: {
|
||||
async resolveModelProviderCredentialBinding() {
|
||||
return binding();
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
async resolveProjectSecretMaterial(request) {
|
||||
const bytes = materials.shift();
|
||||
return {
|
||||
secretRef: request.secretRef,
|
||||
bytes,
|
||||
dispose() {
|
||||
disposed += 1;
|
||||
bytes.fill(0);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
audit: { async record() {} },
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
credentials.authorizationHeader({
|
||||
operation: 'list_models',
|
||||
provider: 'openai-compatible',
|
||||
}),
|
||||
InvalidModelProviderCredentialBindingError,
|
||||
);
|
||||
for (const requestId of ['malformed-a', 'malformed-b', 'malformed-c']) {
|
||||
await assert.rejects(
|
||||
credentials.authorizationHeader(authorizationRequest({ requestId })),
|
||||
ModelProviderCredentialUnavailableError,
|
||||
);
|
||||
}
|
||||
assert.equal(disposed, 3);
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
createStepRunRecord,
|
||||
transitionStepRunMutation,
|
||||
} = require('@qinglong/runtime-core/step-run');
|
||||
const {
|
||||
createModelInvocationCompletionCommand,
|
||||
createModelInvocationMutationIdentity,
|
||||
createModelInvocationStartCommand,
|
||||
} = require('../dist/model-invocation/modelInvocation.js');
|
||||
const {
|
||||
InvalidModelInvocationUsageLedgerError,
|
||||
MAX_MODEL_INVOCATION_USAGE_QUERY_WINDOW_MS,
|
||||
MODEL_INVOCATION_USAGE_LEDGER_SCHEMA,
|
||||
createModelInvocationUsageLedgerRecord,
|
||||
normalizeModelInvocationUsageLedgerQuery,
|
||||
normalizeModelInvocationUsageLedgerRecord,
|
||||
normalizeModelInvocationUsageLedgerSummaryQuery,
|
||||
} = require('@qinglong/ai/usage-ledger');
|
||||
|
||||
const NOW = 1_000_000;
|
||||
|
||||
function audit(phase, overrides = {}) {
|
||||
return {
|
||||
phase,
|
||||
projectId: 'project-a',
|
||||
runId: 'run-a',
|
||||
stepRunId: 'step-a',
|
||||
traceId: 'trace-a',
|
||||
requestId: 'request-a',
|
||||
provider: 'remote',
|
||||
model: 'model-a',
|
||||
policyRevision: 'policy-1',
|
||||
requestDigest: `sha256:${'b'.repeat(64)}`,
|
||||
deadlineAtMs: NOW + 10_000,
|
||||
inputBytes: 128,
|
||||
maxOutputTokens: 64,
|
||||
outputBytes: 0,
|
||||
usage: null,
|
||||
errorCode: null,
|
||||
occurredAtMs: NOW,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function startFixture(projectId = 'project-a') {
|
||||
const current = createStepRunRecord({
|
||||
id: 'step-a',
|
||||
runId: 'run-a',
|
||||
stepKey: 'summarize',
|
||||
kind: 'model',
|
||||
definitionRef: 'prompt:summary@1',
|
||||
definitionDigest: 'a'.repeat(64),
|
||||
required: true,
|
||||
initialStatus: 'ready',
|
||||
inputRef: 'artifact:input-a',
|
||||
mutationId: 'create-step-a',
|
||||
createdAtMs: NOW - 1,
|
||||
});
|
||||
const identity = createModelInvocationMutationIdentity('request-a', 'start');
|
||||
const mutation = transitionStepRunMutation(
|
||||
current,
|
||||
{
|
||||
expectedVersion: current.version,
|
||||
expectedDigest: current.stepRunDigest,
|
||||
mutationId: identity.mutationId,
|
||||
to: 'running',
|
||||
atMs: NOW,
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 1,
|
||||
expectedRunEventSequence: 1,
|
||||
eventId: identity.eventId,
|
||||
dedupeKey: identity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
);
|
||||
return createModelInvocationStartCommand(
|
||||
audit('admitted', { projectId }),
|
||||
mutation,
|
||||
);
|
||||
}
|
||||
|
||||
function completionFixture({
|
||||
errorCode = null,
|
||||
usage = { inputTokens: 5, outputTokens: 2, totalTokens: 7, costMicros: 11 },
|
||||
} = {}) {
|
||||
const startCommand = startFixture();
|
||||
const identity = createModelInvocationMutationIdentity(
|
||||
'request-a',
|
||||
'completion',
|
||||
);
|
||||
const failed = errorCode !== null;
|
||||
const mutation = transitionStepRunMutation(
|
||||
startCommand.stepRunMutation.stepRun,
|
||||
{
|
||||
expectedVersion: startCommand.start.startedStepRunVersion,
|
||||
expectedDigest: startCommand.start.startedStepRunDigest,
|
||||
mutationId: identity.mutationId,
|
||||
to: failed ? 'failed' : 'succeeded',
|
||||
atMs: NOW + 25,
|
||||
...(failed
|
||||
? {
|
||||
resultCode: 'model_provider_failed',
|
||||
errorSummary: 'Model invocation failed',
|
||||
}
|
||||
: { outputRef: 'model-invocation:request-a' }),
|
||||
},
|
||||
{
|
||||
expectedRunVersion: 2,
|
||||
expectedRunEventSequence: 2,
|
||||
eventId: identity.eventId,
|
||||
dedupeKey: identity.dedupeKey,
|
||||
actor: { type: 'executor', id: 'model-gateway' },
|
||||
},
|
||||
);
|
||||
return createModelInvocationCompletionCommand(
|
||||
startCommand.start,
|
||||
audit(failed ? 'failed' : 'completed', {
|
||||
occurredAtMs: NOW + 25,
|
||||
outputBytes: failed ? 0 : 12,
|
||||
usage,
|
||||
errorCode,
|
||||
}),
|
||||
mutation,
|
||||
);
|
||||
}
|
||||
|
||||
test('usage ledger derives one immutable content-free billing fact', () => {
|
||||
const start = startFixture().start;
|
||||
const completion = completionFixture().completion;
|
||||
const ledger = createModelInvocationUsageLedgerRecord(start, completion);
|
||||
|
||||
assert.ok(ledger);
|
||||
assert.equal(ledger.schema, MODEL_INVOCATION_USAGE_LEDGER_SCHEMA);
|
||||
assert.equal(ledger.invocationId, completion.invocationId);
|
||||
assert.equal(ledger.completionDigest, completion.completionDigest);
|
||||
assert.equal(ledger.provider, start.provider);
|
||||
assert.equal(ledger.model, start.model);
|
||||
assert.equal(ledger.policyRevision, start.policyRevision);
|
||||
assert.equal(ledger.outcome, 'succeeded');
|
||||
assert.equal(ledger.inputTokens, 5);
|
||||
assert.equal(ledger.outputTokens, 2);
|
||||
assert.equal(ledger.totalTokens, 7);
|
||||
assert.equal(ledger.costMicros, 11);
|
||||
assert.deepEqual(
|
||||
createModelInvocationUsageLedgerRecord(start, completion),
|
||||
ledger,
|
||||
);
|
||||
assert.deepEqual(normalizeModelInvocationUsageLedgerRecord(ledger), ledger);
|
||||
assert.equal(Object.isFrozen(ledger), true);
|
||||
assert.equal(JSON.stringify(ledger).includes('top secret prompt'), false);
|
||||
});
|
||||
|
||||
test('failed provider completion remains billable when usage is known', () => {
|
||||
const start = startFixture().start;
|
||||
const completion = completionFixture({
|
||||
errorCode: 'MODEL_PROVIDER_FAILED',
|
||||
usage: { inputTokens: 5, outputTokens: 0, totalTokens: 5 },
|
||||
}).completion;
|
||||
const ledger = createModelInvocationUsageLedgerRecord(start, completion);
|
||||
|
||||
assert.ok(ledger);
|
||||
assert.equal(ledger.outcome, 'failed');
|
||||
assert.equal(ledger.totalTokens, 5);
|
||||
assert.equal(ledger.costMicros, null);
|
||||
});
|
||||
|
||||
test('completion without usage creates no synthetic zero-cost fact', () => {
|
||||
const start = startFixture().start;
|
||||
const completion = completionFixture({
|
||||
errorCode: 'MODEL_PROVIDER_FAILED',
|
||||
usage: null,
|
||||
}).completion;
|
||||
|
||||
assert.equal(createModelInvocationUsageLedgerRecord(start, completion), null);
|
||||
});
|
||||
|
||||
test('detached completion and digest tampering fail closed', () => {
|
||||
const start = startFixture().start;
|
||||
const completion = completionFixture().completion;
|
||||
const ledger = createModelInvocationUsageLedgerRecord(start, completion);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
createModelInvocationUsageLedgerRecord(
|
||||
startFixture('project-b').start,
|
||||
completion,
|
||||
),
|
||||
InvalidModelInvocationUsageLedgerError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelInvocationUsageLedgerRecord({
|
||||
...ledger,
|
||||
totalTokens: ledger.totalTokens + 1,
|
||||
}),
|
||||
InvalidModelInvocationUsageLedgerError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelInvocationUsageLedgerRecord({
|
||||
...ledger,
|
||||
ledgerDigest: '0'.repeat(64),
|
||||
}),
|
||||
InvalidModelInvocationUsageLedgerError,
|
||||
);
|
||||
});
|
||||
|
||||
test('Project usage query is bounded and cursor stays inside its window', () => {
|
||||
const query = {
|
||||
projectId: 'project-a',
|
||||
fromMsInclusive: NOW,
|
||||
toMsExclusive: NOW + 100,
|
||||
limit: 128,
|
||||
after: { settledAtMs: NOW + 25, invocationId: 'request-a' },
|
||||
};
|
||||
|
||||
assert.deepEqual(normalizeModelInvocationUsageLedgerQuery(query), query);
|
||||
assert.deepEqual(
|
||||
normalizeModelInvocationUsageLedgerSummaryQuery({
|
||||
projectId: 'project-a',
|
||||
fromMsInclusive: NOW,
|
||||
toMsExclusive: NOW + 100,
|
||||
}),
|
||||
{
|
||||
projectId: 'project-a',
|
||||
fromMsInclusive: NOW,
|
||||
toMsExclusive: NOW + 100,
|
||||
},
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelInvocationUsageLedgerQuery({
|
||||
...query,
|
||||
toMsExclusive:
|
||||
query.fromMsInclusive +
|
||||
MAX_MODEL_INVOCATION_USAGE_QUERY_WINDOW_MS +
|
||||
1,
|
||||
}),
|
||||
InvalidModelInvocationUsageLedgerError,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelInvocationUsageLedgerQuery({
|
||||
...query,
|
||||
after: { ...query.after, settledAtMs: NOW - 1 },
|
||||
}),
|
||||
InvalidModelInvocationUsageLedgerError,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeModelInvocationUsageLedgerQuery({ ...query, limit: 129 }),
|
||||
InvalidModelInvocationUsageLedgerError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
InvalidModelInvocationQuotaError,
|
||||
ModelInvocationQuotaConfigurationError,
|
||||
createModelInvocationQuotaAdmission,
|
||||
createModelInvocationQuotaReservation,
|
||||
createModelInvocationQuotaSettlement,
|
||||
normalizeModelInvocationProjectQuotaPolicy,
|
||||
normalizeModelInvocationQuotaAdmission,
|
||||
normalizeModelInvocationQuotaReservation,
|
||||
} = require('../dist/usage/usageQuota.js');
|
||||
|
||||
const quota = Object.freeze({
|
||||
revision: 'quota-1',
|
||||
windowMs: 3_600_000,
|
||||
maxInvocations: 10,
|
||||
maxTokens: 10_000,
|
||||
maxCostMicros: 50_000,
|
||||
});
|
||||
|
||||
function admission(overrides = {}) {
|
||||
return createModelInvocationQuotaAdmission({
|
||||
invocationId: 'invocation-a',
|
||||
projectId: 'project-a',
|
||||
modelPolicyRevision: 'model-policy-1',
|
||||
reservedTokens: 1_000,
|
||||
reservedCostMicros: 5_000,
|
||||
quota,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function completion(overrides = {}) {
|
||||
return {
|
||||
invocationId: 'invocation-a',
|
||||
projectId: 'project-a',
|
||||
completionDigest: 'c'.repeat(64),
|
||||
completedAtMs: 3_600_200,
|
||||
usage: {
|
||||
inputTokens: 120,
|
||||
outputTokens: 30,
|
||||
totalTokens: 150,
|
||||
costMicros: 700,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('quota admission reserves one bounded call in a database-aligned window', () => {
|
||||
const value = admission();
|
||||
const reservation = createModelInvocationQuotaReservation(value, 3_600_123);
|
||||
|
||||
assert.deepEqual(normalizeModelInvocationProjectQuotaPolicy(quota), quota);
|
||||
assert.deepEqual(normalizeModelInvocationQuotaAdmission(value), value);
|
||||
assert.deepEqual(
|
||||
normalizeModelInvocationQuotaReservation(reservation),
|
||||
reservation,
|
||||
);
|
||||
assert.equal(reservation.windowStartMs, 3_600_000);
|
||||
assert.equal(reservation.windowEndMs, 7_200_000);
|
||||
assert.match(reservation.admissionDigest, /^[0-9a-f]{64}$/);
|
||||
assert.match(reservation.reservationDigest, /^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test('known usage settles actual consumption and releases unused capacity', () => {
|
||||
const reservation = createModelInvocationQuotaReservation(
|
||||
admission(),
|
||||
3_600_123,
|
||||
);
|
||||
const settlement = createModelInvocationQuotaSettlement(
|
||||
reservation,
|
||||
completion(),
|
||||
);
|
||||
|
||||
assert.equal(settlement.effectiveTokens, 150);
|
||||
assert.equal(settlement.effectiveCostMicros, 700);
|
||||
assert.equal(settlement.retainedTokenReservation, false);
|
||||
assert.equal(settlement.retainedCostReservation, false);
|
||||
});
|
||||
|
||||
test('unknown usage retains the full reservation', () => {
|
||||
const reservation = createModelInvocationQuotaReservation(
|
||||
admission(),
|
||||
3_600_123,
|
||||
);
|
||||
const settlement = createModelInvocationQuotaSettlement(
|
||||
reservation,
|
||||
completion({ usage: null }),
|
||||
);
|
||||
|
||||
assert.equal(settlement.effectiveTokens, 1_000);
|
||||
assert.equal(settlement.effectiveCostMicros, 5_000);
|
||||
assert.equal(settlement.retainedTokenReservation, true);
|
||||
assert.equal(settlement.retainedCostReservation, true);
|
||||
});
|
||||
|
||||
test('known tokens with unknown cost retain only the cost reservation', () => {
|
||||
const reservation = createModelInvocationQuotaReservation(
|
||||
admission(),
|
||||
3_600_123,
|
||||
);
|
||||
const settlement = createModelInvocationQuotaSettlement(
|
||||
reservation,
|
||||
completion({
|
||||
usage: { inputTokens: 120, outputTokens: 30, totalTokens: 150 },
|
||||
}),
|
||||
);
|
||||
|
||||
assert.equal(settlement.effectiveTokens, 150);
|
||||
assert.equal(settlement.effectiveCostMicros, 5_000);
|
||||
assert.equal(settlement.retainedTokenReservation, false);
|
||||
assert.equal(settlement.retainedCostReservation, true);
|
||||
});
|
||||
|
||||
test('cost-disabled quota leaves billing cost to the usage ledger', () => {
|
||||
const reservation = createModelInvocationQuotaReservation(
|
||||
admission({
|
||||
reservedCostMicros: null,
|
||||
quota: { ...quota, maxCostMicros: null },
|
||||
}),
|
||||
3_600_123,
|
||||
);
|
||||
const settlement = createModelInvocationQuotaSettlement(
|
||||
reservation,
|
||||
completion(),
|
||||
);
|
||||
|
||||
assert.equal(settlement.effectiveTokens, 150);
|
||||
assert.equal(settlement.effectiveCostMicros, null);
|
||||
assert.equal(settlement.retainedCostReservation, false);
|
||||
});
|
||||
|
||||
test('cost quota without a per-call cost ceiling fails closed', () => {
|
||||
assert.throws(
|
||||
() => admission({ reservedCostMicros: null }),
|
||||
ModelInvocationQuotaConfigurationError,
|
||||
);
|
||||
});
|
||||
|
||||
test('tampering and unsupported windows fail closed', () => {
|
||||
const value = admission();
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeModelInvocationQuotaAdmission({
|
||||
...value,
|
||||
reservedTokens: value.reservedTokens + 1,
|
||||
}),
|
||||
InvalidModelInvocationQuotaError,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeModelInvocationProjectQuotaPolicy({ ...quota, windowMs: 7 }),
|
||||
InvalidModelInvocationQuotaError,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user