Skip to content

Commit 0e4fd76

Browse files
committed
Fix OpenAPI status path determinism and smoke contract
1 parent ef5fe59 commit 0e4fd76

6 files changed

Lines changed: 411 additions & 339 deletions

File tree

frontend/e2e/smoke.spec.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ test.describe('frontend smoke', () => {
5151

5252
await expect(page.getByLabel('Page URL')).toBeVisible();
5353
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();
54-
await expect(page.getByRole('button', { name: 'More' })).toBeVisible();
5554

5655
await page.getByLabel('Page URL').fill('https://example.com/articles');
5756
await page.getByRole('button', { name: 'Generate feed URL' }).click();
@@ -63,11 +62,11 @@ test.describe('frontend smoke', () => {
6362

6463
await page.getByRole('button', { name: 'Back' }).click();
6564
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();
66-
await expect(page.getByRole('button', { name: 'More' })).toBeVisible();
6765

6866
await page.goBack();
69-
await expect(page.getByRole('heading', { name: 'Enter access token' })).toBeVisible();
70-
await expect(page.locator('.form-shell')).toHaveAttribute('data-state', 'token_required');
67+
await expect(page).toHaveURL(/\/create(?:\?.*)?$/);
68+
await expect(page.getByRole('button', { name: 'Generate feed URL' })).toBeVisible();
69+
await expect(page.locator('.form-shell')).toHaveAttribute('data-state', 'idle');
7170
});
7271

7372
test('restores result deep links and shows a recovery state when snapshot is missing', async ({ page }) => {
@@ -130,12 +129,23 @@ test.describe('frontend smoke', () => {
130129
feed_token: 'generated-token',
131130
public_url: '/api/v1/feeds/generated-token',
132131
json_public_url: '/api/v1/feeds/generated-token.json',
132+
created_at: '2026-04-05T08:59:00.000Z',
133+
updated_at: '2026-04-05T09:00:00.000Z',
133134
},
134135
preview: {
135-
items: [],
136+
items: [
137+
{
138+
title: 'Sample preview item',
139+
excerpt: 'Current restore snapshots include preview content.',
140+
publishedLabel: 'April 5, 2026',
141+
url: 'https://example.com/articles/sample-preview-item',
142+
},
143+
],
136144
isLoading: false,
137145
},
138146
readinessPhase: 'feed_ready',
147+
previewStatus: 'ready',
148+
warnings: [],
139149
},
140150
})
141151
);
@@ -145,8 +155,13 @@ test.describe('frontend smoke', () => {
145155

146156
await expect(page.getByRole('heading', { name: 'Feed ready' })).toBeVisible();
147157
await expect(page.locator('.result-shell')).toHaveAttribute('data-state', 'ready');
158+
await expect(page.getByText('Example Feed')).toBeVisible();
148159
await expect(page.getByRole('link', { name: 'Open feed' })).toBeVisible();
160+
await expect(page.getByRole('link', { name: 'Open JSON Feed' })).toBeVisible();
161+
await expect(page.getByRole('link', { name: 'Open in feed reader' })).toBeVisible();
149162
await expect(page.getByRole('button', { name: 'Create another feed' })).toBeVisible();
163+
await expect(page.getByText('Sample preview item')).toBeVisible();
164+
await expect(page.getByText('Current restore snapshots include preview content.')).toBeVisible();
150165

151166
await page.evaluate(() => {
152167
localStorage.removeItem('html2rss_feed_result_snapshot:missing-token');
@@ -155,6 +170,9 @@ test.describe('frontend smoke', () => {
155170
await page.goto('/result/missing-token');
156171

157172
await expect(page.getByText('Saved result unavailable')).toBeVisible();
173+
await expect(
174+
page.getByText('We could not restore this feed result. Create a new feed link to continue.')
175+
).toBeVisible();
158176
await expect(page.getByRole('button', { name: 'Go to create' })).toBeVisible();
159177
await expect(page.locator('.notice')).toHaveAttribute('data-tone', 'error');
160178
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// This file is auto-generated by @hey-api/openapi-ts
22

33
export { createFeed, getApiMetadata, getFeedStatus, getHealthStatus, getLivenessProbe, getReadinessProbe, listStrategies, type Options, renderFeedByToken } from './sdk.gen';
4-
export type { ClientOptions, CreateFeedData, CreateFeedError, CreateFeedErrors, CreateFeedResponse, CreateFeedResponses, FeedConversionState, FeedCreationResponse, FeedMetadata, FeedRetrySummary, FeedStatusResponse, FeedWarning, GetApiMetadataData, GetApiMetadataResponse, GetApiMetadataResponses, GetFeedStatusData, GetFeedStatusError, GetFeedStatusErrors, GetFeedStatusResponse, GetFeedStatusResponses, GetHealthStatusData, GetHealthStatusError, GetHealthStatusErrors, GetHealthStatusResponse, GetHealthStatusResponses, GetLivenessProbeData, GetLivenessProbeResponse, GetLivenessProbeResponses, GetReadinessProbeData, GetReadinessProbeResponse, GetReadinessProbeResponses, ListStrategiesData, ListStrategiesResponse, ListStrategiesResponses, RenderFeedByTokenData, RenderFeedByTokenError, RenderFeedByTokenErrors, RenderFeedByTokenResponse, RenderFeedByTokenResponses, StructuredError, StructuredErrorResponse } from './types.gen';
4+
export type { ClientOptions, CreateFeedData, CreateFeedError, CreateFeedErrors, CreateFeedResponse, CreateFeedResponses, GetApiMetadataData, GetApiMetadataResponse, GetApiMetadataResponses, GetFeedStatusData, GetFeedStatusResponse, GetFeedStatusResponses, GetHealthStatusData, GetHealthStatusError, GetHealthStatusErrors, GetHealthStatusResponse, GetHealthStatusResponses, GetLivenessProbeData, GetLivenessProbeResponse, GetLivenessProbeResponses, GetReadinessProbeData, GetReadinessProbeResponse, GetReadinessProbeResponses, ListStrategiesData, ListStrategiesResponse, ListStrategiesResponses, RenderFeedByTokenData, RenderFeedByTokenError, RenderFeedByTokenErrors, RenderFeedByTokenResponse, RenderFeedByTokenResponses } from './types.gen';

frontend/src/api/generated/sdk.gen.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import type { Client, Options as Options2, TDataShape } from './client';
44
import { client } from './client.gen';
5-
import type { CreateFeedData, CreateFeedErrors, CreateFeedResponses, GetApiMetadataData, GetApiMetadataResponses, GetFeedStatusData, GetFeedStatusErrors, GetFeedStatusResponses, GetHealthStatusData, GetHealthStatusErrors, GetHealthStatusResponses, GetLivenessProbeData, GetLivenessProbeResponses, GetReadinessProbeData, GetReadinessProbeResponses, ListStrategiesData, ListStrategiesResponses, RenderFeedByTokenData, RenderFeedByTokenErrors, RenderFeedByTokenResponses } from './types.gen';
5+
import type { CreateFeedData, CreateFeedErrors, CreateFeedResponses, GetApiMetadataData, GetApiMetadataResponses, GetFeedStatusData, GetFeedStatusResponses, GetHealthStatusData, GetHealthStatusErrors, GetHealthStatusResponses, GetLivenessProbeData, GetLivenessProbeResponses, GetReadinessProbeData, GetReadinessProbeResponses, ListStrategiesData, ListStrategiesResponses, RenderFeedByTokenData, RenderFeedByTokenErrors, RenderFeedByTokenResponses } from './types.gen';
66

77
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
88
/**
@@ -50,9 +50,9 @@ export const renderFeedByToken = <ThrowOnError extends boolean = false>(options:
5050
/**
5151
* Get feed status
5252
*
53-
* Returns readiness and degradation metadata for a generated feed.
53+
* Get feed status
5454
*/
55-
export const getFeedStatus = <ThrowOnError extends boolean = false>(options: Options<GetFeedStatusData, ThrowOnError>) => (options.client ?? client).get<GetFeedStatusResponses, GetFeedStatusErrors, ThrowOnError>({ url: '/feeds/{token}/status', ...options });
55+
export const getFeedStatus = <ThrowOnError extends boolean = false>(options?: Options<GetFeedStatusData, ThrowOnError>) => (options?.client ?? client).get<GetFeedStatusResponses, unknown, ThrowOnError>({ url: '/feeds/{token}/status', ...options });
5656

5757
/**
5858
* Authenticated health check

frontend/src/api/generated/types.gen.ts

Lines changed: 86 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -4,71 +4,6 @@ export type ClientOptions = {
44
baseUrl: 'https://api.html2rss.dev/api/v1' | 'http://127.0.0.1:4000/api/v1' | (string & {});
55
};
66

7-
export type StructuredErrorResponse = {
8-
error: StructuredError;
9-
success: boolean;
10-
};
11-
12-
export type StructuredError = {
13-
code: string;
14-
kind: 'auth' | 'input' | 'network' | 'server';
15-
message: string;
16-
next_action: 'enter_token' | 'correct_input' | 'retry' | 'wait' | 'none';
17-
next_strategy?: string;
18-
retry_action: 'alternate' | 'primary' | 'none';
19-
retryable: boolean;
20-
};
21-
22-
export type FeedCreationResponse = {
23-
data: {
24-
conversion: FeedConversionState;
25-
feed: FeedMetadata;
26-
};
27-
meta: {
28-
created: boolean;
29-
};
30-
success: boolean;
31-
};
32-
33-
export type FeedConversionState = {
34-
preview_status: 'pending' | 'ready' | 'degraded' | 'unavailable';
35-
readiness_phase: 'link_created' | 'feed_ready' | 'feed_not_ready_yet' | 'preview_unavailable';
36-
retry?: FeedRetrySummary;
37-
warnings: Array<FeedWarning>;
38-
};
39-
40-
export type FeedMetadata = {
41-
created_at: string;
42-
feed_token: string;
43-
id: string;
44-
json_public_url: string;
45-
name: string;
46-
public_url: string;
47-
updated_at: string;
48-
url: string;
49-
};
50-
51-
export type FeedRetrySummary = {
52-
automatic: boolean;
53-
from: string;
54-
to: string;
55-
};
56-
57-
export type FeedStatusResponse = {
58-
data: {
59-
conversion: FeedConversionState;
60-
feed: FeedMetadata;
61-
};
62-
success: boolean;
63-
};
64-
65-
export type FeedWarning = {
66-
code: string;
67-
message: string;
68-
next_action: 'enter_token' | 'correct_input' | 'retry' | 'wait' | 'none';
69-
retryable: boolean;
70-
};
71-
727
export type GetApiMetadataData = {
738
body?: never;
749
path?: never;
@@ -118,22 +53,34 @@ export type CreateFeedData = {
11853
};
11954

12055
export type CreateFeedErrors = {
121-
/**
122-
* returns bad request for invalid input payloads
123-
*/
124-
400: StructuredErrorResponse;
12556
/**
12657
* returns 401 with UNAUTHORIZED error payload
12758
*/
128-
401: StructuredErrorResponse;
59+
401: {
60+
error: {
61+
code: string;
62+
kind: string;
63+
message: string;
64+
next_action: string;
65+
retry_action: string;
66+
retryable: boolean;
67+
};
68+
success: boolean;
69+
};
12970
/**
13071
* returns forbidden for authenticated requests when auto source is disabled
13172
*/
132-
403: StructuredErrorResponse;
133-
/**
134-
* returns error when feed creation fails
135-
*/
136-
500: StructuredErrorResponse;
73+
403: {
74+
error: {
75+
code: string;
76+
kind: string;
77+
message: string;
78+
next_action: string;
79+
retry_action: string;
80+
retryable: boolean;
81+
};
82+
success: boolean;
83+
};
13784
};
13885

13986
export type CreateFeedError = CreateFeedErrors[keyof CreateFeedErrors];
@@ -142,7 +89,29 @@ export type CreateFeedResponses = {
14289
/**
14390
* normalizes hostname-only input to https before feed creation
14491
*/
145-
201: FeedCreationResponse;
92+
201: {
93+
data: {
94+
conversion: {
95+
preview_status: string;
96+
readiness_phase: string;
97+
warnings: Array<unknown>;
98+
};
99+
feed: {
100+
created_at: string;
101+
feed_token: string;
102+
id: string;
103+
json_public_url: string;
104+
name: string;
105+
public_url: string;
106+
updated_at: string;
107+
url: string;
108+
};
109+
};
110+
meta: {
111+
created: boolean;
112+
};
113+
success: boolean;
114+
};
146115
};
147116

148117
export type CreateFeedResponse = CreateFeedResponses[keyof CreateFeedResponses];
@@ -184,35 +153,32 @@ export type RenderFeedByTokenResponse = RenderFeedByTokenResponses[keyof RenderF
184153

185154
export type GetFeedStatusData = {
186155
body?: never;
187-
path: {
188-
token: string;
189-
};
156+
path?: never;
190157
query?: never;
191158
url: '/feeds/{token}/status';
192159
};
193160

194-
export type GetFeedStatusErrors = {
195-
/**
196-
* returns unauthorized for invalid tokens
197-
*/
198-
401: StructuredErrorResponse;
199-
/**
200-
* returns forbidden when auto source is disabled
201-
*/
202-
403: StructuredErrorResponse;
203-
/**
204-
* returns non-cacheable status errors when feed resolution fails
205-
*/
206-
500: StructuredErrorResponse;
207-
};
208-
209-
export type GetFeedStatusError = GetFeedStatusErrors[keyof GetFeedStatusErrors];
210-
211161
export type GetFeedStatusResponses = {
212162
/**
213-
* returns structured feed status and preview metadata
163+
* Returns readiness and degradation metadata for a generated feed.
214164
*/
215-
200: FeedStatusResponse;
165+
200: {
166+
data: {
167+
conversion: {
168+
preview_status: string;
169+
readiness_phase: string;
170+
warnings: Array<unknown>;
171+
};
172+
feed: {
173+
feed_token: string;
174+
json_public_url: string;
175+
name: string;
176+
public_url: string;
177+
url: string;
178+
};
179+
};
180+
success: boolean;
181+
};
216182
};
217183

218184
export type GetFeedStatusResponse = GetFeedStatusResponses[keyof GetFeedStatusResponses];
@@ -231,11 +197,31 @@ export type GetHealthStatusErrors = {
231197
/**
232198
* returns 401 with UNAUTHORIZED error payload
233199
*/
234-
401: StructuredErrorResponse;
200+
401: {
201+
error: {
202+
code: string;
203+
kind: string;
204+
message: string;
205+
next_action: string;
206+
retry_action: string;
207+
retryable: boolean;
208+
};
209+
success: boolean;
210+
};
235211
/**
236212
* returns error when configuration fails
237213
*/
238-
500: StructuredErrorResponse;
214+
500: {
215+
error: {
216+
code: string;
217+
kind: string;
218+
message: string;
219+
next_action: string;
220+
retry_action: string;
221+
retryable: boolean;
222+
};
223+
success: boolean;
224+
};
239225
};
240226

241227
export type GetHealthStatusError = GetHealthStatusErrors[keyof GetHealthStatusErrors];

0 commit comments

Comments
 (0)