mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
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:
co-authored by
Test User
Claude Opus 4.7
Steven Enamakel
parent
8dda038280
commit
660e592fcc
@@ -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/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user