Skip to content

Commit 954ff8a

Browse files
rekmarksclaude
andcommitted
fix: address PR review feedback
- Use it.each() for prettifySmallcaps tests - Add --timeout option to redeem-url subcommand - Fix stale isCapData JSDoc (slots: unknown[] → string[]) - Reject empty OCAP_HOME env var (treat as unset) - Extract parseTimeoutMs with positive-integer validation + tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7858315 commit 954ff8a

7 files changed

Lines changed: 111 additions & 114 deletions

File tree

packages/kernel-cli/src/app.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { getServer } from './commands/serve.ts';
2525
import { watchDir } from './commands/watch.ts';
2626
import { defaultConfig } from './config.ts';
2727
import type { Config } from './config.ts';
28-
import { withTimeout } from './utils.ts';
28+
import { parseTimeoutMs, withTimeout } from './utils.ts';
2929

3030
/**
3131
* Console transport that omits tags from output.
@@ -319,12 +319,11 @@ const yargsInstance = yargs(hideBin(process.argv))
319319
execArgs.push(String(args['params-json']));
320320
}
321321
await ensureDaemon(socketPath);
322+
const timeoutMs = parseTimeoutMs(args.timeout);
322323
await handleDaemonExec(
323324
execArgs,
324325
socketPath,
325-
typeof args.timeout === 'number' && args.timeout > 0
326-
? { timeoutMs: args.timeout * 1000 }
327-
: {},
326+
timeoutMs === undefined ? {} : { timeoutMs },
328327
);
329328
},
330329
)
@@ -338,13 +337,19 @@ const yargsInstance = yargs(hideBin(process.argv))
338337
type: 'string',
339338
demandOption: true,
340339
})
340+
.option('timeout', {
341+
describe: 'Read timeout in seconds (default: no timeout)',
342+
type: 'number',
343+
})
341344
.example(
342345
'$0 daemon redeem-url ocap:abc123@12D3KooW...,/ip4/...',
343346
'Redeem an OCAP URL',
344347
),
345348
async (args) => {
346349
await ensureDaemon(socketPath);
347-
await handleRedeemURL(args.url, socketPath);
350+
await handleRedeemURL(args.url, socketPath, {
351+
timeoutMs: parseTimeoutMs(args.timeout),
352+
});
348353
},
349354
)
350355
.command(
@@ -413,9 +418,7 @@ const yargsInstance = yargs(hideBin(process.argv))
413418
args: parsedArgs,
414419
socketPath,
415420
raw: args.raw,
416-
...(typeof args.timeout === 'number' && args.timeout > 0
417-
? { timeoutMs: args.timeout * 1000 }
418-
: {}),
421+
timeoutMs: parseTimeoutMs(args.timeout),
419422
});
420423
},
421424
);

packages/kernel-cli/src/commands/daemon.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,15 +317,19 @@ export async function handleDaemonExec(
317317
*
318318
* @param url - The OCAP URL to redeem.
319319
* @param socketPath - The daemon socket path.
320+
* @param options - Additional options.
321+
* @param options.timeoutMs - Read timeout in milliseconds.
320322
*/
321323
export async function handleRedeemURL(
322324
url: string,
323325
socketPath: string,
326+
{ timeoutMs }: { timeoutMs?: number } = {},
324327
): Promise<void> {
325328
const response = await sendCommand({
326329
socketPath,
327330
method: 'redeemOcapURL',
328331
params: { url },
332+
...(timeoutMs === undefined ? {} : { timeoutMs }),
329333
});
330334

331335
if (isJsonRpcFailure(response)) {

packages/kernel-cli/src/utils.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
44
import {
55
isErrorWithCode,
66
isProcessAlive,
7+
parseTimeoutMs,
78
readPidFile,
89
sendSignal,
910
waitFor,
@@ -123,6 +124,28 @@ describe('utils', () => {
123124
});
124125
});
125126

127+
describe('parseTimeoutMs', () => {
128+
it('returns undefined when value is undefined', () => {
129+
expect(parseTimeoutMs(undefined)).toBeUndefined();
130+
});
131+
132+
it.each([1, 5, 30, 100])(
133+
'converts %i seconds to milliseconds',
134+
(seconds) => {
135+
expect(parseTimeoutMs(seconds)).toBe(seconds * 1000);
136+
},
137+
);
138+
139+
it.each([0, -1, -100, 1.5, 0.1, NaN, Infinity, -Infinity])(
140+
'throws for invalid value %s',
141+
(value) => {
142+
expect(() => parseTimeoutMs(value)).toThrow(
143+
'--timeout must be a positive integer',
144+
);
145+
},
146+
);
147+
});
148+
126149
describe('waitFor', () => {
127150
beforeEach(() => {
128151
vi.useFakeTimers({ now: 0 });

packages/kernel-cli/src/utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
import { readFile } from 'node:fs/promises';
22

3+
/**
4+
* Parse and validate a `--timeout` option value (seconds → milliseconds).
5+
*
6+
* @param value - The raw option value from yargs.
7+
* @returns The timeout in milliseconds, or `undefined` if not provided.
8+
* @throws If the value is not a positive integer.
9+
*/
10+
export function parseTimeoutMs(value: number | undefined): number | undefined {
11+
if (value === undefined) {
12+
return undefined;
13+
}
14+
if (!Number.isInteger(value) || value <= 0) {
15+
throw new Error(`--timeout must be a positive integer, got: ${value}`);
16+
}
17+
return value * 1000;
18+
}
19+
320
/**
421
* Check whether an unknown error is a Node.js system error with the given code.
522
*

packages/kernel-utils/src/nodejs/ocap-home.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,9 @@ import { join } from 'node:path';
99
*/
1010
export function getOcapHome(): string {
1111
// eslint-disable-next-line n/no-process-env
12-
return process.env.OCAP_HOME ?? join(homedir(), '.ocap');
12+
const envValue = process.env.OCAP_HOME;
13+
if (envValue) {
14+
return envValue;
15+
}
16+
return join(homedir(), '.ocap');
1317
}

packages/kernel-utils/src/prettify-smallcaps.test.ts

Lines changed: 50 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,41 @@ import { describe, it, expect } from 'vitest';
33
import { prettifySmallcaps } from './prettify-smallcaps.ts';
44

55
describe('prettifySmallcaps', () => {
6-
it('decodes a plain string', () => {
7-
expect(prettifySmallcaps({ body: '#"0xca46b9"', slots: [] })).toBe(
8-
'0xca46b9',
9-
);
10-
});
11-
12-
it('decodes a number', () => {
13-
expect(prettifySmallcaps({ body: '#42', slots: [] })).toBe(42);
14-
});
15-
16-
it('decodes null', () => {
17-
expect(prettifySmallcaps({ body: '#null', slots: [] })).toBeNull();
18-
});
19-
20-
it('decodes a boolean', () => {
21-
expect(prettifySmallcaps({ body: '#true', slots: [] })).toBe(true);
22-
});
23-
24-
it('replaces a remotable slot reference', () => {
25-
expect(prettifySmallcaps({ body: '#"$0"', slots: ['ko12'] })).toBe(
26-
'<ko12>',
27-
);
28-
});
29-
30-
it('replaces a remotable slot reference with interface name', () => {
31-
expect(
32-
prettifySmallcaps({ body: '#"$0.Alleged: MyObj"', slots: ['ko12'] }),
33-
).toBe('<ko12> (Alleged: MyObj)');
34-
});
35-
36-
it('replaces a promise slot reference', () => {
37-
expect(prettifySmallcaps({ body: '#"&0"', slots: ['kp42'] })).toBe(
38-
'<kp42>',
39-
);
6+
it.each([
7+
['plain string', '#"0xca46b9"', [], '0xca46b9'],
8+
['number', '#42', [], 42],
9+
['null', '#null', [], null],
10+
['boolean', '#true', [], true],
11+
])('decodes a %s', (_label, body, slots, expected) => {
12+
expect(prettifySmallcaps({ body, slots })).toStrictEqual(expected);
13+
});
14+
15+
it.each([
16+
['remotable', '#"$0"', ['ko12'], '<ko12>'],
17+
[
18+
'remotable with iface',
19+
'#"$0.Alleged: MyObj"',
20+
['ko12'],
21+
'<ko12> (Alleged: MyObj)',
22+
],
23+
['promise', '#"&0"', ['kp42'], '<kp42>'],
24+
['missing slot index', '#"$5"', ['ko1'], '<?5>'],
25+
])('replaces a %s slot reference', (_label, body, slots, expected) => {
26+
expect(prettifySmallcaps({ body, slots })).toBe(expected);
27+
});
28+
29+
it.each([
30+
['escaped string (!)', '#"!$0"', '$0'],
31+
['double escape (!!)', '#"!!hello"', '!hello'],
32+
['non-negative bigint (+)', '#"+7"', '7n'],
33+
['negative bigint (-)', '#"-7"', '-7n'],
34+
['#undefined', '#"#undefined"', '[undefined]'],
35+
['#NaN', '#"#NaN"', '[NaN]'],
36+
['#Infinity', '#"#Infinity"', '[Infinity]'],
37+
['#-Infinity', '#"#-Infinity"', '[-Infinity]'],
38+
['symbol (%)', '#"%foo"', '[Symbol: foo]'],
39+
])('decodes %s', (_label, body, expected) => {
40+
expect(prettifySmallcaps({ body, slots: [] })).toBe(expected);
4041
});
4142

4243
it('decodes an object with mixed values', () => {
@@ -60,66 +61,12 @@ describe('prettifySmallcaps', () => {
6061
});
6162
});
6263

63-
it('strips smallcaps escape prefix from strings', () => {
64-
expect(prettifySmallcaps({ body: '#"!$0"', slots: ['ko1'] })).toBe('$0');
65-
});
66-
67-
it('strips double escape prefix', () => {
68-
expect(prettifySmallcaps({ body: '#"!!hello"', slots: [] })).toBe('!hello');
69-
});
70-
7164
it('leaves non-slot strings unchanged', () => {
7265
expect(
7366
prettifySmallcaps({ body: '#{"text":"hello world"}', slots: [] }),
7467
).toStrictEqual({ text: 'hello world' });
7568
});
7669

77-
it('handles missing slot index gracefully', () => {
78-
expect(prettifySmallcaps({ body: '#"$5"', slots: ['ko1'] })).toBe('<?5>');
79-
});
80-
81-
it('throws if body does not start with #', () => {
82-
expect(() => prettifySmallcaps({ body: '"hello"', slots: [] })).toThrow(
83-
"Expected body to start with '#'",
84-
);
85-
});
86-
87-
it('decodes a non-negative bigint', () => {
88-
expect(prettifySmallcaps({ body: '#"+7"', slots: [] })).toBe('7n');
89-
});
90-
91-
it('decodes a negative bigint', () => {
92-
expect(prettifySmallcaps({ body: '#"-7"', slots: [] })).toBe('-7n');
93-
});
94-
95-
it('decodes #undefined', () => {
96-
expect(prettifySmallcaps({ body: '#"#undefined"', slots: [] })).toBe(
97-
'[undefined]',
98-
);
99-
});
100-
101-
it('decodes #NaN', () => {
102-
expect(prettifySmallcaps({ body: '#"#NaN"', slots: [] })).toBe('[NaN]');
103-
});
104-
105-
it('decodes #Infinity', () => {
106-
expect(prettifySmallcaps({ body: '#"#Infinity"', slots: [] })).toBe(
107-
'[Infinity]',
108-
);
109-
});
110-
111-
it('decodes #-Infinity', () => {
112-
expect(prettifySmallcaps({ body: '#"#-Infinity"', slots: [] })).toBe(
113-
'[-Infinity]',
114-
);
115-
});
116-
117-
it('decodes a symbol', () => {
118-
expect(prettifySmallcaps({ body: '#"%foo"', slots: [] })).toBe(
119-
'[Symbol: foo]',
120-
);
121-
});
122-
12370
it('decodes a tagged value', () => {
12471
expect(
12572
prettifySmallcaps({
@@ -129,22 +76,15 @@ describe('prettifySmallcaps', () => {
12976
).toStrictEqual({ '[Tagged: match:any]': '[undefined]' });
13077
});
13178

132-
it('decodes an error', () => {
133-
expect(
134-
prettifySmallcaps({
135-
body: '#{"#error":"boom","name":"TypeError"}',
136-
slots: [],
137-
}),
138-
).toBe('[TypeError: boom]');
139-
});
140-
141-
it('decodes an error without name', () => {
142-
expect(
143-
prettifySmallcaps({
144-
body: '#{"#error":"something broke"}',
145-
slots: [],
146-
}),
147-
).toBe('[Error: something broke]');
79+
it.each([
80+
['with name', '#{"#error":"boom","name":"TypeError"}', '[TypeError: boom]'],
81+
[
82+
'without name',
83+
'#{"#error":"something broke"}',
84+
'[Error: something broke]',
85+
],
86+
])('decodes an error %s', (_label, body, expected) => {
87+
expect(prettifySmallcaps({ body, slots: [] })).toBe(expected);
14888
});
14989

15090
it('unescapes record keys', () => {
@@ -155,4 +95,10 @@ describe('prettifySmallcaps', () => {
15595
}),
15696
).toStrictEqual({ '#foo': 'bar', normal: 'baz' });
15797
});
98+
99+
it('throws if body does not start with #', () => {
100+
expect(() => prettifySmallcaps({ body: '"hello"', slots: [] })).toThrow(
101+
"Expected body to start with '#'",
102+
);
103+
});
158104
});

packages/kernel-utils/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export const isJsonRpcMessage = (value: unknown): value is JsonRpcMessage =>
9696
is(value, JsonRpcMessageStruct);
9797

9898
/**
99-
* Check whether a value has the shape of Endo CapData (`{ body: string, slots: unknown[] }`).
99+
* Check whether a value has the shape of Endo CapData (`{ body: string, slots: string[] }`).
100100
*
101101
* @param value - The value to check.
102102
* @returns `true` when `value` looks like CapData.

0 commit comments

Comments
 (0)