Files
waggle-os/packages/core/tests/embedding-provider-quota.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

268 lines
9.8 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import Database from 'better-sqlite3';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createEmbeddingProvider, EmbeddingQuotaExceededError, getMinimumTierForProvider } from '@waggle/hive-mind-core';
import { TierError, TIER_CAPABILITIES } from '@waggle/shared';
describe('Embedding Provider — Tier & Quota Enforcement', () => {
let tmpDir: string;
let db: InstanceType<typeof Database>;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-embed-test-'));
db = new Database(path.join(tmpDir, 'quota.db'));
});
afterEach(() => {
db.close();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('Tier enforcement on provider selection', () => {
it('FREE user requesting litellm throws TierError (litellm stays Team-only)', async () => {
// Solo (FREE) unlocks BYO cloud embeddings (voyage/openai) but NOT the
// managed litellm router — that stays a paid-tier provider.
await expect(
createEmbeddingProvider({
provider: 'litellm',
userTier: 'FREE',
quotaDb: db,
})
).rejects.toThrow(TierError);
});
it('FREE user requesting inprocess succeeds', async () => {
// inprocess may fail to load ONNX in test env, but should NOT throw TierError
try {
await createEmbeddingProvider({
provider: 'inprocess',
userTier: 'FREE',
quotaDb: db,
});
} catch (err) {
// If it fails, it should NOT be a TierError — it should be a probe failure
expect(err).not.toBeInstanceOf(TierError);
}
});
it('FREE user requesting voyage does not throw TierError (Solo unlocks BYO cloud embeddings)', async () => {
// voyage will fail to connect (no real API), but should NOT throw TierError —
// Solo now allows the voyage/openai providers directly.
const provider = await createEmbeddingProvider({
provider: 'auto',
userTier: 'FREE',
quotaDb: db,
});
// Should fall back to mock (no real providers in test), but no TierError
expect(provider.getActiveProvider()).toBeDefined();
});
it('auto mode surfaces only key-backed providers for FREE tier', async () => {
const provider = await createEmbeddingProvider({
provider: 'auto',
userTier: 'FREE',
quotaDb: db,
});
// FREE allows inprocess, mock, ollama + cloud (voyage/openai); cloud is
// skipped here without API keys, so only local providers + mock surface.
const status = provider.getStatus();
for (const p of status.availableProviders) {
expect(['inprocess', 'mock', 'ollama']).toContain(p);
}
});
});
describe('Quota enforcement', () => {
// All current tiers have unlimited quotas (-1), so we patch FREE
// to a finite quota for these tests to exercise the quota mechanism.
const originalQuota = TIER_CAPABILITIES.FREE.embeddingQuotaPerMonth;
beforeEach(() => {
(TIER_CAPABILITIES.FREE as { embeddingQuotaPerMonth: number }).embeddingQuotaPerMonth = 500;
});
afterEach(() => {
(TIER_CAPABILITIES.FREE as { embeddingQuotaPerMonth: number }).embeddingQuotaPerMonth = originalQuota;
});
it('FREE user at 499 embeddings succeeds', async () => {
const provider = await createEmbeddingProvider({
provider: 'mock',
userTier: 'FREE',
userId: 'test-user',
quotaDb: db,
});
// Pre-fill 499 embeddings
const ym = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
db.prepare('INSERT INTO embedding_usage (user_id, year_month, count, updated_at) VALUES (?, ?, 499, ?)').run('test-user', ym, Date.now());
// 500th should succeed (499 + 1 = 500 = quota)
const result = await provider.embed('test text');
expect(result).toBeInstanceOf(Float32Array);
});
it('FREE user at 500 embeddings throws EmbeddingQuotaExceededError', async () => {
const provider = await createEmbeddingProvider({
provider: 'mock',
userTier: 'FREE',
userId: 'test-user',
quotaDb: db,
});
// Pre-fill to quota limit
const ym = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
db.prepare('INSERT INTO embedding_usage (user_id, year_month, count, updated_at) VALUES (?, ?, 500, ?)').run('test-user', ym, Date.now());
await expect(provider.embed('test text')).rejects.toThrow(EmbeddingQuotaExceededError);
});
it('TEAMS user with unlimited quota never throws quota error', async () => {
const provider = await createEmbeddingProvider({
provider: 'mock',
userTier: 'TEAMS',
userId: 'test-user',
quotaDb: db,
});
// Even with high usage, should succeed (TEAMS has -1 = unlimited)
const ym = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
db.prepare('INSERT INTO embedding_usage (user_id, year_month, count, updated_at) VALUES (?, ?, 999999, ?)').run('test-user', ym, Date.now());
const result = await provider.embed('test text');
expect(result).toBeInstanceOf(Float32Array);
});
});
describe('getQuotaStatus', () => {
const originalQuota = TIER_CAPABILITIES.FREE.embeddingQuotaPerMonth;
beforeEach(() => {
(TIER_CAPABILITIES.FREE as { embeddingQuotaPerMonth: number }).embeddingQuotaPerMonth = 500;
});
afterEach(() => {
(TIER_CAPABILITIES.FREE as { embeddingQuotaPerMonth: number }).embeddingQuotaPerMonth = originalQuota;
});
it('returns correct percentage for FREE user', async () => {
const provider = await createEmbeddingProvider({
provider: 'mock',
userTier: 'FREE',
userId: 'test-user',
quotaDb: db,
});
// Use 250 of 500 quota
const ym = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
db.prepare('INSERT INTO embedding_usage (user_id, year_month, count, updated_at) VALUES (?, ?, 250, ?)').run('test-user', ym, Date.now());
const status = provider.getQuotaStatus();
expect(status.tier).toBe('FREE');
expect(status.quota).toBe(500);
expect(status.used).toBe(250);
expect(status.remaining).toBe(250);
expect(status.percentage).toBe(50);
expect(status.resetsAt).toBeTruthy();
});
it('returns unlimited for TEAMS tier', async () => {
const provider = await createEmbeddingProvider({
provider: 'mock',
userTier: 'TEAMS',
userId: 'test-user',
quotaDb: db,
});
const status = provider.getQuotaStatus();
expect(status.tier).toBe('TEAMS');
expect(status.quota).toBe(-1);
expect(status.remaining).toBe(-1);
expect(status.percentage).toBe(0);
});
});
describe('WAGGLE_EVAL_MODE tier bypass (PA v5 §11.3)', () => {
// Defensive cleanup: never leak the flag across tests. If a prior run
// crashed mid-test, this block restores a known-clean baseline.
beforeEach(() => { delete process.env.WAGGLE_EVAL_MODE; });
afterEach(() => { delete process.env.WAGGLE_EVAL_MODE; });
it('without WAGGLE_EVAL_MODE: FREE + litellm still throws TierError (control)', async () => {
// Explicit sanity check that the normal gate is still live — baseline
// for the bypass test below. litellm is the provider FREE still lacks.
expect(process.env.WAGGLE_EVAL_MODE).toBeUndefined();
await expect(
createEmbeddingProvider({
provider: 'litellm',
userTier: 'FREE',
quotaDb: db,
})
).rejects.toThrow(TierError);
});
it('with WAGGLE_EVAL_MODE=1: FREE + litellm no longer throws TierError', async () => {
process.env.WAGGLE_EVAL_MODE = '1';
try {
await createEmbeddingProvider({
provider: 'litellm',
userTier: 'FREE',
quotaDb: db,
});
} catch (err) {
// Probe failure (no real voyage backend in tests) is fine — just not
// a TierError. The point: the tier gate is bypassed.
expect(err).not.toBeInstanceOf(TierError);
}
});
it('with WAGGLE_EVAL_MODE=1 and no userTier set: behaves as if unenforced', async () => {
process.env.WAGGLE_EVAL_MODE = '1';
const provider = await createEmbeddingProvider({
provider: 'auto',
quotaDb: db,
});
// Falls back to mock with no tier skip/probe; provider is constructed.
expect(provider.getActiveProvider()).toBeDefined();
});
it('only activates when env value is exactly "1" (defensive literal match)', async () => {
// Guard against accidental truthy-but-not-"1" values. Harness must use
// "1" exactly per §11.3.
for (const bad of ['true', 'yes', '0', '']) {
process.env.WAGGLE_EVAL_MODE = bad;
await expect(
createEmbeddingProvider({
provider: 'litellm',
userTier: 'FREE',
quotaDb: db,
})
).rejects.toThrow(TierError);
delete process.env.WAGGLE_EVAL_MODE;
}
});
});
describe('getMinimumTierForProvider', () => {
it('inprocess requires TRIAL (first tier that allows it)', () => {
expect(getMinimumTierForProvider('inprocess')).toBe('TRIAL');
});
it('voyage requires TRIAL (TRIAL unlocks all providers)', () => {
// TRIAL is the first tier in TIERS array and has all providers
expect(getMinimumTierForProvider('voyage')).toBe('TRIAL');
});
it('litellm requires TRIAL', () => {
// TRIAL is the first tier with litellm (all unlocked)
expect(getMinimumTierForProvider('litellm')).toBe('TRIAL');
});
it('mock requires TRIAL', () => {
expect(getMinimumTierForProvider('mock')).toBe('TRIAL');
});
});
});