fix(walkthrough): name an outdated server instead of failing to parse its HTML
A server without these routes does not answer 404 with JSON. The unmatched /api path reaches the OpenCode proxy, and OpenCode serves its embedded web UI for anything it does not recognise — HTML, status 200 — so a client newer than its server parsed a web page as JSON and put "Unexpected token '<', "<!doctype" in the panel, naming neither the cause nor the remedy. The client now checks the content type before parsing. A non-JSON answer on 2xx or 404 blocks with "this server is older than the app, update it and refresh". A non-JSON 5xx keeps its own failure: a server that answered badly is not a server missing the feature, and sending that user to upgrade chases the wrong thing.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
// A server older than this client does not answer 404-with-JSON: unmatched
|
||||
// `/api/*` reaches the OpenCode proxy, and OpenCode serves its embedded web UI
|
||||
// for any unknown path — HTML, status 200. These tests pin that the panel gets
|
||||
// an actionable code instead of a JSON parser error.
|
||||
|
||||
let nextResponse: Response = new Response('{}', { headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
mock.module('@/lib/runtime-fetch', () => ({
|
||||
runtimeFetch: mock(async () => nextResponse),
|
||||
}));
|
||||
|
||||
const { fetchWalkthrough, generateWalkthrough } = await import('./api');
|
||||
const { WalkthroughError } = await import('./types');
|
||||
import type { WalkthroughSource } from './types';
|
||||
|
||||
const SOURCE: WalkthroughSource = { kind: 'working-tree', scope: 'all' };
|
||||
|
||||
const html = (status: number) =>
|
||||
new Response('<!doctype html><html><body>OpenCode</body></html>', {
|
||||
status,
|
||||
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||||
});
|
||||
|
||||
describe('walkthrough api', () => {
|
||||
beforeEach(() => {
|
||||
nextResponse = new Response('{}', { headers: { 'Content-Type': 'application/json' } });
|
||||
});
|
||||
|
||||
test('reads a JSON answer', async () => {
|
||||
nextResponse = new Response(JSON.stringify({ hunkCount: 3 }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
const result = await fetchWalkthrough('/repo', SOURCE);
|
||||
|
||||
expect(result.hunkCount).toBe(3);
|
||||
});
|
||||
|
||||
test('reports HTML served with 200 as a server without the routes', async () => {
|
||||
nextResponse = html(200);
|
||||
|
||||
const error = await fetchWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
|
||||
|
||||
expect(error).toBeInstanceOf(WalkthroughError);
|
||||
expect((error as InstanceType<typeof WalkthroughError>).code).toBe('server-unsupported');
|
||||
expect((error as Error).message).not.toContain('JSON');
|
||||
});
|
||||
|
||||
test('reports a non-JSON 404 the same way', async () => {
|
||||
nextResponse = html(404);
|
||||
|
||||
const error = await generateWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
|
||||
|
||||
expect((error as InstanceType<typeof WalkthroughError>).code).toBe('server-unsupported');
|
||||
});
|
||||
|
||||
test('keeps a server-side failure rather than blaming the server version', async () => {
|
||||
nextResponse = new Response(JSON.stringify({ error: 'model exploded', code: 'output-exhausted' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
const error = await generateWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
|
||||
|
||||
expect((error as InstanceType<typeof WalkthroughError>).code).toBe('output-exhausted');
|
||||
expect((error as Error).message).toBe('model exploded');
|
||||
});
|
||||
|
||||
test('a 5xx that is not JSON is a broken server, not a missing route', async () => {
|
||||
nextResponse = html(502);
|
||||
|
||||
const error = await fetchWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
|
||||
|
||||
expect((error as InstanceType<typeof WalkthroughError>).code).toBe(undefined);
|
||||
expect((error as Error).message).toBe('Failed to load walkthrough');
|
||||
});
|
||||
|
||||
test('JSON that does not parse is reported without the parser wording', async () => {
|
||||
nextResponse = new Response('{"walkthrough":', { headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
const error = await fetchWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
|
||||
|
||||
expect(error).toBeInstanceOf(WalkthroughError);
|
||||
expect((error as Error).message).toBe('The server returned a malformed walkthrough response');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user