Skip to content

Commit dc89ec6

Browse files
committed
fix(quota): correct current_interval_usage_count to hold actual usage
API fields current_interval_usage_count and current_weekly_usage_count actually hold remaining quota (not usage as the name implies). This caused --output json to expose semantically incorrect data. Changes (no schema change — only fix field values): - --output json: usage_count now holds actual usage (total - original_value) - --quiet: correct used/remaining computation using fixed values - table rendering already correct (was treating usage_count as remaining) Closes #68
1 parent e6de2aa commit dc89ec6

File tree

2 files changed

+183
-34
lines changed

2 files changed

+183
-34
lines changed

src/commands/quota/show.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,27 @@ export default defineCommand({
3030
const models = response.model_remains || [];
3131
const format = detectOutputFormat(flags.output as string | undefined);
3232

33+
// API field current_interval_usage_count actually holds the remaining quota (not usage).
34+
// Fix values in-place so the JSON output is semantically correct with no schema change.
35+
const fixedModels = models.map((m) => ({
36+
...m,
37+
current_interval_usage_count: m.current_interval_total_count - m.current_interval_usage_count,
38+
current_weekly_usage_count: m.current_weekly_total_count - m.current_weekly_usage_count,
39+
}));
40+
3341
if (format !== 'text') {
34-
console.log(formatOutput(response, format));
42+
console.log(formatOutput({ ...response, model_remains: fixedModels }, format));
3543
return;
3644
}
3745

3846
if (config.quiet) {
39-
for (const m of models) {
40-
const remaining = m.current_interval_total_count - m.current_interval_usage_count;
41-
console.log(`${m.model_name}\t${m.current_interval_usage_count}\t${m.current_interval_total_count}\t${remaining}`);
47+
for (const m of fixedModels) {
48+
const used = m.current_interval_usage_count; // already usage after fix
49+
console.log(`${m.model_name}\t${used}\t${m.current_interval_total_count}\t${m.current_interval_total_count - used}`);
4250
}
4351
return;
4452
}
4553

46-
renderQuotaTable(models, config);
54+
renderQuotaTable(fixedModels, config);
4755
},
4856
});

test/commands/quota/show.test.ts

Lines changed: 170 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,185 @@
1-
import { describe, it, expect } from 'bun:test';
2-
import { default as showCommand } from '../../../src/commands/quota/show';
1+
import { describe, it, expect, afterEach, vi } from 'bun:test';
2+
3+
// Store mock fn reference so we can configure it before the module imports it
4+
const mockRequestJson = vi.fn();
5+
6+
vi.mock('../../../src/client/http', () => ({
7+
requestJson: mockRequestJson,
8+
}));
9+
10+
// Pre-configure mock: API returns usage_count fields holding REMAINING values (the bug)
11+
mockRequestJson.mockResolvedValue({
12+
model_remains: [
13+
{
14+
model_name: 'MiniMax-M*',
15+
start_time: 1775750400000,
16+
end_time: 1775768400000,
17+
remains_time: 1464894,
18+
current_interval_total_count: 1500,
19+
current_interval_usage_count: 1417, // ← API says "usage" but value = remaining (the bug)
20+
current_weekly_total_count: 0,
21+
current_weekly_usage_count: 0,
22+
weekly_start_time: 1775404800000,
23+
weekly_end_time: 1776009600000,
24+
weekly_remains_time: 242664894,
25+
},
26+
{
27+
model_name: 'speech-hd',
28+
start_time: 1775750400000,
29+
end_time: 1775836800000,
30+
remains_time: 69864894,
31+
current_interval_total_count: 4000,
32+
current_interval_usage_count: 4000, // exhausted → remaining = 0
33+
current_weekly_total_count: 28000,
34+
current_weekly_usage_count: 28000,
35+
weekly_start_time: 1775404800000,
36+
weekly_end_time: 1776009600000,
37+
weekly_remains_time: 242664894,
38+
},
39+
],
40+
base_resp: { status_code: 0, status_msg: 'success' },
41+
});
42+
43+
import type { Config } from '../../../src/config/schema';
44+
import type { GlobalFlags } from '../../../src/types/flags';
45+
46+
const baseConfig: Config = {
47+
apiKey: 'test-key',
48+
region: 'global' as const,
49+
baseUrl: 'https://api.minimax.io',
50+
output: 'text' as const,
51+
timeout: 10,
52+
verbose: false,
53+
quiet: false,
54+
noColor: true,
55+
yes: false,
56+
dryRun: false,
57+
nonInteractive: true,
58+
async: false,
59+
};
60+
61+
const baseFlags: GlobalFlags = {
62+
quiet: false,
63+
verbose: false,
64+
noColor: true,
65+
yes: false,
66+
dryRun: false,
67+
help: false,
68+
nonInteractive: true,
69+
async: false,
70+
};
371

472
describe('quota show command', () => {
5-
it('has correct name', () => {
73+
afterEach(() => {
74+
mockRequestJson.mockClear();
75+
});
76+
77+
it('has correct name', async () => {
78+
const { default: showCommand } = await import('../../../src/commands/quota/show');
679
expect(showCommand.name).toBe('quota show');
780
});
881

9-
it('handles dry run', async () => {
10-
const config = {
11-
apiKey: 'test-key',
12-
region: 'global' as const,
13-
baseUrl: 'https://api.mmx.io',
14-
output: 'text' as const,
15-
timeout: 10,
16-
verbose: false,
17-
quiet: false,
18-
noColor: true,
19-
yes: false,
20-
dryRun: true,
21-
nonInteractive: true,
22-
async: false,
23-
};
82+
it('handles dry run without calling API', async () => {
83+
const { default: showCommand } = await import('../../../src/commands/quota/show');
2484

2585
const originalLog = console.log;
2686
let output = '';
2787
console.log = (msg: string) => { output += msg; };
2888

2989
try {
30-
await showCommand.execute(config, {
31-
quiet: false,
32-
verbose: false,
33-
noColor: true,
34-
yes: false,
35-
dryRun: true,
36-
help: false,
37-
nonInteractive: true,
38-
async: false,
39-
});
40-
90+
await showCommand.execute({ ...baseConfig, dryRun: true }, baseFlags);
4191
expect(output).toContain('Would fetch quota');
92+
expect(mockRequestJson).not.toHaveBeenCalled();
93+
} finally {
94+
console.log = originalLog;
95+
}
96+
});
97+
98+
it('--output json: fixes current_interval_usage_count to be actual usage (no new fields)', async () => {
99+
const { default: showCommand } = await import('../../../src/commands/quota/show');
100+
101+
const config = { ...baseConfig, output: 'json' as const };
102+
const originalLog = console.log;
103+
let output = '';
104+
console.log = (msg: string) => { output += msg; };
105+
106+
try {
107+
await showCommand.execute(config, { ...baseFlags, output: 'json' });
108+
const parsed = JSON.parse(output);
109+
110+
const m = parsed.model_remains[0];
111+
// After fix: usage_count = total - remaining = 1500 - 1417 = 83 (actual usage)
112+
expect(m.current_interval_usage_count).toBe(83);
113+
expect(m.current_interval_total_count).toBe(1500);
114+
// No new fields added — schema unchanged
115+
expect(m.current_interval_remaining_count).toBeUndefined();
116+
} finally {
117+
console.log = originalLog;
118+
}
119+
});
120+
121+
it('--output json: fixes current_weekly_usage_count to be actual usage for exhausted model', async () => {
122+
const { default: showCommand } = await import('../../../src/commands/quota/show');
123+
124+
const config = { ...baseConfig, output: 'json' as const };
125+
const originalLog = console.log;
126+
let output = '';
127+
console.log = (msg: string) => { output += msg; };
128+
129+
try {
130+
await showCommand.execute(config, { ...baseFlags, output: 'json' });
131+
const parsed = JSON.parse(output);
132+
133+
// speech-hd: weekly_usage_count = weekly_total - remaining = 28000 - 28000 = 0
134+
const speech = parsed.model_remains.find(
135+
(m: { model_name: string }) => m.model_name === 'speech-hd',
136+
);
137+
expect(speech.current_weekly_usage_count).toBe(0);
138+
expect(speech.current_weekly_total_count).toBe(28000);
139+
} finally {
140+
console.log = originalLog;
141+
}
142+
});
143+
144+
it('--quiet: tab line contains correct values (usage=83, total=1500, remaining=1417)', async () => {
145+
const { default: showCommand } = await import('../../../src/commands/quota/show');
146+
147+
const config = { ...baseConfig, quiet: true };
148+
const originalLog = console.log;
149+
let output = '';
150+
console.log = (msg: string) => { output += msg; };
151+
152+
try {
153+
// flags.output must be 'text' (not auto-detected) to reach quiet branch
154+
await showCommand.execute(config, { ...baseFlags, output: 'text' });
155+
const trimmed = output.trim();
156+
157+
// After fix: usage_count=83 (actual usage), remaining=1417
158+
expect(trimmed).toContain('MiniMax-M*');
159+
expect(trimmed).toContain('\t83\t'); // used = 1500-1417 = 83
160+
expect(trimmed).toContain('\t1500\t'); // total
161+
expect(trimmed).toContain('1417'); // remaining
162+
} finally {
163+
console.log = originalLog;
164+
}
165+
});
166+
167+
it('--quiet: exhausted quota shows usage=0 (total - remaining = 4000-4000)', async () => {
168+
const { default: showCommand } = await import('../../../src/commands/quota/show');
169+
170+
const config = { ...baseConfig, quiet: true };
171+
const originalLog = console.log;
172+
let output = '';
173+
console.log = (msg: string) => { output += msg; };
174+
175+
try {
176+
await showCommand.execute(config, { ...baseFlags, output: 'text' });
177+
const trimmed = output.trim();
178+
179+
// speech-hd: total=4000, remaining=4000 → usage=0
180+
expect(trimmed).toContain('speech-hd');
181+
expect(trimmed).toContain('\t0\t'); // usage = 0
182+
expect(trimmed).toContain('\t4000\t'); // total
42183
} finally {
43184
console.log = originalLog;
44185
}

0 commit comments

Comments
 (0)