@jackwener/opencli 1.7.4 → 1.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +76 -51
  2. package/README.zh-CN.md +78 -62
  3. package/cli-manifest.json +4558 -2979
  4. package/clis/antigravity/serve.js +71 -25
  5. package/clis/baidu-scholar/search.js +87 -0
  6. package/clis/baidu-scholar/search.test.js +23 -0
  7. package/clis/bilibili/video.js +61 -0
  8. package/clis/bilibili/video.test.js +81 -0
  9. package/clis/deepseek/ask.js +94 -0
  10. package/clis/deepseek/ask.test.js +73 -0
  11. package/clis/deepseek/history.js +25 -0
  12. package/clis/deepseek/new.js +20 -0
  13. package/clis/deepseek/read.js +22 -0
  14. package/clis/deepseek/status.js +24 -0
  15. package/clis/deepseek/utils.js +291 -0
  16. package/clis/deepseek/utils.test.js +37 -0
  17. package/clis/eastmoney/_secid.js +78 -0
  18. package/clis/eastmoney/announcement.js +52 -0
  19. package/clis/eastmoney/convertible.js +73 -0
  20. package/clis/eastmoney/etf.js +65 -0
  21. package/clis/eastmoney/holders.js +78 -0
  22. package/clis/eastmoney/index-board.js +96 -0
  23. package/clis/eastmoney/kline.js +87 -0
  24. package/clis/eastmoney/kuaixun.js +54 -0
  25. package/clis/eastmoney/longhu.js +67 -0
  26. package/clis/eastmoney/money-flow.js +78 -0
  27. package/clis/eastmoney/northbound.js +57 -0
  28. package/clis/eastmoney/quote.js +107 -0
  29. package/clis/eastmoney/rank.js +94 -0
  30. package/clis/eastmoney/sectors.js +76 -0
  31. package/clis/google-scholar/search.js +58 -0
  32. package/clis/google-scholar/search.test.js +23 -0
  33. package/clis/gov-law/commands.test.js +39 -0
  34. package/clis/gov-law/recent.js +22 -0
  35. package/clis/gov-law/search.js +41 -0
  36. package/clis/gov-law/shared.js +51 -0
  37. package/clis/gov-policy/commands.test.js +27 -0
  38. package/clis/gov-policy/recent.js +47 -0
  39. package/clis/gov-policy/search.js +48 -0
  40. package/clis/jianyu/search.js +139 -3
  41. package/clis/jianyu/search.test.js +25 -0
  42. package/clis/jianyu/shared/procurement-detail.js +15 -0
  43. package/clis/jianyu/shared/procurement-detail.test.js +12 -0
  44. package/clis/nowcoder/companies.js +23 -0
  45. package/clis/nowcoder/creators.js +27 -0
  46. package/clis/nowcoder/detail.js +61 -0
  47. package/clis/nowcoder/experience.js +36 -0
  48. package/clis/nowcoder/hot.js +24 -0
  49. package/clis/nowcoder/jobs.js +21 -0
  50. package/clis/nowcoder/notifications.js +29 -0
  51. package/clis/nowcoder/papers.js +40 -0
  52. package/clis/nowcoder/practice.js +37 -0
  53. package/clis/nowcoder/recommend.js +30 -0
  54. package/clis/nowcoder/referral.js +39 -0
  55. package/clis/nowcoder/salary.js +40 -0
  56. package/clis/nowcoder/search.js +49 -0
  57. package/clis/nowcoder/suggest.js +33 -0
  58. package/clis/nowcoder/topics.js +27 -0
  59. package/clis/nowcoder/trending.js +25 -0
  60. package/clis/twitter/list-add.js +337 -0
  61. package/clis/twitter/list-add.test.js +15 -0
  62. package/clis/twitter/list-remove.js +297 -0
  63. package/clis/twitter/list-remove.test.js +14 -0
  64. package/clis/twitter/list-tweets.js +185 -0
  65. package/clis/twitter/list-tweets.test.js +108 -0
  66. package/clis/twitter/lists.js +134 -47
  67. package/clis/twitter/lists.test.js +105 -38
  68. package/clis/twitter/shared.js +7 -2
  69. package/clis/twitter/tweets.js +218 -0
  70. package/clis/twitter/tweets.test.js +125 -0
  71. package/clis/wanfang/search.js +66 -0
  72. package/clis/wanfang/search.test.js +23 -0
  73. package/clis/web/read.js +1 -1
  74. package/clis/weixin/download.js +3 -2
  75. package/clis/xiaohongshu/publish.js +149 -28
  76. package/clis/xiaohongshu/publish.test.js +319 -6
  77. package/clis/xiaoyuzhou/download.js +8 -4
  78. package/clis/xiaoyuzhou/download.test.js +23 -13
  79. package/clis/xiaoyuzhou/episode.js +9 -4
  80. package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
  81. package/clis/xiaoyuzhou/podcast.js +9 -4
  82. package/clis/xiaoyuzhou/utils.js +0 -40
  83. package/clis/xiaoyuzhou/utils.test.js +15 -75
  84. package/clis/youtube/channel.js +35 -0
  85. package/clis/zsxq/dynamics.js +1 -1
  86. package/clis/zsxq/utils.js +6 -3
  87. package/clis/zsxq/utils.test.js +31 -0
  88. package/dist/src/browser/base-page.d.ts +14 -4
  89. package/dist/src/browser/base-page.js +35 -25
  90. package/dist/src/browser/bridge.d.ts +1 -0
  91. package/dist/src/browser/bridge.js +1 -1
  92. package/dist/src/browser/cdp.d.ts +1 -0
  93. package/dist/src/browser/cdp.js +13 -4
  94. package/dist/src/browser/compound.d.ts +59 -0
  95. package/dist/src/browser/compound.js +112 -0
  96. package/dist/src/browser/compound.test.js +175 -0
  97. package/dist/src/browser/daemon-client.d.ts +6 -4
  98. package/dist/src/browser/daemon-client.js +6 -1
  99. package/dist/src/browser/daemon-client.test.js +40 -1
  100. package/dist/src/browser/dom-snapshot.d.ts +7 -0
  101. package/dist/src/browser/dom-snapshot.js +83 -5
  102. package/dist/src/browser/dom-snapshot.test.js +65 -0
  103. package/dist/src/browser/extract.d.ts +69 -0
  104. package/dist/src/browser/extract.js +132 -0
  105. package/dist/src/browser/extract.test.js +129 -0
  106. package/dist/src/browser/find.d.ts +76 -0
  107. package/dist/src/browser/find.js +179 -0
  108. package/dist/src/browser/find.test.js +120 -0
  109. package/dist/src/browser/html-tree.d.ts +75 -0
  110. package/dist/src/browser/html-tree.js +112 -0
  111. package/dist/src/browser/html-tree.test.d.ts +1 -0
  112. package/dist/src/browser/html-tree.test.js +181 -0
  113. package/dist/src/browser/network-cache.d.ts +48 -0
  114. package/dist/src/browser/network-cache.js +66 -0
  115. package/dist/src/browser/network-cache.test.d.ts +1 -0
  116. package/dist/src/browser/network-cache.test.js +58 -0
  117. package/dist/src/browser/network-key.d.ts +22 -0
  118. package/dist/src/browser/network-key.js +66 -0
  119. package/dist/src/browser/network-key.test.d.ts +1 -0
  120. package/dist/src/browser/network-key.test.js +49 -0
  121. package/dist/src/browser/page.d.ts +14 -4
  122. package/dist/src/browser/page.js +48 -7
  123. package/dist/src/browser/page.test.js +97 -0
  124. package/dist/src/browser/shape-filter.d.ts +52 -0
  125. package/dist/src/browser/shape-filter.js +101 -0
  126. package/dist/src/browser/shape-filter.test.d.ts +1 -0
  127. package/dist/src/browser/shape-filter.test.js +101 -0
  128. package/dist/src/browser/shape.d.ts +23 -0
  129. package/dist/src/browser/shape.js +95 -0
  130. package/dist/src/browser/shape.test.d.ts +1 -0
  131. package/dist/src/browser/shape.test.js +82 -0
  132. package/dist/src/browser/target-errors.d.ts +14 -1
  133. package/dist/src/browser/target-errors.js +13 -0
  134. package/dist/src/browser/target-errors.test.js +39 -6
  135. package/dist/src/browser/target-resolver.d.ts +57 -10
  136. package/dist/src/browser/target-resolver.js +195 -75
  137. package/dist/src/browser/target-resolver.test.js +80 -5
  138. package/dist/src/cli.js +849 -267
  139. package/dist/src/cli.test.js +961 -90
  140. package/dist/src/commanderAdapter.d.ts +0 -1
  141. package/dist/src/commanderAdapter.js +2 -16
  142. package/dist/src/commanderAdapter.test.js +1 -1
  143. package/dist/src/completion-shared.js +2 -5
  144. package/dist/src/daemon.js +8 -0
  145. package/dist/src/download/article-download.d.ts +1 -0
  146. package/dist/src/download/article-download.js +3 -0
  147. package/dist/src/download/article-download.test.d.ts +1 -0
  148. package/dist/src/download/article-download.test.js +39 -0
  149. package/dist/src/execution.js +7 -2
  150. package/dist/src/execution.test.js +54 -0
  151. package/dist/src/main.js +16 -0
  152. package/dist/src/plugin.d.ts +1 -8
  153. package/dist/src/plugin.js +1 -27
  154. package/dist/src/plugin.test.js +1 -59
  155. package/dist/src/registry.d.ts +1 -0
  156. package/dist/src/registry.js +3 -2
  157. package/dist/src/registry.test.js +22 -0
  158. package/dist/src/types.d.ts +32 -8
  159. package/package.json +1 -1
  160. package/clis/twitter/lists-parser.js +0 -77
  161. package/clis/twitter/lists.d.ts +0 -5
  162. package/dist/src/cascade.d.ts +0 -46
  163. package/dist/src/cascade.js +0 -135
  164. package/dist/src/explore.d.ts +0 -99
  165. package/dist/src/explore.js +0 -402
  166. package/dist/src/generate-verified.d.ts +0 -105
  167. package/dist/src/generate-verified.js +0 -696
  168. package/dist/src/generate-verified.test.js +0 -925
  169. package/dist/src/generate.d.ts +0 -46
  170. package/dist/src/generate.js +0 -117
  171. package/dist/src/record.d.ts +0 -96
  172. package/dist/src/record.js +0 -657
  173. package/dist/src/record.test.js +0 -293
  174. package/dist/src/skill-generate.d.ts +0 -30
  175. package/dist/src/skill-generate.js +0 -75
  176. package/dist/src/skill-generate.test.js +0 -173
  177. package/dist/src/synthesize.d.ts +0 -97
  178. package/dist/src/synthesize.js +0 -208
  179. /package/dist/src/{generate-verified.test.d.ts → browser/compound.test.d.ts} +0 -0
  180. /package/dist/src/{record.test.d.ts → browser/extract.test.d.ts} +0 -0
  181. /package/dist/src/{skill-generate.test.d.ts → browser/find.test.d.ts} +0 -0
@@ -1,925 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import * as fs from 'node:fs';
3
- import * as os from 'node:os';
4
- import * as path from 'node:path';
5
- const { mockExploreUrl, mockLoadExploreBundle, mockSynthesizeFromExplore, mockBrowserSession, mockCascadeProbe, mockExecutePipeline, mockRegisterCommand, } = vi.hoisted(() => ({
6
- mockExploreUrl: vi.fn(),
7
- mockLoadExploreBundle: vi.fn(),
8
- mockSynthesizeFromExplore: vi.fn(),
9
- mockBrowserSession: vi.fn(),
10
- mockCascadeProbe: vi.fn(),
11
- mockExecutePipeline: vi.fn(),
12
- mockRegisterCommand: vi.fn(),
13
- }));
14
- vi.mock('./explore.js', () => ({
15
- exploreUrl: mockExploreUrl,
16
- }));
17
- vi.mock('./synthesize.js', () => ({
18
- loadExploreBundle: mockLoadExploreBundle,
19
- synthesizeFromExplore: mockSynthesizeFromExplore,
20
- }));
21
- vi.mock('./runtime.js', () => ({
22
- browserSession: mockBrowserSession,
23
- }));
24
- vi.mock('./cascade.js', () => ({
25
- cascadeProbe: mockCascadeProbe,
26
- }));
27
- vi.mock('./pipeline/index.js', () => ({
28
- executePipeline: mockExecutePipeline,
29
- }));
30
- vi.mock('./registry.js', async () => {
31
- const actual = await vi.importActual('./registry.js');
32
- return {
33
- ...actual,
34
- registerCommand: mockRegisterCommand,
35
- };
36
- });
37
- vi.mock('./discovery.js', () => ({
38
- USER_CLIS_DIR: '/tmp/opencli-user-clis',
39
- }));
40
- import { Strategy } from './registry.js';
41
- import { generateVerifiedFromUrl } from './generate-verified.js';
42
- describe('generateVerifiedFromUrl', () => {
43
- let tempDir;
44
- beforeEach(() => {
45
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-generate-verified-'));
46
- mockExploreUrl.mockReset();
47
- mockLoadExploreBundle.mockReset();
48
- mockSynthesizeFromExplore.mockReset();
49
- mockBrowserSession.mockReset();
50
- mockCascadeProbe.mockReset();
51
- mockExecutePipeline.mockReset();
52
- mockRegisterCommand.mockReset();
53
- });
54
- afterEach(() => {
55
- fs.rmSync(tempDir, { recursive: true, force: true });
56
- });
57
- // ── Blocked outcomes ──────────────────────────────────────────────────────
58
- it('returns blocked with no-viable-api-surface when discover finds no API endpoints', async () => {
59
- mockExploreUrl.mockResolvedValue({
60
- site: 'demo',
61
- target_url: 'https://demo.test',
62
- final_url: 'https://demo.test',
63
- title: 'Demo',
64
- framework: {},
65
- stores: [],
66
- top_strategy: 'public',
67
- endpoint_count: 1,
68
- api_endpoint_count: 0,
69
- capabilities: [],
70
- auth_indicators: [],
71
- out_dir: tempDir,
72
- });
73
- mockLoadExploreBundle.mockReturnValue({
74
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test' },
75
- endpoints: [],
76
- capabilities: [],
77
- });
78
- mockSynthesizeFromExplore.mockReturnValue({
79
- site: 'demo',
80
- explore_dir: tempDir,
81
- out_dir: tempDir,
82
- candidate_count: 0,
83
- candidates: [],
84
- });
85
- const result = await generateVerifiedFromUrl({
86
- url: 'https://demo.test',
87
- BrowserFactory: class {
88
- },
89
- noRegister: true,
90
- });
91
- expect(result.status).toBe('blocked');
92
- expect(result.reason).toBe('no-viable-api-surface');
93
- expect(result.stage).toBe('explore');
94
- expect(result.confidence).toBe('high');
95
- expect(result.message).toBeDefined();
96
- expect(result.stats.api_endpoint_count).toBe(0);
97
- expect(mockBrowserSession).not.toHaveBeenCalled();
98
- });
99
- it('returns blocked with auth-too-complex when no PUBLIC/COOKIE probe succeeds', async () => {
100
- const candidatePath = path.join(tempDir, 'hot.json');
101
- fs.writeFileSync(candidatePath, JSON.stringify({
102
- site: 'demo',
103
- name: 'hot',
104
- description: 'demo hot',
105
- domain: 'demo.test',
106
- strategy: 'public',
107
- browser: false,
108
- args: {},
109
- columns: ['title', 'url'],
110
- pipeline: [
111
- { fetch: { url: 'https://demo.test/api/hot' } },
112
- { select: 'data.items' },
113
- ],
114
- }, null, 2));
115
- mockExploreUrl.mockResolvedValue({
116
- site: 'demo',
117
- target_url: 'https://demo.test',
118
- final_url: 'https://demo.test',
119
- title: 'Demo',
120
- framework: {},
121
- stores: [],
122
- top_strategy: 'cookie',
123
- endpoint_count: 1,
124
- api_endpoint_count: 1,
125
- capabilities: [{ name: 'hot' }],
126
- auth_indicators: [],
127
- out_dir: tempDir,
128
- });
129
- mockLoadExploreBundle.mockReturnValue({
130
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test' },
131
- endpoints: [{
132
- pattern: 'demo.test/api/hot',
133
- url: 'https://demo.test/api/hot',
134
- itemPath: 'data.items',
135
- itemCount: 5,
136
- detectedFields: { title: 'title', url: 'url' },
137
- }],
138
- capabilities: [{ name: 'hot', strategy: 'cookie', endpoint: 'demo.test/api/hot', itemPath: 'data.items' }],
139
- });
140
- mockSynthesizeFromExplore.mockReturnValue({
141
- site: 'demo',
142
- explore_dir: tempDir,
143
- out_dir: tempDir,
144
- candidate_count: 1,
145
- candidates: [{ name: 'hot', path: candidatePath, strategy: 'public' }],
146
- });
147
- const page = { goto: vi.fn() };
148
- mockBrowserSession.mockImplementation(async (_factory, fn) => fn(page));
149
- mockCascadeProbe.mockResolvedValue({
150
- bestStrategy: Strategy.COOKIE,
151
- probes: [
152
- { strategy: Strategy.PUBLIC, success: false },
153
- { strategy: Strategy.COOKIE, success: false },
154
- ],
155
- confidence: 0.3,
156
- });
157
- const result = await generateVerifiedFromUrl({
158
- url: 'https://demo.test',
159
- BrowserFactory: class {
160
- },
161
- noRegister: true,
162
- });
163
- expect(mockExecutePipeline).not.toHaveBeenCalled();
164
- expect(result.status).toBe('blocked');
165
- expect(result.reason).toBe('auth-too-complex');
166
- expect(result.stage).toBe('cascade');
167
- expect(result.confidence).toBe('high');
168
- });
169
- // ── Success outcomes ──────────────────────────────────────────────────────
170
- it('verifies the selected candidate in a single session and registers on success with sidecar metadata', async () => {
171
- const hotPath = path.join(tempDir, 'hot.json');
172
- const searchPath = path.join(tempDir, 'search.json');
173
- fs.writeFileSync(hotPath, JSON.stringify({
174
- site: 'demo',
175
- name: 'hot',
176
- description: 'demo hot',
177
- domain: 'demo.test',
178
- strategy: 'public',
179
- browser: false,
180
- args: {
181
- limit: { type: 'int', default: 20 },
182
- },
183
- columns: ['title', 'url'],
184
- pipeline: [
185
- { fetch: { url: 'https://demo.test/api/hot?limit=${{ args.limit | default(20) }}' } },
186
- { select: 'data.items' },
187
- { map: { rank: '${{ index + 1 }}', title: '${{ item.title }}', url: '${{ item.url }}' } },
188
- { limit: '${{ args.limit | default(20) }}' },
189
- ],
190
- }, null, 2));
191
- fs.writeFileSync(searchPath, JSON.stringify({
192
- site: 'demo',
193
- name: 'search',
194
- description: 'demo search',
195
- domain: 'demo.test',
196
- strategy: 'public',
197
- browser: false,
198
- args: {
199
- keyword: { type: 'str', required: true },
200
- },
201
- columns: ['title', 'url'],
202
- pipeline: [
203
- { fetch: { url: 'https://demo.test/api/search?q=${{ args.keyword }}' } },
204
- { select: 'payload.items' },
205
- { map: { title: '${{ item.title }}', url: '${{ item.url }}' } },
206
- ],
207
- }, null, 2));
208
- mockExploreUrl.mockResolvedValue({
209
- site: 'demo',
210
- target_url: 'https://demo.test',
211
- final_url: 'https://demo.test/home',
212
- title: 'Demo',
213
- framework: {},
214
- stores: [],
215
- top_strategy: 'cookie',
216
- endpoint_count: 2,
217
- api_endpoint_count: 2,
218
- capabilities: [{ name: 'hot' }, { name: 'search' }],
219
- auth_indicators: [],
220
- out_dir: tempDir,
221
- });
222
- mockLoadExploreBundle.mockReturnValue({
223
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test/home' },
224
- endpoints: [
225
- {
226
- pattern: 'demo.test/api/hot',
227
- url: 'https://demo.test/api/hot?limit=20',
228
- itemPath: 'data.items',
229
- itemCount: 5,
230
- detectedFields: { title: 'title', url: 'url' },
231
- },
232
- {
233
- pattern: 'demo.test/api/search',
234
- url: 'https://demo.test/api/search?q=test',
235
- itemPath: 'payload.items',
236
- itemCount: 10,
237
- detectedFields: { title: 'headline', url: 'permalink' },
238
- },
239
- ],
240
- capabilities: [
241
- { name: 'hot', strategy: 'public', endpoint: 'demo.test/api/hot', itemPath: 'data.items' },
242
- { name: 'search', strategy: 'cookie', endpoint: 'demo.test/api/search', itemPath: 'payload.items' },
243
- ],
244
- });
245
- mockSynthesizeFromExplore.mockReturnValue({
246
- site: 'demo',
247
- explore_dir: tempDir,
248
- out_dir: tempDir,
249
- candidate_count: 2,
250
- candidates: [
251
- { name: 'hot', path: hotPath, strategy: 'public' },
252
- { name: 'search', path: searchPath, strategy: 'public' },
253
- ],
254
- });
255
- const page = { goto: vi.fn() };
256
- mockBrowserSession.mockImplementation(async (_factory, fn) => fn(page));
257
- mockCascadeProbe.mockResolvedValue({
258
- bestStrategy: Strategy.COOKIE,
259
- probes: [
260
- { strategy: Strategy.PUBLIC, success: false },
261
- { strategy: Strategy.COOKIE, success: true },
262
- ],
263
- confidence: 0.9,
264
- });
265
- mockExecutePipeline.mockResolvedValue([{ title: 'hello', url: 'https://demo.test/item/1' }]);
266
- const result = await generateVerifiedFromUrl({
267
- url: 'https://demo.test',
268
- BrowserFactory: class {
269
- },
270
- goal: 'search',
271
- noRegister: false,
272
- });
273
- expect(mockBrowserSession).toHaveBeenCalledTimes(1);
274
- expect(page.goto).toHaveBeenCalledWith('https://demo.test/home');
275
- expect(mockCascadeProbe).toHaveBeenCalledWith(page, 'https://demo.test/api/search?q=test', { maxStrategy: Strategy.COOKIE });
276
- expect(mockExecutePipeline).toHaveBeenCalledTimes(1);
277
- expect(mockRegisterCommand).toHaveBeenCalledTimes(1);
278
- expect(result.status).toBe('success');
279
- expect(result.adapter).toBeDefined();
280
- expect(result.adapter.command).toBe('demo/search');
281
- expect(result.adapter.strategy).toBe(Strategy.COOKIE);
282
- expect(result.adapter.metadata_path).toBeDefined();
283
- expect(result.adapter.reusability).toBe('verified-artifact');
284
- expect(result.reusability).toBe('verified-artifact');
285
- expect(result.stats.verified).toBe(true);
286
- expect(result.stats.repair_attempted).toBe(false);
287
- // Verify sidecar metadata was written
288
- expect(result.adapter.metadata_path).toMatch(/\.meta\.json$/);
289
- const metaContent = JSON.parse(fs.readFileSync(result.adapter.metadata_path, 'utf-8'));
290
- expect(metaContent).toEqual(expect.objectContaining({
291
- artifact_kind: 'verified',
292
- schema_version: 1,
293
- source_url: 'https://demo.test',
294
- strategy: Strategy.COOKIE,
295
- verified: true,
296
- reusable: true,
297
- reusability_reason: 'verified-artifact',
298
- }));
299
- });
300
- it('writes verified artifact + sidecar metadata for --no-register success', async () => {
301
- const candidatePath = path.join(tempDir, 'search.json');
302
- fs.writeFileSync(candidatePath, JSON.stringify({
303
- site: 'demo',
304
- name: 'search',
305
- description: 'demo search',
306
- domain: 'demo.test',
307
- strategy: 'public',
308
- browser: false,
309
- args: {
310
- keyword: { type: 'str', required: true },
311
- },
312
- columns: ['title', 'url'],
313
- pipeline: [
314
- { fetch: { url: 'https://demo.test/api/search?q=${{ args.keyword }}' } },
315
- { select: 'payload.items' },
316
- { map: { title: '${{ item.title }}', url: '${{ item.url }}' } },
317
- ],
318
- }, null, 2));
319
- mockExploreUrl.mockResolvedValue({
320
- site: 'demo',
321
- target_url: 'https://demo.test',
322
- final_url: 'https://demo.test/home',
323
- title: 'Demo',
324
- framework: {},
325
- stores: [],
326
- top_strategy: 'cookie',
327
- endpoint_count: 1,
328
- api_endpoint_count: 1,
329
- capabilities: [{ name: 'search' }],
330
- auth_indicators: [],
331
- out_dir: tempDir,
332
- });
333
- mockLoadExploreBundle.mockReturnValue({
334
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test/home' },
335
- endpoints: [{
336
- pattern: 'demo.test/api/search',
337
- url: 'https://demo.test/api/search?q=test',
338
- itemPath: 'payload.items',
339
- itemCount: 10,
340
- detectedFields: { title: 'headline', url: 'permalink' },
341
- }],
342
- capabilities: [{ name: 'search', strategy: 'cookie', endpoint: 'demo.test/api/search', itemPath: 'payload.items' }],
343
- });
344
- mockSynthesizeFromExplore.mockReturnValue({
345
- site: 'demo',
346
- explore_dir: tempDir,
347
- out_dir: tempDir,
348
- candidate_count: 1,
349
- candidates: [{ name: 'search', path: candidatePath, strategy: 'public' }],
350
- });
351
- const page = { goto: vi.fn() };
352
- mockBrowserSession.mockImplementation(async (_factory, fn) => fn(page));
353
- mockCascadeProbe.mockResolvedValue({
354
- bestStrategy: Strategy.COOKIE,
355
- probes: [
356
- { strategy: Strategy.PUBLIC, success: false },
357
- { strategy: Strategy.COOKIE, success: true },
358
- ],
359
- confidence: 0.9,
360
- });
361
- mockExecutePipeline.mockResolvedValue([{ title: 'hello', url: 'https://demo.test/item/1' }]);
362
- const result = await generateVerifiedFromUrl({
363
- url: 'https://demo.test',
364
- BrowserFactory: class {
365
- },
366
- goal: 'search',
367
- noRegister: true,
368
- });
369
- expect(result.status).toBe('success');
370
- expect(path.normalize(result.adapter.path).endsWith(path.join('verified', 'search.verified.js'))).toBe(true);
371
- expect(result.adapter?.path).not.toBe(candidatePath);
372
- expect(path.normalize(result.adapter.metadata_path).endsWith(path.join('verified', 'search.verified.meta.json'))).toBe(true);
373
- expect(fs.existsSync(result.adapter.path)).toBe(true);
374
- expect(fs.existsSync(result.adapter.metadata_path)).toBe(true);
375
- expect(mockRegisterCommand).not.toHaveBeenCalled();
376
- });
377
- // ── needs-human-check outcomes ────────────────────────────────────────────
378
- it('returns needs-human-check with structured escalation when repair exhausted', async () => {
379
- const candidatePath = path.join(tempDir, 'hot.json');
380
- fs.writeFileSync(candidatePath, JSON.stringify({
381
- site: 'demo',
382
- name: 'hot',
383
- description: 'demo hot',
384
- domain: 'demo.test',
385
- strategy: 'public',
386
- browser: false,
387
- args: {
388
- limit: { type: 'int', default: 20 },
389
- },
390
- columns: ['title', 'url'],
391
- pipeline: [
392
- { fetch: { url: 'https://demo.test/api/hot?limit=${{ args.limit | default(20) }}' } },
393
- { select: 'wrong.items' },
394
- { map: { rank: '${{ index + 1 }}', title: '${{ item.title }}', url: '${{ item.url }}' } },
395
- { limit: '${{ args.limit | default(20) }}' },
396
- ],
397
- }, null, 2));
398
- mockExploreUrl.mockResolvedValue({
399
- site: 'demo',
400
- target_url: 'https://demo.test',
401
- final_url: 'https://demo.test',
402
- title: 'Demo',
403
- framework: {},
404
- stores: [],
405
- top_strategy: 'public',
406
- endpoint_count: 1,
407
- api_endpoint_count: 1,
408
- capabilities: [{ name: 'hot' }],
409
- auth_indicators: [],
410
- out_dir: tempDir,
411
- });
412
- mockLoadExploreBundle.mockReturnValue({
413
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test' },
414
- endpoints: [{
415
- pattern: 'demo.test/api/hot',
416
- url: 'https://demo.test/api/hot?limit=20',
417
- itemPath: 'data.items',
418
- itemCount: 5,
419
- detectedFields: { title: 'title', url: 'url' },
420
- }],
421
- capabilities: [{ name: 'hot', strategy: 'public', endpoint: 'demo.test/api/hot', itemPath: 'data.items' }],
422
- });
423
- mockSynthesizeFromExplore.mockReturnValue({
424
- site: 'demo',
425
- explore_dir: tempDir,
426
- out_dir: tempDir,
427
- candidate_count: 1,
428
- candidates: [{ name: 'hot', path: candidatePath, strategy: 'public' }],
429
- });
430
- const page = { goto: vi.fn() };
431
- mockBrowserSession.mockImplementation(async (_factory, fn) => fn(page));
432
- mockCascadeProbe.mockResolvedValue({
433
- bestStrategy: Strategy.PUBLIC,
434
- probes: [{ strategy: Strategy.PUBLIC, success: true }],
435
- confidence: 1,
436
- });
437
- mockExecutePipeline.mockResolvedValueOnce([]).mockResolvedValueOnce([]);
438
- const result = await generateVerifiedFromUrl({
439
- url: 'https://demo.test',
440
- BrowserFactory: class {
441
- },
442
- noRegister: true,
443
- });
444
- expect(mockExecutePipeline).toHaveBeenCalledTimes(2);
445
- expect(mockExecutePipeline.mock.calls[0]?.[1]).toEqual(expect.arrayContaining([{ select: 'wrong.items' }]));
446
- expect(mockExecutePipeline.mock.calls[1]?.[1]).toEqual(expect.arrayContaining([{ select: 'data.items' }]));
447
- // Verify structured escalation contract
448
- expect(result.status).toBe('needs-human-check');
449
- expect(result.escalation).toBeDefined();
450
- expect(result.escalation.stage).toBe('fallback');
451
- expect(result.escalation.reason).toBe('empty-result');
452
- expect(result.escalation.confidence).toBe('low');
453
- expect(result.escalation.suggested_action).toBe('inspect-with-browser');
454
- expect(result.escalation.candidate).toBeDefined();
455
- expect(result.escalation.candidate.command).toBe('demo/hot');
456
- expect(result.escalation.candidate.reusability).toBe('unverified-candidate');
457
- expect(result.reusability).toBe('unverified-candidate');
458
- expect(result.message).toContain('Repair exhausted');
459
- expect(result.stats.repair_attempted).toBe(true);
460
- expect(result.stats.verified).toBe(false);
461
- });
462
- it('returns needs-human-check with ask-for-sample-arg for unsupported required args', async () => {
463
- const candidatePath = path.join(tempDir, 'detail.json');
464
- fs.writeFileSync(candidatePath, JSON.stringify({
465
- site: 'demo',
466
- name: 'detail',
467
- description: 'demo detail',
468
- domain: 'demo.test',
469
- strategy: 'public',
470
- browser: false,
471
- args: {
472
- id: { type: 'str', required: true },
473
- },
474
- columns: ['title', 'url'],
475
- pipeline: [
476
- { fetch: { url: 'https://demo.test/api/detail?id=${{ args.id }}' } },
477
- { select: 'data.item' },
478
- ],
479
- }, null, 2));
480
- mockExploreUrl.mockResolvedValue({
481
- site: 'demo',
482
- target_url: 'https://demo.test/detail/123',
483
- final_url: 'https://demo.test/detail/123',
484
- title: 'Demo detail',
485
- framework: {},
486
- stores: [],
487
- top_strategy: 'public',
488
- endpoint_count: 1,
489
- api_endpoint_count: 1,
490
- capabilities: [{ name: 'detail' }],
491
- auth_indicators: [],
492
- out_dir: tempDir,
493
- });
494
- mockLoadExploreBundle.mockReturnValue({
495
- manifest: { site: 'demo', target_url: 'https://demo.test/detail/123', final_url: 'https://demo.test/detail/123' },
496
- endpoints: [{
497
- pattern: 'demo.test/api/detail',
498
- url: 'https://demo.test/api/detail?id=123',
499
- itemPath: 'data.item',
500
- itemCount: 1,
501
- detectedFields: { title: 'title', url: 'url' },
502
- }],
503
- capabilities: [{ name: 'detail', strategy: 'public', endpoint: 'demo.test/api/detail', itemPath: 'data.item' }],
504
- });
505
- mockSynthesizeFromExplore.mockReturnValue({
506
- site: 'demo',
507
- explore_dir: tempDir,
508
- out_dir: tempDir,
509
- candidate_count: 1,
510
- candidates: [{ name: 'detail', path: candidatePath, strategy: 'public' }],
511
- });
512
- const result = await generateVerifiedFromUrl({
513
- url: 'https://demo.test/detail/123',
514
- BrowserFactory: class {
515
- },
516
- goal: 'detail',
517
- noRegister: true,
518
- });
519
- expect(mockBrowserSession).not.toHaveBeenCalled();
520
- expect(result.status).toBe('needs-human-check');
521
- expect(result.escalation).toBeDefined();
522
- expect(result.escalation.stage).toBe('synthesize');
523
- expect(result.escalation.reason).toBe('unsupported-required-args');
524
- expect(result.escalation.confidence).toBe('high');
525
- expect(result.escalation.suggested_action).toBe('ask-for-sample-arg');
526
- expect(result.escalation.candidate.reusability).toBe('unverified-candidate');
527
- expect(result.reusability).toBe('unverified-candidate');
528
- expect(result.message).toContain('required args: id');
529
- });
530
- // ── Contract shape validation ─────────────────────────────────────────────
531
- it('all outcome statuses include status and stats', async () => {
532
- // Test the blocked path - simplest to set up
533
- mockExploreUrl.mockResolvedValue({
534
- site: 'demo',
535
- target_url: 'https://demo.test',
536
- final_url: 'https://demo.test',
537
- title: 'Demo',
538
- framework: {},
539
- stores: [],
540
- top_strategy: 'public',
541
- endpoint_count: 0,
542
- api_endpoint_count: 0,
543
- capabilities: [],
544
- auth_indicators: [],
545
- out_dir: tempDir,
546
- });
547
- mockLoadExploreBundle.mockReturnValue({
548
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test' },
549
- endpoints: [],
550
- capabilities: [],
551
- });
552
- mockSynthesizeFromExplore.mockReturnValue({
553
- site: 'demo',
554
- explore_dir: tempDir,
555
- out_dir: tempDir,
556
- candidate_count: 0,
557
- candidates: [],
558
- });
559
- const result = await generateVerifiedFromUrl({
560
- url: 'https://demo.test',
561
- BrowserFactory: class {
562
- },
563
- });
564
- // Every outcome must have these three fields
565
- expect(result).toHaveProperty('status');
566
- expect(result).toHaveProperty('stats');
567
- expect(['success', 'blocked', 'needs-human-check']).toContain(result.status);
568
- // Blocked must have stage + reason + confidence
569
- if (result.status === 'blocked') {
570
- expect(result).toHaveProperty('reason');
571
- expect(result).toHaveProperty('stage');
572
- expect(result).toHaveProperty('confidence');
573
- }
574
- });
575
- // ── P2: EarlyHint callback ────────────────────────────────────────────────
576
- it('emits explore stop hint when no API surface found', async () => {
577
- mockExploreUrl.mockResolvedValue({
578
- site: 'demo',
579
- target_url: 'https://demo.test',
580
- final_url: 'https://demo.test',
581
- title: 'Demo',
582
- framework: {},
583
- stores: [],
584
- top_strategy: 'public',
585
- endpoint_count: 1,
586
- api_endpoint_count: 0,
587
- capabilities: [],
588
- auth_indicators: [],
589
- out_dir: tempDir,
590
- });
591
- mockLoadExploreBundle.mockReturnValue({
592
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test' },
593
- endpoints: [],
594
- capabilities: [],
595
- });
596
- mockSynthesizeFromExplore.mockReturnValue({
597
- site: 'demo',
598
- explore_dir: tempDir,
599
- out_dir: tempDir,
600
- candidate_count: 0,
601
- candidates: [],
602
- });
603
- const hints = [];
604
- await generateVerifiedFromUrl({
605
- url: 'https://demo.test',
606
- BrowserFactory: class {
607
- },
608
- noRegister: true,
609
- onEarlyHint: (h) => hints.push(h),
610
- });
611
- expect(hints).toHaveLength(1);
612
- expect(hints[0]).toEqual({
613
- stage: 'explore',
614
- continue: false,
615
- reason: 'no-viable-api-surface',
616
- confidence: 'high',
617
- });
618
- });
619
- it('emits explore continue + synthesize stop hints when no candidate found', async () => {
620
- mockExploreUrl.mockResolvedValue({
621
- site: 'demo',
622
- target_url: 'https://demo.test',
623
- final_url: 'https://demo.test',
624
- title: 'Demo',
625
- framework: {},
626
- stores: [],
627
- top_strategy: 'public',
628
- endpoint_count: 1,
629
- api_endpoint_count: 1,
630
- capabilities: [],
631
- auth_indicators: [],
632
- out_dir: tempDir,
633
- });
634
- mockLoadExploreBundle.mockReturnValue({
635
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test' },
636
- endpoints: [{
637
- pattern: 'demo.test/api/hot',
638
- url: 'https://demo.test/api/hot',
639
- itemPath: 'data.items',
640
- itemCount: 5,
641
- detectedFields: { title: 'title' },
642
- }],
643
- capabilities: [],
644
- });
645
- mockSynthesizeFromExplore.mockReturnValue({
646
- site: 'demo',
647
- explore_dir: tempDir,
648
- out_dir: tempDir,
649
- candidate_count: 0,
650
- candidates: [],
651
- });
652
- const hints = [];
653
- await generateVerifiedFromUrl({
654
- url: 'https://demo.test',
655
- BrowserFactory: class {
656
- },
657
- noRegister: true,
658
- onEarlyHint: (h) => hints.push(h),
659
- });
660
- expect(hints).toHaveLength(2);
661
- expect(hints[0]).toMatchObject({ stage: 'explore', continue: true, reason: 'api-surface-looks-viable' });
662
- expect(hints[1]).toMatchObject({ stage: 'synthesize', continue: false, reason: 'no-viable-candidate' });
663
- });
664
- it('emits explore + synthesize continue hints with candidate on success path', async () => {
665
- const hotPath = path.join(tempDir, 'hot.json');
666
- fs.writeFileSync(hotPath, JSON.stringify({
667
- site: 'demo',
668
- name: 'hot',
669
- description: 'demo hot',
670
- domain: 'demo.test',
671
- strategy: 'public',
672
- browser: false,
673
- args: {},
674
- columns: ['title', 'url'],
675
- pipeline: [
676
- { fetch: { url: 'https://demo.test/api/hot' } },
677
- { select: 'data.items' },
678
- ],
679
- }, null, 2));
680
- mockExploreUrl.mockResolvedValue({
681
- site: 'demo',
682
- target_url: 'https://demo.test',
683
- final_url: 'https://demo.test',
684
- title: 'Demo',
685
- framework: {},
686
- stores: [],
687
- top_strategy: 'public',
688
- endpoint_count: 1,
689
- api_endpoint_count: 1,
690
- capabilities: [{ name: 'hot' }],
691
- auth_indicators: [],
692
- out_dir: tempDir,
693
- });
694
- mockLoadExploreBundle.mockReturnValue({
695
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test' },
696
- endpoints: [{
697
- pattern: 'demo.test/api/hot',
698
- url: 'https://demo.test/api/hot',
699
- itemPath: 'data.items',
700
- itemCount: 5,
701
- detectedFields: { title: 'title', url: 'url' },
702
- }],
703
- capabilities: [{ name: 'hot', strategy: 'public', endpoint: 'demo.test/api/hot', itemPath: 'data.items' }],
704
- });
705
- mockSynthesizeFromExplore.mockReturnValue({
706
- site: 'demo',
707
- explore_dir: tempDir,
708
- out_dir: tempDir,
709
- candidate_count: 1,
710
- candidates: [{ name: 'hot', path: hotPath, strategy: 'public' }],
711
- });
712
- const page = { goto: vi.fn() };
713
- mockBrowserSession.mockImplementation(async (_factory, fn) => fn(page));
714
- mockCascadeProbe.mockResolvedValue({
715
- bestStrategy: Strategy.PUBLIC,
716
- probes: [{ strategy: Strategy.PUBLIC, success: true }],
717
- confidence: 1.0,
718
- });
719
- mockExecutePipeline.mockResolvedValue([
720
- { title: 'Post 1', url: 'https://demo.test/1' },
721
- { title: 'Post 2', url: 'https://demo.test/2' },
722
- ]);
723
- const hints = [];
724
- const result = await generateVerifiedFromUrl({
725
- url: 'https://demo.test',
726
- BrowserFactory: class {
727
- },
728
- noRegister: true,
729
- onEarlyHint: (h) => hints.push(h),
730
- });
731
- expect(result.status).toBe('success');
732
- expect(hints).toHaveLength(3);
733
- expect(hints[0]).toMatchObject({ stage: 'explore', continue: true });
734
- expect(hints[1]).toMatchObject({
735
- stage: 'synthesize',
736
- continue: true,
737
- reason: 'candidate-ready-for-verify',
738
- candidate: { name: 'hot', command: 'demo/hot', reusability: 'unverified-candidate' },
739
- });
740
- expect(hints[2]).toMatchObject({
741
- stage: 'cascade',
742
- continue: true,
743
- reason: 'candidate-ready-for-verify',
744
- candidate: { name: 'hot', command: 'demo/hot' },
745
- });
746
- // No candidate on explore hint
747
- expect(hints[0]).not.toHaveProperty('candidate');
748
- });
749
- it('emits cascade stop hint when auth-too-complex', async () => {
750
- const hotPath = path.join(tempDir, 'hot.json');
751
- fs.writeFileSync(hotPath, JSON.stringify({
752
- site: 'demo',
753
- name: 'hot',
754
- description: 'demo hot',
755
- domain: 'demo.test',
756
- strategy: 'public',
757
- browser: false,
758
- args: {},
759
- columns: ['title', 'url'],
760
- pipeline: [
761
- { fetch: { url: 'https://demo.test/api/hot' } },
762
- { select: 'data.items' },
763
- ],
764
- }, null, 2));
765
- mockExploreUrl.mockResolvedValue({
766
- site: 'demo',
767
- target_url: 'https://demo.test',
768
- final_url: 'https://demo.test',
769
- title: 'Demo',
770
- framework: {},
771
- stores: [],
772
- top_strategy: 'cookie',
773
- endpoint_count: 1,
774
- api_endpoint_count: 1,
775
- capabilities: [{ name: 'hot' }],
776
- auth_indicators: [],
777
- out_dir: tempDir,
778
- });
779
- mockLoadExploreBundle.mockReturnValue({
780
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test' },
781
- endpoints: [{
782
- pattern: 'demo.test/api/hot',
783
- url: 'https://demo.test/api/hot',
784
- itemPath: 'data.items',
785
- itemCount: 5,
786
- detectedFields: { title: 'title', url: 'url' },
787
- }],
788
- capabilities: [{ name: 'hot', strategy: 'cookie', endpoint: 'demo.test/api/hot', itemPath: 'data.items' }],
789
- });
790
- mockSynthesizeFromExplore.mockReturnValue({
791
- site: 'demo',
792
- explore_dir: tempDir,
793
- out_dir: tempDir,
794
- candidate_count: 1,
795
- candidates: [{ name: 'hot', path: hotPath, strategy: 'public' }],
796
- });
797
- const page = { goto: vi.fn() };
798
- mockBrowserSession.mockImplementation(async (_factory, fn) => fn(page));
799
- mockCascadeProbe.mockResolvedValue({
800
- bestStrategy: Strategy.COOKIE,
801
- probes: [
802
- { strategy: Strategy.PUBLIC, success: false },
803
- { strategy: Strategy.COOKIE, success: false },
804
- ],
805
- confidence: 0.3,
806
- });
807
- const hints = [];
808
- await generateVerifiedFromUrl({
809
- url: 'https://demo.test',
810
- BrowserFactory: class {
811
- },
812
- noRegister: true,
813
- onEarlyHint: (h) => hints.push(h),
814
- });
815
- expect(hints).toHaveLength(3);
816
- expect(hints[0]).toMatchObject({ stage: 'explore', continue: true });
817
- expect(hints[1]).toMatchObject({ stage: 'synthesize', continue: true, reason: 'candidate-ready-for-verify' });
818
- expect(hints[2]).toMatchObject({ stage: 'cascade', continue: false, reason: 'auth-too-complex' });
819
- // No candidate on stop hint
820
- expect(hints[2]).not.toHaveProperty('candidate');
821
- });
822
- it('does NOT emit P2 hint for unsupported-required-args (P1-only decision)', async () => {
823
- const detailPath = path.join(tempDir, 'detail.json');
824
- fs.writeFileSync(detailPath, JSON.stringify({
825
- site: 'demo',
826
- name: 'detail',
827
- description: 'demo detail',
828
- domain: 'demo.test',
829
- strategy: 'public',
830
- browser: false,
831
- args: {
832
- id: { type: 'str', required: true },
833
- },
834
- columns: ['title', 'url'],
835
- pipeline: [
836
- { fetch: { url: 'https://demo.test/api/detail?id=${{ args.id }}' } },
837
- { select: 'data.item' },
838
- ],
839
- }, null, 2));
840
- mockExploreUrl.mockResolvedValue({
841
- site: 'demo',
842
- target_url: 'https://demo.test/detail/123',
843
- final_url: 'https://demo.test/detail/123',
844
- title: 'Demo detail',
845
- framework: {},
846
- stores: [],
847
- top_strategy: 'public',
848
- endpoint_count: 1,
849
- api_endpoint_count: 1,
850
- capabilities: [{ name: 'detail' }],
851
- auth_indicators: [],
852
- out_dir: tempDir,
853
- });
854
- mockLoadExploreBundle.mockReturnValue({
855
- manifest: { site: 'demo', target_url: 'https://demo.test/detail/123', final_url: 'https://demo.test/detail/123' },
856
- endpoints: [{
857
- pattern: 'demo.test/api/detail',
858
- url: 'https://demo.test/api/detail?id=123',
859
- itemPath: 'data.item',
860
- itemCount: 1,
861
- detectedFields: { title: 'title', url: 'url' },
862
- }],
863
- capabilities: [{ name: 'detail', strategy: 'public', endpoint: 'demo.test/api/detail', itemPath: 'data.item' }],
864
- });
865
- mockSynthesizeFromExplore.mockReturnValue({
866
- site: 'demo',
867
- explore_dir: tempDir,
868
- out_dir: tempDir,
869
- candidate_count: 1,
870
- candidates: [{ name: 'detail', path: detailPath, strategy: 'public' }],
871
- });
872
- const hints = [];
873
- const result = await generateVerifiedFromUrl({
874
- url: 'https://demo.test/detail/123',
875
- BrowserFactory: class {
876
- },
877
- goal: 'detail',
878
- noRegister: true,
879
- onEarlyHint: (h) => hints.push(h),
880
- });
881
- expect(result.status).toBe('needs-human-check');
882
- expect(result.escalation.reason).toBe('unsupported-required-args');
883
- // Only explore continue hint is emitted; NO synthesize hint before the P1 terminal
884
- expect(hints).toHaveLength(1);
885
- expect(hints[0]).toMatchObject({ stage: 'explore', continue: true });
886
- // Specifically: no synthesize hint was emitted
887
- expect(hints.find(h => h.stage === 'synthesize')).toBeUndefined();
888
- });
889
- it('does not emit hints when onEarlyHint is not provided', async () => {
890
- mockExploreUrl.mockResolvedValue({
891
- site: 'demo',
892
- target_url: 'https://demo.test',
893
- final_url: 'https://demo.test',
894
- title: 'Demo',
895
- framework: {},
896
- stores: [],
897
- top_strategy: 'public',
898
- endpoint_count: 1,
899
- api_endpoint_count: 0,
900
- capabilities: [],
901
- auth_indicators: [],
902
- out_dir: tempDir,
903
- });
904
- mockLoadExploreBundle.mockReturnValue({
905
- manifest: { site: 'demo', target_url: 'https://demo.test', final_url: 'https://demo.test' },
906
- endpoints: [],
907
- capabilities: [],
908
- });
909
- mockSynthesizeFromExplore.mockReturnValue({
910
- site: 'demo',
911
- explore_dir: tempDir,
912
- out_dir: tempDir,
913
- candidate_count: 0,
914
- candidates: [],
915
- });
916
- // Should not throw even without onEarlyHint
917
- const result = await generateVerifiedFromUrl({
918
- url: 'https://demo.test',
919
- BrowserFactory: class {
920
- },
921
- noRegister: true,
922
- });
923
- expect(result.status).toBe('blocked');
924
- });
925
- });