mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
256 lines
7.2 KiB
JavaScript
256 lines
7.2 KiB
JavaScript
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();
|
|
});
|