fix: add error handling to parseServiceCliOutput (#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>
This commit is contained in:
Qiaochu Hu
2026-05-15 04:12:44 -07:00
committed by GitHub
co-authored by Test User Claude Opus 4.7 Steven Enamakel
parent 8dda038280
commit 660e592fcc
2 changed files with 89 additions and 2 deletions
+62 -1
View File
@@ -15,7 +15,7 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { isTauri } from './common';
import { isTauri, parseServiceCliOutput } from './common';
const coreIsTauriMock = vi.fn();
@@ -90,3 +90,64 @@ describe('isTauri (tauriCommands/common)', () => {
expect(isTauri()).toBe(false);
});
});
// `parseServiceCliOutput` runs against raw CLI stdout from the `openhuman`
// sidecar. The core process can crash mid-write, return a partial response, or
// drift from the expected JSON schema across versions — any of which produces
// malformed input. The guard must reject those cases with a descriptive error
// rather than handing typed garbage back to callers.
describe('parseServiceCliOutput (tauriCommands/common)', () => {
it('returns the parsed response when shape matches CommandResponse', () => {
const raw = JSON.stringify({ result: { value: 42 }, logs: ['ok'] });
const parsed = parseServiceCliOutput<{ value: number }>(raw);
expect(parsed.result).toEqual({ value: 42 });
expect(parsed.logs).toEqual(['ok']);
});
it('accepts null result and empty logs (valid CommandResponse shape)', () => {
const raw = JSON.stringify({ result: null, logs: [] });
const parsed = parseServiceCliOutput<null>(raw);
expect(parsed.result).toBeNull();
expect(parsed.logs).toEqual([]);
});
it('throws a descriptive error when the input is not valid JSON', () => {
expect(() => parseServiceCliOutput('not-json')).toThrow(/Failed to parse service CLI output/);
});
it('throws when the parsed value is null', () => {
expect(() => parseServiceCliOutput('null')).toThrow(/CommandResponse shape/);
});
it('throws when the parsed value is an array (not an object)', () => {
expect(() => parseServiceCliOutput('[]')).toThrow(/CommandResponse shape/);
});
it('throws when required `logs` field is missing', () => {
expect(() => parseServiceCliOutput(JSON.stringify({ result: 1 }))).toThrow(
/CommandResponse shape/
);
});
it('throws when required `result` field is missing', () => {
expect(() => parseServiceCliOutput(JSON.stringify({ logs: [] }))).toThrow(
/CommandResponse shape/
);
});
it('throws when `logs` is not an array', () => {
expect(() => parseServiceCliOutput(JSON.stringify({ result: 1, logs: 'oops' }))).toThrow(
/CommandResponse shape/
);
});
it('throws when `logs` contains non-string entries', () => {
expect(() => parseServiceCliOutput(JSON.stringify({ result: 1, logs: [1, 2] }))).toThrow(
/CommandResponse shape/
);
});
});
+27 -1
View File
@@ -64,7 +64,33 @@ export function tauriErrorMessage(err: unknown): string {
return 'Unknown Tauri invoke error';
}
function isCommandResponse<T>(value: unknown): value is CommandResponse<T> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
const candidate = value as { result?: unknown; logs?: unknown };
if (!('result' in candidate) || !('logs' in candidate)) {
return false;
}
if (!Array.isArray(candidate.logs)) {
return false;
}
return candidate.logs.every(entry => typeof entry === 'string');
}
export function parseServiceCliOutput<T>(raw: string): CommandResponse<T> {
const parsed = JSON.parse(raw) as CommandResponse<T>;
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (err) {
throw new Error(
`Failed to parse service CLI output as JSON: ${err instanceof Error ? err.message : String(err)}`
);
}
if (!isCommandResponse<T>(parsed)) {
throw new Error(
'Failed to parse service CLI output as JSON: parsed value does not match CommandResponse shape'
);
}
return parsed;
}