Skip to content

Commit 6d88bd9

Browse files
feat: add CLI feedback when waiting for stdin input
- Detects interactive TTY sessions and prints a helpful message to stderr when the command is waiting for input. - Prevents user confusion when running commands without arguments or pipes. - Adds test case to verify the behavior (mocking isTTY).
1 parent e1596e8 commit 6d88bd9

File tree

2 files changed

+90
-40
lines changed

2 files changed

+90
-40
lines changed

lib/commands/unipept/unipept_subcommand.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ export abstract class UnipeptSubcommand {
303303
this.streamInterface = createInterface({ input: createReadStream(input) });
304304
return this.streamInterface[Symbol.asyncIterator]();
305305
} else {
306+
if (process.stdin.isTTY) {
307+
process.stderr.write("Reading from standard input... (Press Ctrl+D to finish)\n");
308+
}
306309
this.streamInterface = createInterface({ input: process.stdin });
307310
return this.streamInterface[Symbol.asyncIterator]();
308311
}
Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,103 @@
11
import { Pept2lca } from '../../../lib/commands/unipept/pept2lca.js';
2+
import { vi, describe, test, expect, afterEach } from 'vitest';
23

3-
test('test command setup', () => {
4-
const command = new Pept2lca();
5-
expect(command.name).toBe("pept2lca");
6-
expect(command.user_agent).toMatch(/^unipept-cli/);
7-
expect(command.command.name()).toBe("pept2lca");
8-
});
4+
describe('UnipeptSubcommand', () => {
5+
afterEach(() => {
6+
vi.restoreAllMocks();
7+
});
98

10-
test('test correct host', () => {
11-
const command = new Pept2lca();
9+
test('test command setup', () => {
10+
const command = new Pept2lca();
11+
expect(command.name).toBe("pept2lca");
12+
expect(command.user_agent).toMatch(/^unipept-cli/);
13+
expect(command.command.name()).toBe("pept2lca");
14+
});
1215

13-
expect(command.host).toBe("https://api.unipept.ugent.be");
14-
expect(command["getHost"]()).toBe("https://api.unipept.ugent.be");
16+
test('test correct host', () => {
17+
const command = new Pept2lca();
1518

16-
command.options.host = "https://optionshost";
17-
expect(command["getHost"]()).toBe("https://optionshost");
19+
expect(command.host).toBe("https://api.unipept.ugent.be");
20+
expect(command["getHost"]()).toBe("https://api.unipept.ugent.be");
1821

19-
command.options.host = "http://optionshost";
20-
expect(command["getHost"]()).toBe("http://optionshost");
22+
command.options.host = "https://optionshost";
23+
expect(command["getHost"]()).toBe("https://optionshost");
2124

22-
command.options.host = "optionshost";
23-
expect(command["getHost"]()).toBe("http://optionshost");
24-
});
25+
command.options.host = "http://optionshost";
26+
expect(command["getHost"]()).toBe("http://optionshost");
2527

26-
test('test correct inputIterator', async () => {
27-
const command = new Pept2lca();
28+
command.options.host = "optionshost";
29+
expect(command["getHost"]()).toBe("http://optionshost");
30+
});
2831

29-
// should be stdin
30-
let input = command["getInputIterator"]([]) as AsyncIterableIterator<string>;
31-
expect(typeof input[Symbol.asyncIterator]).toBe("function");
32-
command['streamInterface']?.close();
32+
test('test correct inputIterator', async () => {
33+
const command = new Pept2lca();
3334

34-
// should be a (non-existant) file and error
35-
input = command["getInputIterator"]([], "filename") as AsyncIterableIterator<string>;
36-
expect(typeof input[Symbol.asyncIterator]).toBe("function");
37-
await expect(async () => { await input.next() }).rejects.toThrow(/no such file/);
35+
// should be stdin
36+
let input = command["getInputIterator"]([]) as AsyncIterableIterator<string>;
37+
expect(typeof input[Symbol.asyncIterator]).toBe("function");
38+
command['streamInterface']?.close();
3839

39-
// should be array
40-
const inputArray = command["getInputIterator"](["A", "B"]) as IterableIterator<string>;
41-
expect(typeof inputArray[Symbol.iterator]).toBe("function");
42-
});
40+
// should be a (non-existant) file and error
41+
input = command["getInputIterator"]([], "filename") as AsyncIterableIterator<string>;
42+
expect(typeof input[Symbol.asyncIterator]).toBe("function");
43+
await expect(async () => { await input.next() }).rejects.toThrow(/no such file/);
4344

44-
test('test selected fields parsing', () => {
45-
const command = new Pept2lca();
45+
// should be array
46+
const inputArray = command["getInputIterator"](["A", "B"]) as IterableIterator<string>;
47+
expect(typeof inputArray[Symbol.iterator]).toBe("function");
48+
});
4649

47-
command.options.select = ["a,b,c"];
48-
expect(command["getSelectedFields"]()).toStrictEqual([/^a$/, /^b$/, /^c$/]);
49-
});
50+
test('test selected fields parsing', () => {
51+
const command = new Pept2lca();
52+
53+
command.options.select = ["a,b,c"];
54+
expect(command["getSelectedFields"]()).toStrictEqual([/^a$/, /^b$/, /^c$/]);
55+
});
56+
57+
test('test selected fields with wildcards', () => {
58+
const command = new Pept2lca();
59+
60+
command.options.select = ["taxon*,name"];
61+
expect(command["getSelectedFields"]()).toStrictEqual([/^taxon.*$/, /^name$/]);
62+
});
63+
64+
test('test inputIterator prints warning when reading from TTY stdin', async () => {
65+
const command = new Pept2lca();
66+
67+
// Mock process.stdin.isTTY
68+
const originalIsTTY = process.stdin.isTTY;
69+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
70+
71+
// Mock process.stderr.write
72+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
73+
74+
const input = command["getInputIterator"]([]) as AsyncIterableIterator<string>;
75+
76+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining("Reading from standard input..."));
77+
78+
command['streamInterface']?.close();
79+
80+
// Restore
81+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY });
82+
});
83+
84+
test('test inputIterator does NOT print warning when reading from piped stdin (not TTY)', async () => {
85+
const command = new Pept2lca();
86+
87+
// Mock process.stdin.isTTY
88+
const originalIsTTY = process.stdin.isTTY;
89+
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
90+
91+
// Mock process.stderr.write
92+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
93+
94+
const input = command["getInputIterator"]([]) as AsyncIterableIterator<string>;
95+
96+
expect(stderrSpy).not.toHaveBeenCalled();
5097

51-
test('test selected fields with wildcards', () => {
52-
const command = new Pept2lca();
98+
command['streamInterface']?.close();
5399

54-
command.options.select = ["taxon*,name"];
55-
expect(command["getSelectedFields"]()).toStrictEqual([/^taxon.*$/, /^name$/]);
100+
// Restore
101+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY });
102+
});
56103
});

0 commit comments

Comments
 (0)