Files
qinglong/packages/ql3-ai/test/localModelProviderCredentialRepository.test.cjs
T

350 lines
10 KiB
JavaScript

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();
});