@epoint-testtech/stage-create 0.0.3-alpha.1
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.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +129 -0
- package/dist/render-template.d.ts +10 -0
- package/dist/render-template.js +34 -0
- package/dist/stage-context.d.ts +27 -0
- package/dist/stage-context.js +175 -0
- package/package.json +33 -0
- package/templates/default/.env.example +18 -0
- package/templates/default/package.json.tpl +20 -0
- package/templates/default/playwright.config.ts +44 -0
- package/templates/default/setup-mac.sh +29 -0
- package/templates/default/setup-windows.cmd +13 -0
- package/templates/default/setup-windows.ps1 +26 -0
- package/templates/default/src/global.setup.ts +5 -0
- package/templates/default/tsconfig.json +12 -0
- package/templates/glue/.env.example +18 -0
- package/templates/glue/package.json.tpl +21 -0
- package/templates/glue/playwright.config.ts +56 -0
- package/templates/glue/src/data/stage-config.yaml.tpl +13 -0
- package/templates/glue/src/gap-executor/gap-executor.ts +162 -0
- package/templates/glue/src/gap-executor/gap-healer.ts +50 -0
- package/templates/glue/src/gap-executor/page-structure-observer.ts +102 -0
- package/templates/glue/src/gap-executor/runtime-runner.ts +817 -0
- package/templates/glue/src/report/glue-report.ts +855 -0
- package/templates/glue/src/report/run-info.ts +85 -0
- package/templates/glue/src/skeletons/crud.skeleton.ts +450 -0
- package/templates/glue/src/skeletons/export.skeleton.ts +114 -0
- package/templates/glue/src/skeletons/index.ts +18 -0
- package/templates/glue/src/skeletons/login.skeleton.ts +104 -0
- package/templates/glue/src/skeletons/menu.skeleton.ts +37 -0
- package/templates/glue/src/tests/example.spec.ts +99 -0
- package/templates/glue/src/web/component/anchor-types.ts +9 -0
- package/templates/glue/src/web/component/dataexport-component.ts +143 -0
- package/templates/glue/src/web/component/index.ts +2 -0
- package/templates/glue/src/web/component/listbox-component.ts +41 -0
- package/templates/glue/tsconfig.json +12 -0
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
|
|
5
|
+
import { chromium, type Browser, type Dialog, type Frame, type Locator, type Page, type Response } from '@playwright/test';
|
|
6
|
+
import type { AgentInferredWorkflow, CrudBusinessModuleContract, GapExplorationResult, GapInferDraft } from '@epoint-testtech/ep-stage-skill';
|
|
7
|
+
import { listGapWorkflows } from '@epoint-testtech/ep-stage-skill';
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
|
+
|
|
10
|
+
import { LoginPage } from '../skeletons/login.skeleton';
|
|
11
|
+
import { MenuPage } from '../skeletons/menu.skeleton';
|
|
12
|
+
import {
|
|
13
|
+
appendResolvedAgentWorkflowToSpec,
|
|
14
|
+
createResolvedWorkflowFromPlanned,
|
|
15
|
+
} from './gap-executor';
|
|
16
|
+
import {
|
|
17
|
+
collectRuntimePageStructureSignals,
|
|
18
|
+
type RuntimePageStructureSignal,
|
|
19
|
+
} from './page-structure-observer.js';
|
|
20
|
+
|
|
21
|
+
export type GapRuntimeRunnerInput = {
|
|
22
|
+
contractPath: string;
|
|
23
|
+
specPath: string;
|
|
24
|
+
projectDir: string;
|
|
25
|
+
menuRoute: string;
|
|
26
|
+
moduleId: string;
|
|
27
|
+
workflowIds: string[];
|
|
28
|
+
headless?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type GapRuntimeRunnerResult = {
|
|
32
|
+
resolvedWorkflows: string[];
|
|
33
|
+
skippedWorkflows: string[];
|
|
34
|
+
needsReviewWorkflows: string[];
|
|
35
|
+
needsReviewWorkflowReasons?: Array<{
|
|
36
|
+
workflowId: string;
|
|
37
|
+
reason: string;
|
|
38
|
+
}>;
|
|
39
|
+
specPath: string;
|
|
40
|
+
pageStructureSignals?: RuntimePageStructureSignal[];
|
|
41
|
+
runtimePageStructureStatus?: 'collected' | 'environment_unavailable';
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type LocatorResolution = {
|
|
45
|
+
locator: string;
|
|
46
|
+
locatorStrategy: 'html_static' | 'agent_refined';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 读取 contract 中可进入 runtime resolve 的 planned workflows。
|
|
51
|
+
* @param contract CRUD 业务模块契约
|
|
52
|
+
* @param workflowIds 目标 workflowId 列表;为空时默认使用全部 planned workflows
|
|
53
|
+
* @returns 过滤后的 planned workflows
|
|
54
|
+
*/
|
|
55
|
+
export function pickPlannedWorkflows(
|
|
56
|
+
contract: CrudBusinessModuleContract,
|
|
57
|
+
workflowIds: string[],
|
|
58
|
+
): AgentInferredWorkflow[] {
|
|
59
|
+
const allowed = new Set(workflowIds);
|
|
60
|
+
return (contract.agentInferredWorkflows ?? []).filter((workflow) => {
|
|
61
|
+
if (workflow.status !== 'planned') {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return allowed.size === 0 || allowed.has(workflow.workflowId);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 生成 runtime runner 结果结构,便于测试和 CLI 输出复用。
|
|
70
|
+
* @param input 运行结果输入
|
|
71
|
+
* @returns 原样返回的结果对象
|
|
72
|
+
*/
|
|
73
|
+
export function summarizeRuntimeRunnerResult(input: GapRuntimeRunnerResult): GapRuntimeRunnerResult {
|
|
74
|
+
return input;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function waitUntilVisible(locator: Locator, timeout = 5_000): Promise<boolean> {
|
|
78
|
+
try {
|
|
79
|
+
await locator.first().waitFor({ state: 'visible', timeout });
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function listSearchContexts(page: Page, iframeKeyword?: string): Array<Page | Frame> {
|
|
87
|
+
const contexts: Array<Page | Frame> = [page];
|
|
88
|
+
for (const frame of page.frames()) {
|
|
89
|
+
if (!iframeKeyword || frame.url().includes(iframeKeyword)) {
|
|
90
|
+
contexts.push(frame);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return contexts;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatRuntimeError(error: unknown): string {
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
return error.message.split('\n')[0] ?? error.message;
|
|
99
|
+
}
|
|
100
|
+
return String(error);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function fallbackLocatorByLabel(label: string): string {
|
|
104
|
+
return `xpath=//a[contains(@class,'mini-button')][normalize-space()='${label}']`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function resolveWorkflowLocator(
|
|
108
|
+
page: Page,
|
|
109
|
+
workflow: AgentInferredWorkflow,
|
|
110
|
+
iframeKeyword?: string,
|
|
111
|
+
): Promise<LocatorResolution> {
|
|
112
|
+
const candidateLocator = workflow.actionCandidates[0]?.locator;
|
|
113
|
+
for (const context of listSearchContexts(page, iframeKeyword)) {
|
|
114
|
+
if (candidateLocator) {
|
|
115
|
+
const candidate = context.locator(candidateLocator);
|
|
116
|
+
if (await waitUntilVisible(candidate)) {
|
|
117
|
+
return {
|
|
118
|
+
locator: candidateLocator,
|
|
119
|
+
locatorStrategy: 'html_static',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const label = workflow.actionCandidates[0]?.label ?? workflow.workflowId;
|
|
125
|
+
const fallbackLocator = fallbackLocatorByLabel(label);
|
|
126
|
+
const fallback = context.locator(fallbackLocator);
|
|
127
|
+
if (await waitUntilVisible(fallback)) {
|
|
128
|
+
return {
|
|
129
|
+
locator: fallbackLocator,
|
|
130
|
+
locatorStrategy: 'agent_refined',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const label = workflow.actionCandidates[0]?.label ?? workflow.workflowId;
|
|
136
|
+
throw new Error(`workflow ${workflow.workflowId} 未能在真实页面定位到按钮「${label}」`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function openModule(page: Page, menuRoute: string): Promise<void> {
|
|
140
|
+
const loginPage = new LoginPage(page);
|
|
141
|
+
const menuPage = new MenuPage(page);
|
|
142
|
+
await loginPage.login();
|
|
143
|
+
await menuPage.navigateToMenu(menuRoute);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type GapExploreInput = {
|
|
147
|
+
contractPath: string;
|
|
148
|
+
projectDir: string;
|
|
149
|
+
menuRoute: string;
|
|
150
|
+
moduleId: string;
|
|
151
|
+
workflowIds: string[];
|
|
152
|
+
headless?: boolean;
|
|
153
|
+
/** 是否进一步点击确认框的确认按钮。默认 false,先只探到确认框文案,避免触发真实业务副作用。 */
|
|
154
|
+
confirmExecute?: boolean;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** 探索时定位到目标按钮所在的运行态上下文与 locator。 */
|
|
158
|
+
type ExploreLocation = {
|
|
159
|
+
context: Page | Frame;
|
|
160
|
+
locator: string;
|
|
161
|
+
locatorStrategy: 'html_static' | 'agent_refined';
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** MiniUI 运行态浮层选择器集合,采用“或抓宽”策略覆盖确认框 / 提示框 / 业务弹窗 / 轻提示。 */
|
|
165
|
+
const OVERLAY_SELECTORS = [
|
|
166
|
+
'.mini-messagebox',
|
|
167
|
+
'[class*="messagebox"]',
|
|
168
|
+
'[class*="msgbox"]',
|
|
169
|
+
'[class*="toast"]',
|
|
170
|
+
'[class*="tip-"]',
|
|
171
|
+
'.mini-window:visible',
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 在 page 及候选 iframe 中定位 gap 按钮,返回命中的上下文与 locator。
|
|
176
|
+
* @param page Playwright 页面
|
|
177
|
+
* @param workflow 目标 gap workflow
|
|
178
|
+
* @param iframeKeyword 列表页 iframe 关键字
|
|
179
|
+
* @returns 命中的上下文与 locator;未命中返回 undefined
|
|
180
|
+
*/
|
|
181
|
+
async function locateWorkflowButton(
|
|
182
|
+
page: Page,
|
|
183
|
+
workflow: AgentInferredWorkflow,
|
|
184
|
+
iframeKeyword?: string,
|
|
185
|
+
): Promise<ExploreLocation | undefined> {
|
|
186
|
+
const candidateLocator = workflow.actionCandidates[0]?.locator;
|
|
187
|
+
const label = workflow.actionCandidates[0]?.label ?? workflow.workflowId;
|
|
188
|
+
const fallbackLocator = fallbackLocatorByLabel(label);
|
|
189
|
+
|
|
190
|
+
for (const context of listSearchContexts(page, iframeKeyword)) {
|
|
191
|
+
if (candidateLocator) {
|
|
192
|
+
const candidate = context.locator(candidateLocator);
|
|
193
|
+
if (await waitUntilVisible(candidate)) {
|
|
194
|
+
return { context, locator: candidateLocator, locatorStrategy: 'html_static' };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const fallback = context.locator(fallbackLocator);
|
|
198
|
+
if (await waitUntilVisible(fallback)) {
|
|
199
|
+
return { context, locator: fallbackLocator, locatorStrategy: 'agent_refined' };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 广谱捕获各上下文中可见浮层的文本,用于识别确认框 / 提示框文案。
|
|
207
|
+
* @param page Playwright 页面
|
|
208
|
+
* @param iframeKeyword 列表页 iframe 关键字
|
|
209
|
+
* @returns 去重后的可见浮层文本数组
|
|
210
|
+
*/
|
|
211
|
+
async function captureOverlayTexts(page: Page, iframeKeyword?: string): Promise<string[]> {
|
|
212
|
+
const texts: string[] = [];
|
|
213
|
+
for (const context of listSearchContexts(page, iframeKeyword)) {
|
|
214
|
+
for (const selector of OVERLAY_SELECTORS) {
|
|
215
|
+
const locator = context.locator(selector);
|
|
216
|
+
const count = await locator.count().catch(() => 0);
|
|
217
|
+
for (let index = 0; index < count; index += 1) {
|
|
218
|
+
const element = locator.nth(index);
|
|
219
|
+
if (await element.isVisible().catch(() => false)) {
|
|
220
|
+
const text = (await element.innerText().catch(() => '')).trim();
|
|
221
|
+
if (text) {
|
|
222
|
+
texts.push(text);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return Array.from(new Set(texts));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 在指定上下文注入 MutationObserver,记录后续新增/变更节点的可见文本。
|
|
233
|
+
*
|
|
234
|
+
* 用于捕获“点确定后”可能瞬时出现又快速消失的 toast / 异常提示——
|
|
235
|
+
* 即使元素随后被移除,其文本已被记录到 window.__stageObservedTexts。
|
|
236
|
+
*
|
|
237
|
+
* @param context 运行态上下文(page 或 frame)
|
|
238
|
+
*/
|
|
239
|
+
async function installOverlayObserver(context: Page | Frame): Promise<void> {
|
|
240
|
+
await context
|
|
241
|
+
.evaluate(() => {
|
|
242
|
+
const w = window as unknown as { __stageObservedTexts?: string[]; __stageObserver?: MutationObserver };
|
|
243
|
+
w.__stageObservedTexts = [];
|
|
244
|
+
const seen = new Set<string>();
|
|
245
|
+
const record = (node: Node): void => {
|
|
246
|
+
if (!(node instanceof HTMLElement)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const text = (node.innerText || '').trim();
|
|
250
|
+
if (text && !seen.has(text)) {
|
|
251
|
+
seen.add(text);
|
|
252
|
+
w.__stageObservedTexts!.push(text);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const observer = new MutationObserver((mutations) => {
|
|
256
|
+
for (const mutation of mutations) {
|
|
257
|
+
mutation.addedNodes.forEach(record);
|
|
258
|
+
if (mutation.type === 'attributes') {
|
|
259
|
+
record(mutation.target);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
observer.observe(document.body, {
|
|
264
|
+
childList: true,
|
|
265
|
+
subtree: true,
|
|
266
|
+
attributes: true,
|
|
267
|
+
attributeFilter: ['style', 'class'],
|
|
268
|
+
});
|
|
269
|
+
w.__stageObserver = observer;
|
|
270
|
+
})
|
|
271
|
+
.catch(() => undefined);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 读取指定上下文中 MutationObserver 记录到的全部文本。
|
|
276
|
+
* @param context 运行态上下文(page 或 frame)
|
|
277
|
+
* @returns 观测到的文本数组
|
|
278
|
+
*/
|
|
279
|
+
async function collectObserverTexts(context: Page | Frame): Promise<string[]> {
|
|
280
|
+
return context
|
|
281
|
+
.evaluate(() => {
|
|
282
|
+
const w = window as unknown as { __stageObservedTexts?: string[] };
|
|
283
|
+
return w.__stageObservedTexts ?? [];
|
|
284
|
+
})
|
|
285
|
+
.catch(() => [] as string[]);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 在确认框出现时点击其确认按钮,触发真实业务执行。
|
|
290
|
+
* @param page Playwright 页面
|
|
291
|
+
* @param iframeKeyword 列表页 iframe 关键字
|
|
292
|
+
* @returns 是否成功点击了确认按钮
|
|
293
|
+
*/
|
|
294
|
+
async function clickConfirmButton(page: Page, iframeKeyword?: string): Promise<boolean> {
|
|
295
|
+
for (const context of listSearchContexts(page, iframeKeyword)) {
|
|
296
|
+
const confirmButton = context
|
|
297
|
+
.locator("xpath=//*[contains(@class,'mini-messagebox')]//a[normalize-space()='确定' or normalize-space()='确认']")
|
|
298
|
+
.first();
|
|
299
|
+
if (await confirmButton.isVisible().catch(() => false)) {
|
|
300
|
+
await confirmButton.click().catch(() => undefined);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 在真实页面探索单个 gap 按钮点击后的运行态行为,产出结构化探索事实。
|
|
309
|
+
*
|
|
310
|
+
* 探索只观测“真实发生了什么”,不生成正式断言;confirmExecute 为 false 时不点击确认框,
|
|
311
|
+
* 保证零业务副作用即可拿到“是否有确认框 + 确认框文案”这一关键事实。
|
|
312
|
+
*
|
|
313
|
+
* @param page Playwright 页面
|
|
314
|
+
* @param workflow 目标 gap workflow
|
|
315
|
+
* @param options 探索选项(iframe 关键字、是否执行确认)
|
|
316
|
+
* @returns 单个 workflow 的探索结果
|
|
317
|
+
*/
|
|
318
|
+
async function exploreWorkflow(
|
|
319
|
+
page: Page,
|
|
320
|
+
workflow: AgentInferredWorkflow,
|
|
321
|
+
options: { iframeKeyword?: string; confirmExecute: boolean },
|
|
322
|
+
): Promise<GapExplorationResult> {
|
|
323
|
+
const label = workflow.actionCandidates[0]?.label ?? workflow.workflowId;
|
|
324
|
+
const result: GapExplorationResult = {
|
|
325
|
+
workflowId: workflow.workflowId,
|
|
326
|
+
buttonLabel: label,
|
|
327
|
+
locator: '',
|
|
328
|
+
locatorStrategy: 'html_static',
|
|
329
|
+
buttonVisible: false,
|
|
330
|
+
buttonClickable: false,
|
|
331
|
+
confirmDialogPresent: false,
|
|
332
|
+
confirmExecuted: false,
|
|
333
|
+
signalType: 'none',
|
|
334
|
+
pageChanged: false,
|
|
335
|
+
downloadTriggered: false,
|
|
336
|
+
networkResponses: [],
|
|
337
|
+
confidence: 'low',
|
|
338
|
+
exploredAt: new Date().toISOString(),
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const location = await locateWorkflowButton(page, workflow, options.iframeKeyword);
|
|
342
|
+
if (!location) {
|
|
343
|
+
return { ...result, notes: `未能在真实页面定位按钮「${label}」` };
|
|
344
|
+
}
|
|
345
|
+
result.locator = location.locator;
|
|
346
|
+
result.locatorStrategy = location.locatorStrategy;
|
|
347
|
+
result.buttonVisible = true;
|
|
348
|
+
|
|
349
|
+
const target = location.context.locator(location.locator).first();
|
|
350
|
+
result.buttonClickable = await target.isEnabled().catch(() => false);
|
|
351
|
+
|
|
352
|
+
const dialogMessages: string[] = [];
|
|
353
|
+
const networkResponses: string[] = [];
|
|
354
|
+
let downloadTriggered = false;
|
|
355
|
+
const onDialog = (dialog: Dialog): void => {
|
|
356
|
+
dialogMessages.push(dialog.message());
|
|
357
|
+
dialog.dismiss().catch(() => undefined);
|
|
358
|
+
};
|
|
359
|
+
const onDownload = (): void => {
|
|
360
|
+
downloadTriggered = true;
|
|
361
|
+
};
|
|
362
|
+
const onResponse = (response: Response): void => {
|
|
363
|
+
const resourceType = response.request().resourceType();
|
|
364
|
+
if (!['xhr', 'fetch', 'document'].includes(resourceType) || networkResponses.length >= 20) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
networkResponses.push(`${response.status()} ${response.url()}`);
|
|
368
|
+
};
|
|
369
|
+
page.on('dialog', onDialog);
|
|
370
|
+
page.on('download', onDownload);
|
|
371
|
+
page.on('response', onResponse);
|
|
372
|
+
|
|
373
|
+
const urlBefore = page.url();
|
|
374
|
+
const overlayBefore = await captureOverlayTexts(page, options.iframeKeyword);
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
await target.click({ timeout: 5_000 });
|
|
378
|
+
} catch {
|
|
379
|
+
// 点击失败也保留已观测事实,交由人工判断。
|
|
380
|
+
}
|
|
381
|
+
await page.waitForTimeout(2_000);
|
|
382
|
+
|
|
383
|
+
const overlayAfterClick = await captureOverlayTexts(page, options.iframeKeyword);
|
|
384
|
+
const newOverlaysAfterClick = overlayAfterClick.filter((text) => !overlayBefore.includes(text));
|
|
385
|
+
if (newOverlaysAfterClick.length > 0) {
|
|
386
|
+
result.confirmDialogPresent = true;
|
|
387
|
+
result.confirmText = newOverlaysAfterClick.join(' | ');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 点确定后可能出现瞬时 toast(尤其异常提示消失很快)。
|
|
391
|
+
// 因此在点确定前装好 observer,点确定后高频轮询,确保捕获“出现过即记录”的文本。
|
|
392
|
+
const postConfirmTexts = new Set<string>();
|
|
393
|
+
if (options.confirmExecute && result.confirmDialogPresent) {
|
|
394
|
+
const contexts = listSearchContexts(page, options.iframeKeyword);
|
|
395
|
+
for (const context of contexts) {
|
|
396
|
+
await installOverlayObserver(context);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
result.confirmExecuted = await clickConfirmButton(page, options.iframeKeyword);
|
|
400
|
+
|
|
401
|
+
const deadline = Date.now() + 6_000;
|
|
402
|
+
while (Date.now() < deadline) {
|
|
403
|
+
for (const context of contexts) {
|
|
404
|
+
for (const text of await collectObserverTexts(context)) {
|
|
405
|
+
postConfirmTexts.add(text.trim());
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const text of await captureOverlayTexts(page, options.iframeKeyword)) {
|
|
409
|
+
postConfirmTexts.add(text);
|
|
410
|
+
}
|
|
411
|
+
await page.waitForTimeout(150);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
page.off('dialog', onDialog);
|
|
416
|
+
page.off('download', onDownload);
|
|
417
|
+
page.off('response', onResponse);
|
|
418
|
+
|
|
419
|
+
result.pageChanged = page.url() !== urlBefore;
|
|
420
|
+
result.downloadTriggered = downloadTriggered;
|
|
421
|
+
result.networkResponses = networkResponses;
|
|
422
|
+
|
|
423
|
+
// 点确定后的“新增信号文本” = 捕获到的全部文本 - 点击前已存在 - 确认框自身文案碎片。
|
|
424
|
+
const confirmFragments = new Set(newOverlaysAfterClick);
|
|
425
|
+
const finalSignalTexts = Array.from(postConfirmTexts).filter(
|
|
426
|
+
(text) => text && !overlayBefore.includes(text) && !confirmFragments.has(text),
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
if (downloadTriggered) {
|
|
430
|
+
result.signalType = 'download';
|
|
431
|
+
} else if (dialogMessages.length > 0) {
|
|
432
|
+
result.signalType = 'alert';
|
|
433
|
+
result.observedMessage = dialogMessages.join(' | ');
|
|
434
|
+
} else if (result.confirmExecuted && finalSignalTexts.length > 0) {
|
|
435
|
+
// 确认执行后出现的轻提示,归类为 toast(同步结果/异常提示的典型形态)。
|
|
436
|
+
result.signalType = 'toast';
|
|
437
|
+
result.observedMessage = finalSignalTexts.join(' | ');
|
|
438
|
+
} else if (result.confirmExecuted && finalSignalTexts.length === 0) {
|
|
439
|
+
// 确认执行后无任何新提示,确认框消失即视为静默完成。
|
|
440
|
+
result.signalType = 'none';
|
|
441
|
+
} else if (result.confirmDialogPresent) {
|
|
442
|
+
result.signalType = 'confirm';
|
|
443
|
+
} else if (result.pageChanged) {
|
|
444
|
+
result.signalType = 'page_change';
|
|
445
|
+
} else {
|
|
446
|
+
result.signalType = 'none';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
result.suggestedIntent = workflow.businessIntent.status === 'resolved'
|
|
450
|
+
? workflow.businessIntent.value
|
|
451
|
+
: `点击「${label}」并完成对应业务操作`;
|
|
452
|
+
result.suggestedAssertion = result.observedMessage ?? result.confirmText;
|
|
453
|
+
result.confidence = result.observedMessage ? 'medium' : result.confirmDialogPresent ? 'medium' : 'low';
|
|
454
|
+
result.notes = [
|
|
455
|
+
`dialogMessages=${JSON.stringify(dialogMessages)}`,
|
|
456
|
+
`newOverlaysAfterClick=${JSON.stringify(newOverlaysAfterClick)}`,
|
|
457
|
+
`postConfirmSignals=${JSON.stringify(finalSignalTexts)}`,
|
|
458
|
+
`networkResponses=${JSON.stringify(networkResponses)}`,
|
|
459
|
+
].join('; ');
|
|
460
|
+
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 探索模式入口:对 contract 中目标 gap 在真实页面逐个探索,落袋结构化探索结果。
|
|
466
|
+
*
|
|
467
|
+
* 与 resolve 不同,探索模式不要求 workflow 已具备 intent/assertion,candidate 也可进入;
|
|
468
|
+
* 探索结果写入 glue-report/exploration 供人工确认,不直接追加测试。
|
|
469
|
+
*
|
|
470
|
+
* @param input 探索输入(contract 路径、菜单、目标 workflowId 等)
|
|
471
|
+
* @returns 全部目标 workflow 的探索结果
|
|
472
|
+
*/
|
|
473
|
+
export async function runGapRuntimeExplore(input: GapExploreInput): Promise<GapExplorationResult[]> {
|
|
474
|
+
dotenv.config({ path: path.join(input.projectDir, '.env') });
|
|
475
|
+
|
|
476
|
+
process.env.STAGE_PROJECT_ROOT_DIR_PATH = input.projectDir;
|
|
477
|
+
process.env.STAGE_AUTH_FILE_PATH = path.join(input.projectDir, 'glue-report', 'stage-auth.json');
|
|
478
|
+
|
|
479
|
+
const contract = JSON.parse(fs.readFileSync(input.contractPath, 'utf8')) as CrudBusinessModuleContract;
|
|
480
|
+
const allowed = new Set(input.workflowIds);
|
|
481
|
+
const targets = (contract.agentInferredWorkflows ?? []).filter(
|
|
482
|
+
(workflow) => allowed.size === 0 || allowed.has(workflow.workflowId),
|
|
483
|
+
);
|
|
484
|
+
const iframeKeyword = contract.pages.list.iframeSrcKeyword;
|
|
485
|
+
|
|
486
|
+
const results: GapExplorationResult[] = [];
|
|
487
|
+
let browser: Browser | undefined;
|
|
488
|
+
try {
|
|
489
|
+
browser = await chromium.launch({ headless: input.headless ?? true, slowMo: 300 });
|
|
490
|
+
const page = await browser.newPage({ viewport: { width: 1800, height: 900 }, ignoreHTTPSErrors: true });
|
|
491
|
+
await openModule(page, input.menuRoute);
|
|
492
|
+
|
|
493
|
+
for (const workflow of targets) {
|
|
494
|
+
const explored = await exploreWorkflow(page, workflow, {
|
|
495
|
+
iframeKeyword,
|
|
496
|
+
confirmExecute: input.confirmExecute ?? false,
|
|
497
|
+
});
|
|
498
|
+
results.push(explored);
|
|
499
|
+
}
|
|
500
|
+
} finally {
|
|
501
|
+
if (browser) {
|
|
502
|
+
await browser.close();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const exploreDir = path.join(input.projectDir, 'glue-report', 'exploration');
|
|
507
|
+
fs.mkdirSync(exploreDir, { recursive: true });
|
|
508
|
+
const outputPath = path.join(exploreDir, `${input.moduleId}.exploration.json`);
|
|
509
|
+
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2), 'utf8');
|
|
510
|
+
|
|
511
|
+
return results;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export type GapInferInput = {
|
|
515
|
+
contractPath: string;
|
|
516
|
+
projectDir: string;
|
|
517
|
+
menuRoute: string;
|
|
518
|
+
moduleId: string;
|
|
519
|
+
workflowIds: string[];
|
|
520
|
+
headless?: boolean;
|
|
521
|
+
/** 是否点击确认框执行业务以观测真实结果。引导模式需要验证真实反馈,默认 true。 */
|
|
522
|
+
confirmExecute?: boolean;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
/** 运行态反馈是否“未实现”类的关键词。 */
|
|
526
|
+
const FEATURE_INCOMPLETE_PATTERN = /未实现|暂未|未开发|未配置|敬请期待|联系管理员/;
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* 对比人工预期与真机观测,判定引导推理草案的功能缺口结论。
|
|
530
|
+
* @param guidedAssertion 人工给定的断言预期
|
|
531
|
+
* @param exploration 真实页面观测结果
|
|
532
|
+
* @returns intentSatisfied 与建议的功能缺口结论
|
|
533
|
+
*/
|
|
534
|
+
function judgeInferDraft(
|
|
535
|
+
guidedAssertion: string,
|
|
536
|
+
exploration: GapExplorationResult,
|
|
537
|
+
): Pick<GapInferDraft, 'intentSatisfied' | 'suggestedGapFinding'> {
|
|
538
|
+
const observed = exploration.observedMessage ?? exploration.confirmText ?? '';
|
|
539
|
+
const satisfied =
|
|
540
|
+
Boolean(guidedAssertion) &&
|
|
541
|
+
(observed.includes(guidedAssertion) ||
|
|
542
|
+
(exploration.signalType === 'toast' && (observed.includes('成功') || observed.includes('完成'))));
|
|
543
|
+
|
|
544
|
+
if (satisfied) {
|
|
545
|
+
return { intentSatisfied: true };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const incomplete = FEATURE_INCOMPLETE_PATTERN.test(observed);
|
|
549
|
+
return {
|
|
550
|
+
intentSatisfied: false,
|
|
551
|
+
suggestedGapFinding: {
|
|
552
|
+
kind: incomplete ? 'feature_incomplete' : 'behavior_mismatch',
|
|
553
|
+
summary: incomplete
|
|
554
|
+
? `功能未实现:人工预期「${guidedAssertion}」,真机反馈未达成。`
|
|
555
|
+
: `行为与预期不符:人工预期「${guidedAssertion}」,真机反馈与预期不同。`,
|
|
556
|
+
observed: observed || undefined,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* 引导模式入口:对 planned(人工已给业务意图)的 gap 在真实页面推理验证,产出待确认草案。
|
|
563
|
+
*
|
|
564
|
+
* 与 explore 不同,infer 要求 workflow 已具备人工业务意图/断言(planned),并对比
|
|
565
|
+
* 「人工预期 vs 真机观测」给出落袋建议;草案写入 glue-report/exploration 供人工确认,不直接落袋。
|
|
566
|
+
*
|
|
567
|
+
* @param input 引导推理输入
|
|
568
|
+
* @returns 全部 planned workflow 的引导推理草案
|
|
569
|
+
*/
|
|
570
|
+
export async function runGapRuntimeInfer(input: GapInferInput): Promise<GapInferDraft[]> {
|
|
571
|
+
dotenv.config({ path: path.join(input.projectDir, '.env') });
|
|
572
|
+
|
|
573
|
+
process.env.STAGE_PROJECT_ROOT_DIR_PATH = input.projectDir;
|
|
574
|
+
process.env.STAGE_AUTH_FILE_PATH = path.join(input.projectDir, 'glue-report', 'stage-auth.json');
|
|
575
|
+
|
|
576
|
+
const contract = JSON.parse(fs.readFileSync(input.contractPath, 'utf8')) as CrudBusinessModuleContract;
|
|
577
|
+
const targets = pickPlannedWorkflows(contract, input.workflowIds);
|
|
578
|
+
const iframeKeyword = contract.pages.list.iframeSrcKeyword;
|
|
579
|
+
|
|
580
|
+
const drafts: GapInferDraft[] = [];
|
|
581
|
+
let browser: Browser | undefined;
|
|
582
|
+
try {
|
|
583
|
+
browser = await chromium.launch({ headless: input.headless ?? true, slowMo: 300 });
|
|
584
|
+
const page = await browser.newPage({ viewport: { width: 1800, height: 900 }, ignoreHTTPSErrors: true });
|
|
585
|
+
await openModule(page, input.menuRoute);
|
|
586
|
+
|
|
587
|
+
for (const workflow of targets) {
|
|
588
|
+
const exploration = await exploreWorkflow(page, workflow, {
|
|
589
|
+
iframeKeyword,
|
|
590
|
+
confirmExecute: input.confirmExecute ?? true,
|
|
591
|
+
});
|
|
592
|
+
const guidedIntent = workflow.businessIntent.status === 'resolved' ? workflow.businessIntent.value ?? '' : '';
|
|
593
|
+
const guidedAssertion =
|
|
594
|
+
workflow.assertionExpectation.status === 'resolved' ? workflow.assertionExpectation.value ?? '' : '';
|
|
595
|
+
const judged = judgeInferDraft(guidedAssertion, exploration);
|
|
596
|
+
drafts.push({
|
|
597
|
+
workflowId: workflow.workflowId,
|
|
598
|
+
buttonLabel: exploration.buttonLabel,
|
|
599
|
+
guidedIntent,
|
|
600
|
+
guidedAssertion,
|
|
601
|
+
exploration,
|
|
602
|
+
intentSatisfied: judged.intentSatisfied,
|
|
603
|
+
suggestedGapFinding: judged.suggestedGapFinding,
|
|
604
|
+
confidence: exploration.confidence,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
} finally {
|
|
608
|
+
if (browser) {
|
|
609
|
+
await browser.close();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const exploreDir = path.join(input.projectDir, 'glue-report', 'exploration');
|
|
614
|
+
fs.mkdirSync(exploreDir, { recursive: true });
|
|
615
|
+
const outputPath = path.join(exploreDir, `${input.moduleId}.infer.json`);
|
|
616
|
+
fs.writeFileSync(outputPath, JSON.stringify(drafts, null, 2), 'utf8');
|
|
617
|
+
|
|
618
|
+
return drafts;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* 在真实页面中把 planned workflows 补齐为 resolved,并追加到目标 spec。
|
|
623
|
+
* @param input runtime runner 输入
|
|
624
|
+
* @returns resolved / skipped / needs_review 汇总结果
|
|
625
|
+
*/
|
|
626
|
+
export async function runGapRuntimeResolve(input: GapRuntimeRunnerInput): Promise<GapRuntimeRunnerResult> {
|
|
627
|
+
dotenv.config({ path: path.join(input.projectDir, '.env') });
|
|
628
|
+
|
|
629
|
+
process.env.STAGE_PROJECT_ROOT_DIR_PATH = input.projectDir;
|
|
630
|
+
process.env.STAGE_AUTH_FILE_PATH = path.join(input.projectDir, 'glue-report', 'stage-auth.json');
|
|
631
|
+
|
|
632
|
+
const contract = JSON.parse(fs.readFileSync(input.contractPath, 'utf8')) as CrudBusinessModuleContract;
|
|
633
|
+
const plannedWorkflows = pickPlannedWorkflows(contract, input.workflowIds);
|
|
634
|
+
let specSource = fs.readFileSync(input.specPath, 'utf8');
|
|
635
|
+
const listIframeKeyword = contract.pages.list.iframeSrcKeyword;
|
|
636
|
+
|
|
637
|
+
if (plannedWorkflows.length === 0) {
|
|
638
|
+
return summarizeRuntimeRunnerResult({
|
|
639
|
+
resolvedWorkflows: [],
|
|
640
|
+
skippedWorkflows: [],
|
|
641
|
+
needsReviewWorkflows: [],
|
|
642
|
+
needsReviewWorkflowReasons: [],
|
|
643
|
+
specPath: input.specPath,
|
|
644
|
+
pageStructureSignals: [],
|
|
645
|
+
runtimePageStructureStatus: 'environment_unavailable',
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
let browser: Browser | undefined;
|
|
650
|
+
const resolvedWorkflows: string[] = [];
|
|
651
|
+
const skippedWorkflows: string[] = [];
|
|
652
|
+
const needsReviewWorkflows: string[] = [];
|
|
653
|
+
const needsReviewWorkflowReasons: Array<{ workflowId: string; reason: string }> = [];
|
|
654
|
+
let pageStructureSignals: RuntimePageStructureSignal[] = [];
|
|
655
|
+
let runtimePageStructureStatus: 'collected' | 'environment_unavailable' = 'collected';
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
browser = await chromium.launch({ headless: input.headless ?? true, slowMo: 500 });
|
|
659
|
+
const page = await browser.newPage({ viewport: { width: 1800, height: 900 }, ignoreHTTPSErrors: true });
|
|
660
|
+
await openModule(page, input.menuRoute);
|
|
661
|
+
|
|
662
|
+
// 打开模块页面后采集 MiniUI 核心结构运行时可见性;采集失败标记环境不可用,不阻断 workflow 推理。
|
|
663
|
+
try {
|
|
664
|
+
pageStructureSignals = await collectRuntimePageStructureSignals(page, listIframeKeyword);
|
|
665
|
+
} catch {
|
|
666
|
+
runtimePageStructureStatus = 'environment_unavailable';
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
for (const workflow of plannedWorkflows) {
|
|
670
|
+
try {
|
|
671
|
+
const resolution = await resolveWorkflowLocator(page, workflow, listIframeKeyword);
|
|
672
|
+
const exploration = await exploreWorkflow(page, workflow, {
|
|
673
|
+
iframeKeyword: listIframeKeyword,
|
|
674
|
+
confirmExecute: true,
|
|
675
|
+
});
|
|
676
|
+
const resolved = createResolvedWorkflowFromPlanned({
|
|
677
|
+
workflow,
|
|
678
|
+
locator: resolution.locator,
|
|
679
|
+
locatorStrategy: resolution.locatorStrategy,
|
|
680
|
+
iframeSrcKeyword: listIframeKeyword,
|
|
681
|
+
exploration,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
specSource = appendResolvedAgentWorkflowToSpec({
|
|
685
|
+
source: specSource,
|
|
686
|
+
moduleId: input.moduleId,
|
|
687
|
+
suiteTitle: `${contract.module.label} CRUD + Export`,
|
|
688
|
+
workflow: resolved,
|
|
689
|
+
});
|
|
690
|
+
resolvedWorkflows.push(workflow.workflowId);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
needsReviewWorkflows.push(workflow.workflowId);
|
|
693
|
+
needsReviewWorkflowReasons.push({
|
|
694
|
+
workflowId: workflow.workflowId,
|
|
695
|
+
reason: formatRuntimeError(error),
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} finally {
|
|
700
|
+
if (browser) {
|
|
701
|
+
await browser.close();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
fs.writeFileSync(input.specPath, specSource, 'utf8');
|
|
706
|
+
|
|
707
|
+
return summarizeRuntimeRunnerResult({
|
|
708
|
+
resolvedWorkflows,
|
|
709
|
+
skippedWorkflows,
|
|
710
|
+
needsReviewWorkflows,
|
|
711
|
+
needsReviewWorkflowReasons,
|
|
712
|
+
specPath: input.specPath,
|
|
713
|
+
pageStructureSignals,
|
|
714
|
+
runtimePageStructureStatus,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* 解析 `--k v` 形式的命令行参数为键值表。
|
|
720
|
+
* @param argv 去除解释器与脚本路径后的参数数组
|
|
721
|
+
* @returns 参数键值表
|
|
722
|
+
*/
|
|
723
|
+
function parseRawArgs(argv: string[]): Record<string, string> {
|
|
724
|
+
const result: Record<string, string> = {};
|
|
725
|
+
const normalizedArgv = argv.filter((arg) => arg !== '--');
|
|
726
|
+
for (let index = 0; index < normalizedArgv.length; index += 2) {
|
|
727
|
+
const key = normalizedArgv[index];
|
|
728
|
+
const value = normalizedArgv[index + 1];
|
|
729
|
+
if (!key?.startsWith('--') || !value) {
|
|
730
|
+
throw new Error(`无效的参数对,靠近 ${key ?? '<empty>'}`);
|
|
731
|
+
}
|
|
732
|
+
result[key.slice(2)] = value;
|
|
733
|
+
}
|
|
734
|
+
return result;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** 校验必填参数存在,缺失时抛错。 */
|
|
738
|
+
function assertRequired(raw: Record<string, string>, required: string[]): void {
|
|
739
|
+
for (const key of required) {
|
|
740
|
+
if (!raw[key]) {
|
|
741
|
+
throw new Error(`Missing required argument --${key}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function toResolveInput(raw: Record<string, string>): GapRuntimeRunnerInput {
|
|
747
|
+
assertRequired(raw, ['contract', 'spec', 'project-dir', 'menu', 'module-id']);
|
|
748
|
+
return {
|
|
749
|
+
contractPath: raw.contract,
|
|
750
|
+
specPath: raw.spec,
|
|
751
|
+
projectDir: raw['project-dir'],
|
|
752
|
+
menuRoute: raw.menu,
|
|
753
|
+
moduleId: raw['module-id'],
|
|
754
|
+
workflowIds: raw['workflow-ids'] ? raw['workflow-ids'].split(',').filter(Boolean) : [],
|
|
755
|
+
headless: raw.headless === 'true',
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function toExploreInput(raw: Record<string, string>): GapExploreInput {
|
|
760
|
+
assertRequired(raw, ['contract', 'project-dir', 'menu', 'module-id']);
|
|
761
|
+
return {
|
|
762
|
+
contractPath: raw.contract,
|
|
763
|
+
projectDir: raw['project-dir'],
|
|
764
|
+
menuRoute: raw.menu,
|
|
765
|
+
moduleId: raw['module-id'],
|
|
766
|
+
workflowIds: raw['workflow-ids'] ? raw['workflow-ids'].split(',').filter(Boolean) : [],
|
|
767
|
+
headless: raw.headless === 'true',
|
|
768
|
+
confirmExecute: raw['confirm-execute'] === 'true',
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function toInferInput(raw: Record<string, string>): GapInferInput {
|
|
773
|
+
assertRequired(raw, ['contract', 'project-dir', 'menu', 'module-id']);
|
|
774
|
+
return {
|
|
775
|
+
contractPath: raw.contract,
|
|
776
|
+
projectDir: raw['project-dir'],
|
|
777
|
+
menuRoute: raw.menu,
|
|
778
|
+
moduleId: raw['module-id'],
|
|
779
|
+
workflowIds: raw['workflow-ids'] ? raw['workflow-ids'].split(',').filter(Boolean) : [],
|
|
780
|
+
headless: raw.headless === 'true',
|
|
781
|
+
// 引导模式默认点确定验证真实结果;显式传 --confirm-execute false 可只探到确认框。
|
|
782
|
+
confirmExecute: raw['confirm-execute'] !== 'false',
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function main(): Promise<void> {
|
|
787
|
+
const raw = parseRawArgs(process.argv.slice(2));
|
|
788
|
+
const mode = raw.mode ?? 'resolve';
|
|
789
|
+
|
|
790
|
+
if (mode === 'gaps') {
|
|
791
|
+
// gap 人工筛选 gate:纯读 contract 打印 gap 清单,不连真机。
|
|
792
|
+
assertRequired(raw, ['contract']);
|
|
793
|
+
const contract = JSON.parse(fs.readFileSync(raw.contract, 'utf8')) as CrudBusinessModuleContract;
|
|
794
|
+
const items = listGapWorkflows(contract);
|
|
795
|
+
process.stdout.write(`${JSON.stringify(items, null, 2)}\n`);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (mode === 'explore') {
|
|
800
|
+
const results = await runGapRuntimeExplore(toExploreInput(raw));
|
|
801
|
+
process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (mode === 'infer') {
|
|
806
|
+
const drafts = await runGapRuntimeInfer(toInferInput(raw));
|
|
807
|
+
process.stdout.write(`${JSON.stringify(drafts, null, 2)}\n`);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const result = await runGapRuntimeResolve(toResolveInput(raw));
|
|
812
|
+
process.stdout.write(`@@EP_STAGE_RUNTIME_SUMMARY@@\n${JSON.stringify(result, null, 2)}\n`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
816
|
+
await main();
|
|
817
|
+
}
|