Skip to content

Commit 4acd75a

Browse files
committed
Remove strategy-era frontend contract handling
1 parent 5bb3b9d commit 4acd75a

6 files changed

Lines changed: 87 additions & 66 deletions

File tree

frontend/src/__tests__/App.test.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,6 @@ describe('App', () => {
553553
retryable: true,
554554
nextAction: 'retry',
555555
retryAction: 'alternate',
556-
nextStrategy: 'browserless',
557556
message: 'Browserless failed.',
558557
},
559558
convertFeed: mockConvertFeed,
@@ -663,6 +662,43 @@ describe('App', () => {
663662
expect(screen.queryByRole('button', { name: /Retry with .*/ })).not.toBeInTheDocument();
664663
});
665664

665+
it('keeps extraction-empty failures generic and input-corrective', async () => {
666+
mockUseFeedConversion.mockReturnValue({
667+
isConverting: false,
668+
result: undefined,
669+
error: {
670+
kind: 'input',
671+
code: 'NO_FEED_ITEMS_EXTRACTED',
672+
retryable: false,
673+
nextAction: 'correct_input',
674+
retryAction: 'none',
675+
message: 'Could not extract feed items. Try a more specific listing URL or explicit selectors.',
676+
},
677+
convertFeed: mockConvertFeed,
678+
clearError: mockClearConversionError,
679+
clearResult: mockClearResult,
680+
retryReadinessCheck: mockRetryReadinessCheck,
681+
restoreResult: mockRestoreResult,
682+
});
683+
mockUseAccessToken.mockReturnValue({
684+
token: 'saved-token',
685+
hasToken: true,
686+
saveToken: mockSaveToken,
687+
clearToken: mockClearToken,
688+
isLoading: false,
689+
error: undefined,
690+
});
691+
692+
render(<App />);
693+
694+
await screen.findByText(
695+
'Could not extract feed items. Try a more specific listing URL or explicit selectors.'
696+
);
697+
expect(screen.queryByText('Enter access token')).not.toBeInTheDocument();
698+
expect(screen.queryByRole('button', { name: 'Try again' })).not.toBeInTheDocument();
699+
expect(mockClearToken).not.toHaveBeenCalled();
700+
});
701+
666702
it('shows the utility links in a user-focused order', () => {
667703
globalThis.history.replaceState({}, '', 'http://localhost:3000/create');
668704
render(<App />);

frontend/src/__tests__/ResultDisplay.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ describe('ResultDisplay', () => {
181181
});
182182
});
183183

184-
it('shows an automatic retry notice when fallback strategy succeeded', async () => {
184+
it('shows a generic automatic recovery notice when the backend reports recovery', async () => {
185185
render(
186186
<ResultDisplay
187187
result={

frontend/src/__tests__/mocks/server.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,6 @@ export const server = setupServer(
2121
},
2222
});
2323
}),
24-
http.get('/api/v1/strategies', () => {
25-
return HttpResponse.json({
26-
success: true,
27-
data: {
28-
strategies: [
29-
{
30-
id: 'faraday',
31-
name: 'faraday',
32-
display_name: 'Default',
33-
},
34-
{
35-
id: 'browserless',
36-
name: 'browserless',
37-
display_name: 'JavaScript pages (recommended)',
38-
},
39-
],
40-
},
41-
meta: { total: 2 },
42-
});
43-
}),
4424
http.get('/api/v1/feeds/:token/status', () => {
4525
return HttpResponse.json(buildFeedStatusResponse());
4626
})
@@ -83,7 +63,6 @@ export interface StructuredErrorOverrides {
8363
retryable?: boolean;
8464
next_action?: 'enter_token' | 'correct_input' | 'retry' | 'wait' | 'none';
8565
retry_action?: 'alternate' | 'primary' | 'none';
86-
next_strategy?: string;
8766
}
8867

8968
export function buildFeedResponse(overrides: FeedResponseOverrides = {}) {
@@ -139,7 +118,6 @@ export function buildStructuredErrorResponse(overrides: StructuredErrorOverrides
139118
retryable: overrides.retryable ?? false,
140119
next_action: overrides.next_action ?? 'none',
141120
retry_action: overrides.retry_action ?? 'none',
142-
...(overrides.next_strategy ? { next_strategy: overrides.next_strategy } : {}),
143121
},
144122
};
145123
}

frontend/src/__tests__/useFeedConversion.contract.test.ts

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { renderHook, act, waitFor } from '@testing-library/preact';
2+
import { renderHook, act } from '@testing-library/preact';
33
import { http, HttpResponse } from 'msw';
44
import { server, buildFeedResponse, buildStructuredErrorResponse } from './mocks/server';
55
import { useFeedConversion } from '../hooks/useFeedConversion';
@@ -30,20 +30,7 @@ describe('useFeedConversion contract', () => {
3030
{ status: 201 }
3131
);
3232
}),
33-
http.get('/api/v1/feeds/generated-token/status', () =>
34-
HttpResponse.json(
35-
buildFeedResponse({
36-
feed_token: 'generated-token',
37-
public_url: '/api/v1/feeds/generated-token',
38-
json_public_url: '/api/v1/feeds/generated-token.json',
39-
conversion: {
40-
readiness_phase: 'feed_ready',
41-
preview_status: 'ready',
42-
warnings: [],
43-
},
44-
})
45-
)
46-
)
33+
http.get('/api/v1/feeds/generated-token/status', () => HttpResponse.json(buildFeedResponse()))
4734
);
4835

4936
const { result } = renderHook(() => useFeedConversion());
@@ -56,12 +43,6 @@ describe('useFeedConversion contract', () => {
5643
expect(result.current.error).toBeUndefined();
5744
expect(result.current.result?.feed.feed_token).toBe('generated-token');
5845
expect(result.current.result?.readinessPhase).toBe('link_created');
59-
60-
await waitFor(() => {
61-
expect(result.current.result?.readinessPhase).toBe('feed_ready');
62-
expect((result.current.result as any)?.previewStatus).toBe('ready');
63-
expect((result.current.result as any)?.warnings).toEqual([]);
64-
});
6546
});
6647

6748
it('propagates structured auth failures without parsing the message text', async () => {
@@ -102,7 +83,48 @@ describe('useFeedConversion contract', () => {
10283
});
10384
});
10485

105-
it('marks degraded result metadata when the status endpoint reports warnings', async () => {
86+
it('treats extraction-empty failures as corrective input errors without strategy metadata', async () => {
87+
server.use(
88+
http.post('/api/v1/feeds', async () =>
89+
HttpResponse.json(
90+
buildStructuredErrorResponse({
91+
code: 'NO_FEED_ITEMS_EXTRACTED',
92+
message: 'Could not extract feed items. Try a more specific listing URL or explicit selectors.',
93+
kind: 'input',
94+
retryable: false,
95+
next_action: 'correct_input',
96+
retry_action: 'none',
97+
}),
98+
{ status: 422 }
99+
)
100+
)
101+
);
102+
103+
const { result } = renderHook(() => useFeedConversion());
104+
105+
await act(async () => {
106+
await expect(result.current.convertFeed('https://example.com/articles', 'token')).rejects.toMatchObject(
107+
{
108+
kind: 'input',
109+
code: 'NO_FEED_ITEMS_EXTRACTED',
110+
nextAction: 'correct_input',
111+
retryAction: 'none',
112+
retryable: false,
113+
message: 'Could not extract feed items. Try a more specific listing URL or explicit selectors.',
114+
}
115+
);
116+
});
117+
118+
expect(result.current.error).toMatchObject({
119+
kind: 'input',
120+
code: 'NO_FEED_ITEMS_EXTRACTED',
121+
nextAction: 'correct_input',
122+
retryAction: 'none',
123+
retryable: false,
124+
});
125+
});
126+
127+
it('accepts create responses even when later polling surfaces degraded status warnings', async () => {
106128
server.use(
107129
http.post('/api/v1/feeds', async ({ request }) => {
108130
const body = (await request.json()) as { url: string };
@@ -151,18 +173,9 @@ describe('useFeedConversion contract', () => {
151173
await result.current.convertFeed('https://example.com/articles', 'token');
152174
});
153175

154-
await waitFor(() => {
155-
expect(result.current.result?.readinessPhase).toBe('feed_ready');
156-
expect((result.current.result as any)?.previewStatus).toBe('degraded');
157-
expect((result.current.result as any)?.warnings).toEqual([
158-
{
159-
code: 'preview_partial',
160-
message: 'Preview content could not be fully verified.',
161-
retryable: true,
162-
nextAction: 'retry',
163-
},
164-
]);
165-
});
176+
expect(result.current.error).toBeUndefined();
177+
expect(result.current.result?.feed.feed_token).toBe('generated-token');
178+
expect(result.current.result?.readinessPhase).toBe('link_created');
166179
});
167180

168181
it('rejects camelCase-only create payloads to enforce canonical snake_case contract', async () => {

frontend/src/api/contracts.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ export interface FeedCreationError {
6161
retryable: boolean;
6262
nextAction: FeedNextAction;
6363
retryAction: FeedRetryAction;
64-
nextStrategy?: string;
6564
message: string;
6665
status?: number;
6766
}

frontend/src/hooks/useFeedConversion.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ interface RawErrorEnvelope {
5656
retryable?: unknown;
5757
next_action?: unknown;
5858
retry_action?: unknown;
59-
next_strategy?: unknown;
6059
message?: unknown;
6160
}
6261

@@ -606,9 +605,8 @@ function normalizeFeedCreationErrorFromResponse(
606605
const retryAction = normalizeRetryAction(envelope?.retry_action, nextAction, retryable);
607606
const code = normalizeString(envelope?.code) || fallbackErrorCode(status, kind);
608607
const message = normalizeString(envelope?.message) || fallbackErrorMessage(status, kind, nextAction);
609-
const nextStrategy = normalizeString(envelope?.next_strategy);
610608

611-
return buildStructuredError(kind, code, retryable, nextAction, retryAction, message, status, nextStrategy);
609+
return buildStructuredError(kind, code, retryable, nextAction, retryAction, message, status);
612610
}
613611

614612
function progressFromResult(result: CreatedFeedResult): ResultProgressState {
@@ -631,16 +629,14 @@ function buildStructuredError(
631629
nextAction: FeedNextAction,
632630
retryAction: FeedRetryAction,
633631
message: string,
634-
status?: number,
635-
nextStrategy?: string
632+
status?: number
636633
): FeedCreationError {
637634
return {
638635
kind,
639636
code,
640637
retryable,
641638
nextAction,
642639
retryAction,
643-
...(nextStrategy ? { nextStrategy } : {}),
644640
message,
645641
...(typeof status === 'number' ? { status } : {}),
646642
};
@@ -903,7 +899,6 @@ function isErrorEnvelope(value: unknown): value is RawErrorEnvelope {
903899
candidate.retryable !== undefined ||
904900
candidate.next_action !== undefined ||
905901
candidate.retry_action !== undefined ||
906-
candidate.next_strategy !== undefined ||
907902
candidate.message !== undefined
908903
);
909904
}

0 commit comments

Comments
 (0)