Skip to content

Commit 3254c0a

Browse files
committed
fix(quota): treat usage_count fields as remaining quota, add semantically correct fields
API fields current_interval_usage_count and current_weekly_usage_count actually hold remaining quota (not usage). This caused --output json to expose semantically incorrect data. Changes: - --output json: add current_interval_remaining_count and current_weekly_remaining_count (total - usage_count), preserving original fields for backward compat - --quiet: fix used=total-remaining computation (usage_count is remaining) - table rendering already correct (treats usage_count as remaining) Closes #68
1 parent e6de2aa commit 3254c0a

2 files changed

Lines changed: 188 additions & 32 deletions

File tree

src/commands/quota/show.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,27 @@ export default defineCommand({
3131
const format = detectOutputFormat(flags.output as string | undefined);
3232

3333
if (format !== 'text') {
34-
console.log(formatOutput(response, format));
34+
// API field current_interval_usage_count actually holds the remaining quota.
35+
// Add computed fields with correct semantics for JSON output.
36+
const fixed = {
37+
...response,
38+
model_remains: response.model_remains.map((m) => ({
39+
...m,
40+
current_interval_remaining_count:
41+
m.current_interval_total_count - m.current_interval_usage_count,
42+
current_weekly_remaining_count:
43+
m.current_weekly_total_count - m.current_weekly_usage_count,
44+
})),
45+
};
46+
console.log(formatOutput(fixed, format));
3547
return;
3648
}
3749

3850
if (config.quiet) {
3951
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}`);
52+
const remaining = m.current_interval_usage_count; // already remaining
53+
const used = m.current_interval_total_count - remaining;
54+
console.log(`${m.model_name}\t${used}\t${m.current_interval_total_count}\t${remaining}`);
4255
}
4356
return;
4457
}

test/commands/quota/show.test.ts

Lines changed: 172 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,187 @@
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 so any future dynamic import uses the configured return value
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 name says "usage" but value = remaining
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+
// config.dryRun must be true to skip API call
91+
await showCommand.execute({ ...baseConfig, dryRun: true }, baseFlags);
4192
expect(output).toContain('Would fetch quota');
93+
expect(mockRequestJson).not.toHaveBeenCalled();
94+
} finally {
95+
console.log = originalLog;
96+
}
97+
});
98+
99+
it('--output json: adds current_interval_remaining_count = total - usage_count', async () => {
100+
const { default: showCommand } = await import('../../../src/commands/quota/show');
101+
102+
const config = { ...baseConfig, output: 'json' as const };
103+
const originalLog = console.log;
104+
let output = '';
105+
console.log = (msg: string) => { output += msg; };
106+
107+
try {
108+
await showCommand.execute(config, { ...baseFlags, output: 'json' });
109+
const parsed = JSON.parse(output);
110+
111+
const m = parsed.model_remains[0];
112+
// remaining = total - usage_count = 1500 - 1417 = 83
113+
expect(m.current_interval_remaining_count).toBe(83);
114+
expect(m.current_interval_total_count).toBe(1500);
115+
// original usage_count field preserved for backward compat
116+
expect(m.current_interval_usage_count).toBe(1417);
117+
} finally {
118+
console.log = originalLog;
119+
}
120+
});
121+
122+
it('--output json: adds current_weekly_remaining_count for exhausted model', async () => {
123+
const { default: showCommand } = await import('../../../src/commands/quota/show');
124+
125+
const config = { ...baseConfig, output: 'json' as const };
126+
const originalLog = console.log;
127+
let output = '';
128+
console.log = (msg: string) => { output += msg; };
129+
130+
try {
131+
await showCommand.execute(config, { ...baseFlags, output: 'json' });
132+
const parsed = JSON.parse(output);
133+
134+
// speech-hd: weekly_total=28000, weekly_usage=28000 → remaining=0
135+
const speech = parsed.model_remains.find(
136+
(m: { model_name: string }) => m.model_name === 'speech-hd',
137+
);
138+
expect(speech.current_weekly_remaining_count).toBe(0);
139+
expect(speech.current_weekly_total_count).toBe(28000);
140+
} finally {
141+
console.log = originalLog;
142+
}
143+
});
144+
145+
it('--quiet: tab line contains correct values for MiniMax-M*', async () => {
146+
const { default: showCommand } = await import('../../../src/commands/quota/show');
147+
148+
const config = { ...baseConfig, quiet: true };
149+
const originalLog = console.log;
150+
let output = '';
151+
console.log = (msg: string) => { output += msg; };
152+
153+
try {
154+
// flags.output must be 'text' (not auto-detected) to reach quiet branch
155+
await showCommand.execute(config, { ...baseFlags, output: 'text' });
156+
const trimmed = output.trim();
157+
158+
// Should contain MiniMax-M* with used=83, total=1500, remaining=1417
159+
expect(trimmed).toContain('MiniMax-M*');
160+
expect(trimmed).toContain('\t83\t'); // used = 1500-1417
161+
expect(trimmed).toContain('\t1500\t'); // total
162+
expect(trimmed).toContain('1417'); // remaining (API's usage_count)
163+
} finally {
164+
console.log = originalLog;
165+
}
166+
});
167+
168+
it('--quiet: exhausted quota shows used=0, remaining=total for speech-hd', async () => {
169+
const { default: showCommand } = await import('../../../src/commands/quota/show');
170+
171+
const config = { ...baseConfig, quiet: true };
172+
const originalLog = console.log;
173+
let output = '';
174+
console.log = (msg: string) => { output += msg; };
175+
176+
try {
177+
// flags.output must be 'text' (not auto-detected) to reach quiet branch
178+
await showCommand.execute(config, { ...baseFlags, output: 'text' });
179+
const trimmed = output.trim();
180+
181+
// speech-hd: total=4000, API remaining=4000 → used=0
182+
expect(trimmed).toContain('speech-hd');
183+
expect(trimmed).toContain('\t0\t'); // used = 0 (4000-4000)
184+
expect(trimmed).toContain('\t4000\t'); // total
42185
} finally {
43186
console.log = originalLog;
44187
}

0 commit comments

Comments
 (0)