feat(billing): enforce image generation quota with admin API and user display
Memind CI / Test, build, and release guards (push) Failing after 3s
Memind CI / Test, build, and release guards (push) Failing after 3s
Add period_images_bonus and ledger tracking, gate image_make on remaining quota, expose admin image-quota routes, show remaining image quota in the balance popover, and document that admin UI must live in memind_adm (5174) not ops. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -323,3 +323,69 @@ test('MindSpace image generation fails closed when semantic review is unavailabl
|
||||
assert.equal(result.code, 'IMAGE_REVIEW_UNAVAILABLE');
|
||||
assert.equal(storeCount, 0);
|
||||
});
|
||||
|
||||
test('MindSpace image generation blocks when user image quota is exhausted', async () => {
|
||||
const service = createMindSpaceImageGenerationService({
|
||||
configService: { async resolveImageGenerationPurpose() { return { ok: true, presetId: 'hero' }; } },
|
||||
subscriptionService: {
|
||||
async checkImageQuota() {
|
||||
return {
|
||||
ok: false,
|
||||
code: 'image_quota_exceeded',
|
||||
message: '图片生成额度不足',
|
||||
quota: { remaining: 0 },
|
||||
};
|
||||
},
|
||||
},
|
||||
imageMakeClient: {
|
||||
async generateImage() {
|
||||
throw new Error('should not call image_make when quota is exhausted');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.generate({ userId: 'user-1', purpose: 'hero', prompt: 'test' });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.code, 'image_quota_exceeded');
|
||||
assert.equal(result.fallback, false);
|
||||
});
|
||||
|
||||
test('MindSpace image generation consumes quota after a successful hero generation', async () => {
|
||||
const events = [];
|
||||
const service = createMindSpaceImageGenerationService({
|
||||
configService: { async resolveImageGenerationPurpose() { return { ok: true, presetId: 'hero' }; } },
|
||||
subscriptionService: {
|
||||
async checkImageQuota() {
|
||||
events.push('check');
|
||||
return { ok: true, quota: { remaining: 5 } };
|
||||
},
|
||||
async consumeImageQuota(userId, count, conn, options) {
|
||||
events.push(['consume', userId, count, options?.refId]);
|
||||
return { fullyCovers: true };
|
||||
},
|
||||
},
|
||||
imageMakeClient: {
|
||||
async generateImage() {
|
||||
return {
|
||||
jobId: 'job-quota-1', buffer: Buffer.from('image'), mimeType: 'image/webp',
|
||||
sha256: 'abc', width: 768, height: 432,
|
||||
};
|
||||
},
|
||||
async acknowledge() {},
|
||||
},
|
||||
imageReviewService: passingReviewService(),
|
||||
assetService: {
|
||||
async createChatAsset() {
|
||||
return {
|
||||
id: 'asset-1',
|
||||
publicUrl: 'https://example.test/image.webp',
|
||||
workspaceRelativePath: 'public/images/hero.webp',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.generate({ userId: 'user-1', purpose: 'hero', prompt: 'test' });
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(events, ['check', ['consume', 'user-1', 1, 'job-quota-1']]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user