fix: clarify quota check is read-only, prevent refactoring pitfalls
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 46s
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 46s
- Rename checkAndReserve→checkQuota (method only reads, does not reserve) - Add doc comment: incrementJobCount is the actual quota reservation - Update user-ai.service.spec.ts references - Add comments for apiKeyMode/credentialId reassignment in budget fallback Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c88af39673
commit
7aea03f6e0
@ -11,8 +11,8 @@ export class UserAiQuotaService {
|
||||
return new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
||||
}
|
||||
|
||||
/** Check if user can create a new AI job, throw if over limit */
|
||||
async checkAndReserve(userId: string, apiKeyMode: string): Promise<void> {
|
||||
/** Read-only check: throws if user is over daily job or token limit. Actual reservation (upsert) is done by incrementJobCount. */
|
||||
async checkQuota(userId: string, apiKeyMode: string): Promise<void> {
|
||||
const settings = await this.prisma.userAiSettings.findUnique({ where: { userId } });
|
||||
const maxJobs = settings?.maxDailyAiJobs ?? 20;
|
||||
const maxTokens = settings?.maxDailyTokenBudget ?? 100_000;
|
||||
|
||||
@ -29,7 +29,7 @@ describe('UserAiService.createAnalysisJob', () => {
|
||||
};
|
||||
snapshotBuilder = { buildSnapshot: jest.fn().mockResolvedValue(mockSnapshot) };
|
||||
priorityRules = { computeJobPriority: jest.fn().mockReturnValue(50) };
|
||||
quota = { checkAndReserve: jest.fn().mockResolvedValue(undefined), incrementJobCount: jest.fn().mockResolvedValue(undefined) };
|
||||
quota = { checkQuota: jest.fn().mockResolvedValue(undefined), incrementJobCount: jest.fn().mockResolvedValue(undefined) };
|
||||
budget = { checkPlatformBudget: jest.fn().mockResolvedValue(undefined) };
|
||||
const crypto = { encrypt: jest.fn(), decrypt: jest.fn(), hash: jest.fn(), mask: jest.fn() } as any;
|
||||
|
||||
@ -132,7 +132,7 @@ describe('UserAiService.createAnalysisJob', () => {
|
||||
|
||||
await service.createAnalysisJob('u1', validDto);
|
||||
|
||||
expect(quota.checkAndReserve).toHaveBeenCalledWith('u1', 'platform_key');
|
||||
expect(quota.checkQuota).toHaveBeenCalledWith('u1', 'platform_key');
|
||||
expect(budget.checkPlatformBudget).toHaveBeenCalledWith('deepseek', 'deepseek-chat');
|
||||
expect(quota.incrementJobCount).toHaveBeenCalledWith('u1', 'platform_key');
|
||||
});
|
||||
|
||||
@ -230,6 +230,7 @@ export class UserAiService {
|
||||
}
|
||||
|
||||
// 4. Resolve apiKeyMode / credentialId
|
||||
// NOTE: apiKeyMode may be reassigned in step 6 fallback — steps 7–11 consume the final values
|
||||
let apiKeyMode = dto.apiKeyMode ?? settings.apiKeyMode;
|
||||
let credentialId: string | undefined = dto.credentialId ?? settings.defaultCredentialId ?? undefined;
|
||||
if (apiKeyMode === 'user_deepseek_key' && !credentialId) {
|
||||
@ -244,8 +245,8 @@ export class UserAiService {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Quota check
|
||||
await this.quota.checkAndReserve(userId, apiKeyMode);
|
||||
// 5. Quota check (read-only fast-fail; actual reservation at step 11)
|
||||
await this.quota.checkQuota(userId, apiKeyMode);
|
||||
|
||||
// 6. Platform budget check (platform_key only) — with fallback to user key
|
||||
if (apiKeyMode === 'platform_key') {
|
||||
@ -253,7 +254,7 @@ export class UserAiService {
|
||||
await this.budget.checkPlatformBudget('deepseek', 'deepseek-chat');
|
||||
} catch (err: any) {
|
||||
if (err?.response?.errorCode === 'PLATFORM_CIRCUIT_OPEN' && settings.fallbackToPlatformKey && settings.defaultCredentialId) {
|
||||
// Fallback: switch to user key mode
|
||||
// Fallback: apiKeyMode + credentialId reassigned — downstream steps use updated values
|
||||
apiKeyMode = 'user_deepseek_key';
|
||||
credentialId = settings.defaultCredentialId;
|
||||
} else {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user