mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
350 lines
10 KiB
JavaScript
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();
|
|
});
|