Skip to content

Commit 1e90b88

Browse files
committed
fix: register file commands
1 parent 65aed9c commit 1e90b88

4 files changed

Lines changed: 257 additions & 0 deletions

File tree

src/registry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import quotaShow from './commands/quota/show';
2222
import configShow from './commands/config/show';
2323
import configSet from './commands/config/set';
2424
import configExportSchema from './commands/config/export-schema';
25+
import fileUpload from './commands/file/upload';
26+
import fileList from './commands/file/list';
27+
import fileDelete from './commands/file/delete';
2528
import update from './commands/update';
2629
import help from './commands/help';
2730

@@ -197,6 +200,7 @@ ${b('Resources:')}
197200
${a('vision')} ${d('Image understanding (describe)')}
198201
${a('quota')} ${d('Usage quotas (show)')}
199202
${a('config')} ${d('CLI configuration (show, set, export-schema)')}
203+
${a('file')} ${d('File storage (upload, list, delete)')}
200204
${a('update')} ${d('Update mmx to a newer version')}
201205
202206
${b('Global Flags:')}
@@ -284,6 +288,9 @@ export const registry = new CommandRegistry({
284288
'config show': configShow,
285289
'config set': configSet,
286290
'config export-schema': configExportSchema,
291+
'file upload': fileUpload,
292+
'file list': fileList,
293+
'file delete': fileDelete,
287294
'update': update,
288295
'help': help,
289296
});

test/commands/aliases.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ describe('command aliases', () => {
1313
const synthesize = registry.resolve(['speech', 'synthesize']);
1414
expect(generate.command).toBe(synthesize.command);
1515
});
16+
17+
it('resolves file storage commands', () => {
18+
expect(registry.resolve(['file', 'upload']).command.name).toBe('file upload');
19+
expect(registry.resolve(['file', 'list']).command.name).toBe('file list');
20+
expect(registry.resolve(['file', 'delete']).command.name).toBe('file delete');
21+
});
1622
});
1723

1824
describe('text chat --prompt alias', () => {

test/commands/file/delete.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { describe, it, expect, afterEach } from 'bun:test';
2+
import { default as deleteCommand } from '../../../src/commands/file/delete';
3+
import { createMockServer, jsonResponse, type MockServer } from '../../helpers/mock-server';
4+
import type { Config } from '../../../src/config/schema';
5+
import type { GlobalFlags } from '../../../src/types/flags';
6+
7+
function makeConfig(overrides: Partial<Config> = {}): Config {
8+
return {
9+
apiKey: 'test-key',
10+
region: 'global',
11+
baseUrl: 'https://api.mmx.io',
12+
output: 'text',
13+
timeout: 10,
14+
verbose: false,
15+
quiet: false,
16+
noColor: true,
17+
yes: false,
18+
dryRun: false,
19+
nonInteractive: true,
20+
async: false,
21+
...overrides,
22+
};
23+
}
24+
25+
const baseFlags: GlobalFlags = {
26+
quiet: false,
27+
verbose: false,
28+
noColor: true,
29+
yes: false,
30+
dryRun: false,
31+
help: false,
32+
nonInteractive: true,
33+
async: false,
34+
};
35+
36+
async function captureStdout(fn: () => Promise<void>): Promise<string> {
37+
const originalWrite = process.stdout.write;
38+
let output = '';
39+
process.stdout.write = ((chunk: string | Uint8Array) => {
40+
output += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8');
41+
return true;
42+
}) as typeof process.stdout.write;
43+
44+
try {
45+
await fn();
46+
return output;
47+
} finally {
48+
process.stdout.write = originalWrite;
49+
}
50+
}
51+
52+
describe('file delete command', () => {
53+
let server: MockServer;
54+
55+
afterEach(() => {
56+
server?.close();
57+
});
58+
59+
it('has correct name', () => {
60+
expect(deleteCommand.name).toBe('file delete');
61+
});
62+
63+
it('requires file-id argument', async () => {
64+
await expect(
65+
deleteCommand.execute(makeConfig({ dryRun: true }), baseFlags),
66+
).rejects.toThrow('Missing required argument: --file-id');
67+
});
68+
69+
it('handles dry run', async () => {
70+
const output = await captureStdout(async () => {
71+
await deleteCommand.execute(makeConfig({ dryRun: true, output: 'json' }), {
72+
...baseFlags,
73+
dryRun: true,
74+
fileId: 'file-123',
75+
});
76+
});
77+
78+
const parsed = JSON.parse(output);
79+
expect(parsed.request.delete_file).toBe('file-123');
80+
});
81+
82+
it('sends DELETE request and prints result', async () => {
83+
let method = '';
84+
let fileId = '';
85+
server = createMockServer({
86+
routes: {
87+
'/v1/files': (req) => {
88+
method = req.method;
89+
fileId = new URL(req.url).searchParams.get('file_id') ?? '';
90+
return jsonResponse({
91+
base_resp: { status_code: 0, status_msg: '' },
92+
id: fileId,
93+
object: 'file',
94+
deleted: true,
95+
});
96+
},
97+
},
98+
});
99+
100+
const output = await captureStdout(async () => {
101+
await deleteCommand.execute(makeConfig({ baseUrl: server.url, output: 'json' }), {
102+
...baseFlags,
103+
fileId: 'file-123',
104+
});
105+
});
106+
107+
const parsed = JSON.parse(output);
108+
expect(method).toBe('DELETE');
109+
expect(fileId).toBe('file-123');
110+
expect(parsed).toEqual({ id: 'file-123', deleted: true });
111+
});
112+
113+
it('prints compact status in quiet mode', async () => {
114+
server = createMockServer({
115+
routes: {
116+
'/v1/files': () => jsonResponse({
117+
base_resp: { status_code: 0, status_msg: '' },
118+
id: 'file-123',
119+
object: 'file',
120+
deleted: true,
121+
}),
122+
},
123+
});
124+
125+
const output = await captureStdout(async () => {
126+
await deleteCommand.execute(makeConfig({ baseUrl: server.url, quiet: true }), {
127+
...baseFlags,
128+
quiet: true,
129+
fileId: 'file-123',
130+
});
131+
});
132+
133+
expect(output).toBe('deleted\n');
134+
});
135+
});

test/commands/file/list.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, it, expect, afterEach } from 'bun:test';
2+
import { default as listCommand } from '../../../src/commands/file/list';
3+
import { createMockServer, jsonResponse, type MockServer } from '../../helpers/mock-server';
4+
import type { Config } from '../../../src/config/schema';
5+
import type { GlobalFlags } from '../../../src/types/flags';
6+
7+
function makeConfig(overrides: Partial<Config> = {}): Config {
8+
return {
9+
apiKey: 'test-key',
10+
region: 'global',
11+
baseUrl: 'https://api.mmx.io',
12+
output: 'text',
13+
timeout: 10,
14+
verbose: false,
15+
quiet: false,
16+
noColor: true,
17+
yes: false,
18+
dryRun: false,
19+
nonInteractive: true,
20+
async: false,
21+
...overrides,
22+
};
23+
}
24+
25+
const baseFlags: GlobalFlags = {
26+
quiet: false,
27+
verbose: false,
28+
noColor: true,
29+
yes: false,
30+
dryRun: false,
31+
help: false,
32+
nonInteractive: true,
33+
async: false,
34+
};
35+
36+
async function captureStdout(fn: () => Promise<void>): Promise<string> {
37+
const originalWrite = process.stdout.write;
38+
let output = '';
39+
process.stdout.write = ((chunk: string | Uint8Array) => {
40+
output += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8');
41+
return true;
42+
}) as typeof process.stdout.write;
43+
44+
try {
45+
await fn();
46+
return output;
47+
} finally {
48+
process.stdout.write = originalWrite;
49+
}
50+
}
51+
52+
describe('file list command', () => {
53+
let server: MockServer;
54+
55+
afterEach(() => {
56+
server?.close();
57+
});
58+
59+
it('has correct name', () => {
60+
expect(listCommand.name).toBe('file list');
61+
});
62+
63+
it('handles dry run', async () => {
64+
const output = await captureStdout(async () => {
65+
await listCommand.execute(makeConfig({ dryRun: true }), baseFlags);
66+
});
67+
68+
expect(output).toBe('Would list uploaded files.\n');
69+
});
70+
71+
it('shows empty state when no files are returned', async () => {
72+
server = createMockServer({
73+
routes: {
74+
'/v1/files': () => jsonResponse({ base_resp: { status_code: 0, status_msg: '' }, data: [] }),
75+
},
76+
});
77+
78+
const output = await captureStdout(async () => {
79+
await listCommand.execute(makeConfig({ baseUrl: server.url }), baseFlags);
80+
});
81+
82+
expect(output).toBe('No files found.\n');
83+
});
84+
85+
it('prints JSON output for file list responses', async () => {
86+
server = createMockServer({
87+
routes: {
88+
'/v1/files': () => jsonResponse({
89+
base_resp: { status_code: 0, status_msg: '' },
90+
data: [{
91+
file_id: 'file-123',
92+
bytes: 2048,
93+
created_at: 1700000000,
94+
filename: 'doc.pdf',
95+
purpose: 'retrieval',
96+
}],
97+
}),
98+
},
99+
});
100+
101+
const output = await captureStdout(async () => {
102+
await listCommand.execute(makeConfig({ baseUrl: server.url, output: 'json' }), baseFlags);
103+
});
104+
105+
const parsed = JSON.parse(output);
106+
expect(parsed.data[0].file_id).toBe('file-123');
107+
expect(parsed.data[0].filename).toBe('doc.pdf');
108+
});
109+
});

0 commit comments

Comments
 (0)