Skip to content

Commit ce753f8

Browse files
fix(cli): use console.log for --json output, add tests
Switch --json output from process.stdout.write/process.stderr.write to console.log/console.error, aligning with the existing pattern in lookup.ts. This ensures the test harness captures JSON output correctly. Add unit tests verifying --json flag in help output and JSON error formatting for pre-chain validation errors. Add integration tests for content view/set, pop info/set, and register domain in --json mode, validating JSON shape and absence of human output markers. Add code comment explaining why ora spinners are safe inside maybeQuiet (withCapturedConsole replaces .write on the stream object that ora caches by reference).
1 parent 29c71c9 commit ce753f8

9 files changed

Lines changed: 277 additions & 14 deletions

File tree

packages/cli/src/cli/commands/content.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,25 @@ export function attachContentCommands(root: Command) {
5656
);
5757

5858
if (!jsonOutput) console.log(chalk.bold("\n▶ Content View\n"));
59+
// Ora caches process.stderr (the object), not .write (the method).
60+
// withCapturedConsole replaces .write on the object, so spinner
61+
// output is captured as long as start/succeed/fail run inside maybeQuiet.
5962
const spinner = ora();
6063

6164
const result = await maybeQuiet(jsonOutput, () =>
6265
viewDomainContentHash(context.clientWrapper!, context.account.address, name, spinner),
6366
);
6467

6568
if (jsonOutput) {
66-
process.stdout.write(JSON.stringify(result) + "\n");
69+
console.log(JSON.stringify(result));
6770
} else {
6871
console.log(chalk.green("\n✓ Complete\n"));
6972
}
7073
process.exit(0);
7174
} catch (error) {
7275
const errorMessage = formatErrorMessage(error);
7376
if (jsonOutput) {
74-
process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n");
77+
console.error(JSON.stringify({ error: errorMessage }));
7578
process.exit(1);
7679
}
7780
console.error(chalk.red(`\n✗ Error: ${errorMessage}\n`));
@@ -100,6 +103,7 @@ export function attachContentCommands(root: Command) {
100103
);
101104

102105
if (!jsonOutput) console.log(chalk.bold("\n▶ Content Set\n"));
106+
// See view handler above for why ora is safe inside maybeQuiet.
103107
const spinner = ora();
104108

105109
const result = await maybeQuiet(jsonOutput, () =>
@@ -114,15 +118,15 @@ export function attachContentCommands(root: Command) {
114118
);
115119

116120
if (jsonOutput) {
117-
process.stdout.write(JSON.stringify(result) + "\n");
121+
console.log(JSON.stringify(result));
118122
} else {
119123
console.log(chalk.green("\n✓ Complete\n"));
120124
}
121125
process.exit(0);
122126
} catch (error) {
123127
const errorMessage = formatErrorMessage(error);
124128
if (jsonOutput) {
125-
process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n");
129+
console.error(JSON.stringify({ error: errorMessage }));
126130
process.exit(1);
127131
}
128132
console.error(chalk.red(`\n✗ Error: ${errorMessage}\n`));

packages/cli/src/cli/commands/pop.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ export function attachPopCommands(root: Command): void {
8686
);
8787

8888
if (jsonOutput) {
89-
process.stdout.write(
89+
console.log(
9090
JSON.stringify({
9191
ok: true,
9292
status: ProofOfPersonhoodStatus[parsedStatus].toLowerCase(),
9393
statusCode: parsedStatus,
94-
}) + "\n",
94+
}),
9595
);
9696
} else {
9797
console.log(chalk.green("\n✓ PoP Status Updated\n"));
@@ -100,7 +100,7 @@ export function attachPopCommands(root: Command): void {
100100
} catch (error) {
101101
const errorMessage = formatErrorMessage(error);
102102
if (jsonOutput) {
103-
process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n");
103+
console.error(JSON.stringify({ error: errorMessage }));
104104
process.exit(1);
105105
}
106106
console.error(chalk.red(`\n✗ Error: ${errorMessage}\n`));
@@ -121,13 +121,13 @@ export function attachPopCommands(root: Command): void {
121121
const info = await maybeQuiet(jsonOutput, () => readPopInfo(mergedOptions));
122122

123123
if (jsonOutput) {
124-
process.stdout.write(
124+
console.log(
125125
JSON.stringify({
126126
substrate: info.substrate,
127127
evm: info.evm,
128128
status: ProofOfPersonhoodStatus[info.status].toLowerCase(),
129129
statusCode: info.status,
130-
}) + "\n",
130+
}),
131131
);
132132
} else {
133133
console.log(chalk.bold("\n📋 ProofOfPersonhood Status\n"));
@@ -143,7 +143,7 @@ export function attachPopCommands(root: Command): void {
143143
} catch (error) {
144144
const errorMessage = formatErrorMessage(error);
145145
if (jsonOutput) {
146-
process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n");
146+
console.error(JSON.stringify({ error: errorMessage }));
147147
process.exit(1);
148148
}
149149
console.error(chalk.red(`\n✗ Error: ${errorMessage}\n`));

packages/cli/src/cli/commands/registerCommand.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ export function attachRegisterCommand(root: Command) {
6262
const result = await maybeQuiet(jsonOutput, () => executeRegistration(merged));
6363

6464
if (jsonOutput) {
65-
process.stdout.write(JSON.stringify(result) + "\n");
65+
console.log(JSON.stringify(result));
6666
}
6767
process.exit(0);
6868
} catch (error) {
6969
const errorMessage = formatErrorMessage(error);
7070
if (jsonOutput) {
71-
process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n");
71+
console.error(JSON.stringify({ error: errorMessage }));
7272
process.exit(1);
7373
}
7474
console.error(`\n${chalk.red.bold("✗ Error:")} ${errorMessage}\n`);
@@ -93,13 +93,13 @@ export function attachRegisterCommand(root: Command) {
9393
const result = await maybeQuiet(jsonOutput, () => executeSubnameRegistration(merged));
9494

9595
if (jsonOutput) {
96-
process.stdout.write(JSON.stringify(result) + "\n");
96+
console.log(JSON.stringify(result));
9797
}
9898
process.exit(0);
9999
} catch (error) {
100100
const errorMessage = formatErrorMessage(error);
101101
if (jsonOutput) {
102-
process.stderr.write(JSON.stringify({ error: errorMessage }) + "\n");
102+
console.error(JSON.stringify({ error: errorMessage }));
103103
process.exit(1);
104104
}
105105
console.error(`\n${chalk.red.bold("✗ Error:")} ${errorMessage}\n`);

packages/cli/tests/integration/content/content.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,90 @@ test(
223223
},
224224
{ timeout: TEST_TIMEOUT_MS },
225225
);
226+
227+
// --json tests
228+
229+
test(
230+
"content view --json returns structured result for registered domain",
231+
async () => {
232+
const result = await runDotnsCli(["content", "view", REGISTERED_DOMAIN, "--json"]);
233+
234+
expect(result.exitCode).toBe(HARNESS_SUCCESS_EXIT_CODE);
235+
236+
expect(result.combinedOutput).not.toContain("▶");
237+
expect(result.combinedOutput).not.toContain("✓");
238+
239+
const parsed = JSON.parse(result.combinedOutput.trim());
240+
241+
expect(parsed.domain).toBe(`${REGISTERED_DOMAIN}.dot`);
242+
expect(parsed).toHaveProperty("contenthash");
243+
expect(parsed).toHaveProperty("cid");
244+
},
245+
{ timeout: TEST_TIMEOUT_MS },
246+
);
247+
248+
test(
249+
"content view --json returns nulls for unregistered domain",
250+
async () => {
251+
const result = await runDotnsCli(["content", "view", UNREGISTERED_DOMAIN, "--json"]);
252+
253+
expect(result.exitCode).toBe(HARNESS_SUCCESS_EXIT_CODE);
254+
255+
const parsed = JSON.parse(result.combinedOutput.trim());
256+
257+
expect(parsed.domain).toBe(`${UNREGISTERED_DOMAIN}.dot`);
258+
expect(parsed.contenthash).toBeNull();
259+
expect(parsed.cid).toBeNull();
260+
},
261+
{ timeout: TEST_TIMEOUT_MS },
262+
);
263+
264+
test(
265+
"content set --json returns structured result",
266+
async () => {
267+
const result = await runDotnsCli([
268+
"content",
269+
"--key-uri",
270+
"//Alice",
271+
"set",
272+
REGISTERED_DOMAIN,
273+
TEST_CID,
274+
"--json",
275+
]);
276+
277+
expect(result.exitCode).toBe(HARNESS_SUCCESS_EXIT_CODE);
278+
279+
expect(result.combinedOutput).not.toContain("▶");
280+
expect(result.combinedOutput).not.toContain("✓");
281+
282+
const parsed = JSON.parse(result.combinedOutput.trim());
283+
284+
expect(parsed.ok).toBe(true);
285+
expect(parsed.domain).toBe(`${REGISTERED_DOMAIN}.dot`);
286+
expect(parsed.cid).toBeString();
287+
expect(parsed.contenthash).toBeString();
288+
expect(parsed.txHash).toBeString();
289+
},
290+
{ timeout: TEST_TIMEOUT_MS },
291+
);
292+
293+
test(
294+
"content set --json emits JSON error for unregistered domain",
295+
async () => {
296+
const result = await runDotnsCli([
297+
"content",
298+
"--key-uri",
299+
"//Alice",
300+
"set",
301+
UNREGISTERED_DOMAIN,
302+
TEST_CID,
303+
"--json",
304+
]);
305+
306+
expect(result.exitCode).toBe(HARNESS_SUCCESS_EXIT_CODE);
307+
308+
const parsed = JSON.parse(result.standardError.trim());
309+
expect(parsed.error).toContain("is not registered");
310+
},
311+
{ timeout: TEST_TIMEOUT_MS },
312+
);

packages/cli/tests/integration/pop/setPop.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,43 @@ test(
227227
},
228228
{ timeout: TEST_TIMEOUT_MS },
229229
);
230+
231+
// --json tests
232+
233+
test(
234+
"pop info --json returns structured result",
235+
async () => {
236+
const result = await runDotnsCli(["pop", "--key-uri", "//Alice", "info", "--json"]);
237+
238+
expect(result.exitCode).toBe(HARNESS_SUCCESS_EXIT_CODE);
239+
240+
expect(result.combinedOutput).not.toContain("📋");
241+
expect(result.combinedOutput).not.toContain("✓");
242+
243+
const parsed = JSON.parse(result.combinedOutput.trim());
244+
245+
expect(parsed.substrate).toBeString();
246+
expect(parsed.evm).toBeString();
247+
expect(parsed.status).toBeString();
248+
expect(parsed.statusCode).toBeNumber();
249+
},
250+
{ timeout: TEST_TIMEOUT_MS },
251+
);
252+
253+
test(
254+
"pop set --json returns structured result",
255+
async () => {
256+
const result = await runDotnsCli(["pop", "--key-uri", "//Alice", "set", "lite", "--json"]);
257+
258+
expect(result.exitCode).toBe(HARNESS_SUCCESS_EXIT_CODE);
259+
260+
expect(result.combinedOutput).not.toContain("✓ PoP Status Updated");
261+
262+
const parsed = JSON.parse(result.combinedOutput.trim());
263+
264+
expect(parsed.ok).toBe(true);
265+
expect(parsed.status).toBeString();
266+
expect(parsed.statusCode).toBeNumber();
267+
},
268+
{ timeout: TEST_TIMEOUT_MS },
269+
);

packages/cli/tests/integration/register/register.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,33 @@ test(
254254
},
255255
{ timeout: REGISTER_TEST_TIMEOUT_MS },
256256
);
257+
258+
// --json tests
259+
260+
test(
261+
"register domain --json returns structured result",
262+
async () => {
263+
createPathsForTest("register_domain_json");
264+
const keystorePath = await ensureDefaultKeystore();
265+
const label = generateRandomLabel(ProofOfPersonhoodStatus.NoStatus);
266+
267+
const result = await runDotnsCli(
268+
["register", "domain", "--account", TEST_ACCOUNT, "--name", label, "--json"],
269+
{ DOTNS_KEYSTORE_PATH: keystorePath, DOTNS_KEYSTORE_PASSWORD: TEST_PASSWORD },
270+
);
271+
272+
expect(result.exitCode).toBe(HARNESS_SUCCESS_EXIT_CODE);
273+
274+
expect(result.combinedOutput).not.toContain("═══");
275+
expect(result.combinedOutput).not.toContain("▶");
276+
expect(result.combinedOutput).not.toContain("✓");
277+
278+
const parsed = JSON.parse(result.combinedOutput.trim());
279+
280+
expect(parsed.ok).toBe(true);
281+
expect(parsed.label).toBe(label);
282+
expect(parsed.domain).toBe(`${label}.dot`);
283+
expect(parsed.owner).toBeString();
284+
},
285+
{ timeout: REGISTER_TEST_TIMEOUT_MS },
286+
);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { expect, test } from "bun:test";
2+
import {
3+
HARNESS_HELP_SUCCESS_EXIT_CODE,
4+
HARNESS_SUCCESS_EXIT_CODE,
5+
runDotnsCli,
6+
} from "../../_helpers/cliHelpers";
7+
import { DEFAULT_MNEMONIC } from "../../../src/utils/constants";
8+
9+
test("content view --help shows --json option", async () => {
10+
const result = await runDotnsCli(["content", "view", "--help"]);
11+
12+
expect(result.exitCode).toBe(HARNESS_HELP_SUCCESS_EXIT_CODE);
13+
expect(result.combinedOutput).toContain("--json");
14+
expect(result.combinedOutput).toContain("Output result as JSON");
15+
});
16+
17+
test("content set --help shows --json option", async () => {
18+
const result = await runDotnsCli(["content", "set", "--help"]);
19+
20+
expect(result.exitCode).toBe(HARNESS_HELP_SUCCESS_EXIT_CODE);
21+
expect(result.combinedOutput).toContain("--json");
22+
expect(result.combinedOutput).toContain("Output result as JSON");
23+
});
24+
25+
test("content set --json emits JSON error when both mnemonic and key-uri provided", async () => {
26+
const result = await runDotnsCli([
27+
"content",
28+
"--mnemonic",
29+
DEFAULT_MNEMONIC,
30+
"--key-uri",
31+
"//Alice",
32+
"set",
33+
"testdomain",
34+
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
35+
"--json",
36+
]);
37+
38+
expect(result.exitCode).toBe(HARNESS_SUCCESS_EXIT_CODE);
39+
40+
const errorOutput = result.standardError.trim();
41+
const parsed = JSON.parse(errorOutput);
42+
expect(parsed.error).toContain("Cannot specify both");
43+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect, test } from "bun:test";
2+
import { HARNESS_HELP_SUCCESS_EXIT_CODE, runDotnsCli } from "../../_helpers/cliHelpers";
3+
4+
test("pop set --help shows --json option", async () => {
5+
const result = await runDotnsCli(["pop", "set", "--help"]);
6+
7+
expect(result.exitCode).toBe(HARNESS_HELP_SUCCESS_EXIT_CODE);
8+
expect(result.combinedOutput).toContain("--json");
9+
expect(result.combinedOutput).toContain("Output result as JSON");
10+
});
11+
12+
test("pop info --help shows --json option", async () => {
13+
const result = await runDotnsCli(["pop", "info", "--help"]);
14+
15+
expect(result.exitCode).toBe(HARNESS_HELP_SUCCESS_EXIT_CODE);
16+
expect(result.combinedOutput).toContain("--json");
17+
expect(result.combinedOutput).toContain("Output result as JSON");
18+
});

0 commit comments

Comments
 (0)