Skip to content

Commit 660e592

Browse files
hobostayTest Userclaudesenamakel
authored
fix: add error handling to parseServiceCliOutput (tinyhumansai#1737)
Co-authored-by: Test User <test@example.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
1 parent 8dda038 commit 660e592

2 files changed

Lines changed: 89 additions & 2 deletions

File tree

app/src/utils/tauriCommands/common.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
1717

18-
import { isTauri } from './common';
18+
import { isTauri, parseServiceCliOutput } from './common';
1919

2020
const coreIsTauriMock = vi.fn();
2121

@@ -90,3 +90,64 @@ describe('isTauri (tauriCommands/common)', () => {
9090
expect(isTauri()).toBe(false);
9191
});
9292
});
93+
94+
// `parseServiceCliOutput` runs against raw CLI stdout from the `openhuman`
95+
// sidecar. The core process can crash mid-write, return a partial response, or
96+
// drift from the expected JSON schema across versions — any of which produces
97+
// malformed input. The guard must reject those cases with a descriptive error
98+
// rather than handing typed garbage back to callers.
99+
describe('parseServiceCliOutput (tauriCommands/common)', () => {
100+
it('returns the parsed response when shape matches CommandResponse', () => {
101+
const raw = JSON.stringify({ result: { value: 42 }, logs: ['ok'] });
102+
103+
const parsed = parseServiceCliOutput<{ value: number }>(raw);
104+
105+
expect(parsed.result).toEqual({ value: 42 });
106+
expect(parsed.logs).toEqual(['ok']);
107+
});
108+
109+
it('accepts null result and empty logs (valid CommandResponse shape)', () => {
110+
const raw = JSON.stringify({ result: null, logs: [] });
111+
112+
const parsed = parseServiceCliOutput<null>(raw);
113+
114+
expect(parsed.result).toBeNull();
115+
expect(parsed.logs).toEqual([]);
116+
});
117+
118+
it('throws a descriptive error when the input is not valid JSON', () => {
119+
expect(() => parseServiceCliOutput('not-json')).toThrow(/Failed to parse service CLI output/);
120+
});
121+
122+
it('throws when the parsed value is null', () => {
123+
expect(() => parseServiceCliOutput('null')).toThrow(/CommandResponse shape/);
124+
});
125+
126+
it('throws when the parsed value is an array (not an object)', () => {
127+
expect(() => parseServiceCliOutput('[]')).toThrow(/CommandResponse shape/);
128+
});
129+
130+
it('throws when required `logs` field is missing', () => {
131+
expect(() => parseServiceCliOutput(JSON.stringify({ result: 1 }))).toThrow(
132+
/CommandResponse shape/
133+
);
134+
});
135+
136+
it('throws when required `result` field is missing', () => {
137+
expect(() => parseServiceCliOutput(JSON.stringify({ logs: [] }))).toThrow(
138+
/CommandResponse shape/
139+
);
140+
});
141+
142+
it('throws when `logs` is not an array', () => {
143+
expect(() => parseServiceCliOutput(JSON.stringify({ result: 1, logs: 'oops' }))).toThrow(
144+
/CommandResponse shape/
145+
);
146+
});
147+
148+
it('throws when `logs` contains non-string entries', () => {
149+
expect(() => parseServiceCliOutput(JSON.stringify({ result: 1, logs: [1, 2] }))).toThrow(
150+
/CommandResponse shape/
151+
);
152+
});
153+
});

app/src/utils/tauriCommands/common.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,33 @@ export function tauriErrorMessage(err: unknown): string {
6464
return 'Unknown Tauri invoke error';
6565
}
6666

67+
function isCommandResponse<T>(value: unknown): value is CommandResponse<T> {
68+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
69+
return false;
70+
}
71+
const candidate = value as { result?: unknown; logs?: unknown };
72+
if (!('result' in candidate) || !('logs' in candidate)) {
73+
return false;
74+
}
75+
if (!Array.isArray(candidate.logs)) {
76+
return false;
77+
}
78+
return candidate.logs.every(entry => typeof entry === 'string');
79+
}
80+
6781
export function parseServiceCliOutput<T>(raw: string): CommandResponse<T> {
68-
const parsed = JSON.parse(raw) as CommandResponse<T>;
82+
let parsed: unknown;
83+
try {
84+
parsed = JSON.parse(raw);
85+
} catch (err) {
86+
throw new Error(
87+
`Failed to parse service CLI output as JSON: ${err instanceof Error ? err.message : String(err)}`
88+
);
89+
}
90+
if (!isCommandResponse<T>(parsed)) {
91+
throw new Error(
92+
'Failed to parse service CLI output as JSON: parsed value does not match CommandResponse shape'
93+
);
94+
}
6995
return parsed;
7096
}

0 commit comments

Comments
 (0)