@autoview/cli 0.1.0
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/LICENSE +661 -0
- package/README.md +407 -0
- package/lib/AutoViewAgent.d.ts +109 -0
- package/lib/AutoViewAgent.js +123 -0
- package/lib/AutoViewAgent.js.map +1 -0
- package/lib/agent/emitMcpServer.d.ts +15 -0
- package/lib/agent/emitMcpServer.js +157 -0
- package/lib/agent/emitMcpServer.js.map +1 -0
- package/lib/agent/emitReport.d.ts +14 -0
- package/lib/agent/emitReport.js +85 -0
- package/lib/agent/emitReport.js.map +1 -0
- package/lib/agent/toolSurface.d.ts +130 -0
- package/lib/agent/toolSurface.js +342 -0
- package/lib/agent/toolSurface.js.map +1 -0
- package/lib/agent/verifyAgentTasks.d.ts +87 -0
- package/lib/agent/verifyAgentTasks.js +126 -0
- package/lib/agent/verifyAgentTasks.js.map +1 -0
- package/lib/cli/main.d.ts +2 -0
- package/lib/cli/main.js +295 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
- package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
- package/lib/constants/AutoViewFrontendTemplate.js +46 -0
- package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
- package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
- package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
- package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
- package/lib/context/IAutoViewAgentContext.d.ts +60 -0
- package/lib/context/IAutoViewAgentContext.js +3 -0
- package/lib/context/IAutoViewAgentContext.js.map +1 -0
- package/lib/fromSwagger.d.ts +53 -0
- package/lib/fromSwagger.js +513 -0
- package/lib/fromSwagger.js.map +1 -0
- package/lib/generateDeterministic.d.ts +26 -0
- package/lib/generateDeterministic.js +75 -0
- package/lib/generateDeterministic.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +41 -0
- package/lib/index.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
- package/lib/orchestrate/orchestrateAutoView.js +491 -0
- package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
- package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
- package/lib/orchestrate/utils/HistoryMessage.js +25 -0
- package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
- package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
- package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
- package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
- package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
- package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
- package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
- package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
- package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
- package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
- package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
- package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
- package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
- package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
- package/lib/preview/renderPreview.d.ts +22 -0
- package/lib/preview/renderPreview.js +198 -0
- package/lib/preview/renderPreview.js.map +1 -0
- package/lib/typings/compiler.d.ts +39 -0
- package/lib/typings/compiler.js +3 -0
- package/lib/typings/compiler.js.map +1 -0
- package/lib/typings/events.d.ts +106 -0
- package/lib/typings/events.js +3 -0
- package/lib/typings/events.js.map +1 -0
- package/lib/typings/index.d.ts +10 -0
- package/lib/typings/index.js +27 -0
- package/lib/typings/index.js.map +1 -0
- package/lib/typings/misc.d.ts +78 -0
- package/lib/typings/misc.js +3 -0
- package/lib/typings/misc.js.map +1 -0
- package/lib/utils/ArrayUtil.d.ts +8 -0
- package/lib/utils/ArrayUtil.js +30 -0
- package/lib/utils/ArrayUtil.js.map +1 -0
- package/lib/utils/StringUtil.d.ts +11 -0
- package/lib/utils/StringUtil.js +28 -0
- package/lib/utils/StringUtil.js.map +1 -0
- package/lib/utils/classifyEndpoints.d.ts +62 -0
- package/lib/utils/classifyEndpoints.js +216 -0
- package/lib/utils/classifyEndpoints.js.map +1 -0
- package/lib/utils/endpointFilter.d.ts +26 -0
- package/lib/utils/endpointFilter.js +0 -0
- package/lib/utils/endpointFilter.js.map +1 -0
- package/lib/utils/extractFields.d.ts +85 -0
- package/lib/utils/extractFields.js +231 -0
- package/lib/utils/extractFields.js.map +1 -0
- package/lib/utils/index.d.ts +13 -0
- package/lib/utils/index.js +30 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/normalizeForNestia.d.ts +34 -0
- package/lib/utils/normalizeForNestia.js +133 -0
- package/lib/utils/normalizeForNestia.js.map +1 -0
- package/lib/utils/resourcePlan.d.ts +39 -0
- package/lib/utils/resourcePlan.js +95 -0
- package/lib/utils/resourcePlan.js.map +1 -0
- package/lib/utils/sliceDocument.d.ts +17 -0
- package/lib/utils/sliceDocument.js +114 -0
- package/lib/utils/sliceDocument.js.map +1 -0
- package/lib/utils/toEndpoints.d.ts +90 -0
- package/lib/utils/toEndpoints.js +227 -0
- package/lib/utils/toEndpoints.js.map +1 -0
- package/lib/verify/runWorkflows.d.ts +25 -0
- package/lib/verify/runWorkflows.js +366 -0
- package/lib/verify/runWorkflows.js.map +1 -0
- package/lib/verify/workflows.d.ts +53 -0
- package/lib/verify/workflows.js +107 -0
- package/lib/verify/workflows.js.map +1 -0
- package/package.json +82 -0
- package/prompts/AUTOVIEW_RENDER.md +398 -0
- package/prompts/AUTOVIEW_REVIEW.md +60 -0
- package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
- package/src/AutoViewAgent.ts +222 -0
- package/src/agent/emitMcpServer.integration.test.ts +168 -0
- package/src/agent/emitMcpServer.test.ts +51 -0
- package/src/agent/emitMcpServer.ts +178 -0
- package/src/agent/emitReport.ts +117 -0
- package/src/agent/toolSurface.test.ts +243 -0
- package/src/agent/toolSurface.ts +501 -0
- package/src/agent/verifyAgentTasks.test.ts +106 -0
- package/src/agent/verifyAgentTasks.ts +171 -0
- package/src/cli/main.ts +363 -0
- package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
- package/src/constants/AutoViewFrontendTemplate.ts +42 -0
- package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
- package/src/context/IAutoViewAgentContext.ts +84 -0
- package/src/fromSwagger.test.ts +269 -0
- package/src/fromSwagger.ts +500 -0
- package/src/generateDeterministic.test.ts +39 -0
- package/src/generateDeterministic.ts +77 -0
- package/src/index.ts +30 -0
- package/src/orchestrate/orchestrateAutoView.ts +590 -0
- package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
- package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
- package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
- package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
- package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
- package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
- package/src/orchestrate/renderNavTs.test.ts +74 -0
- package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
- package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
- package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
- package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
- package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
- package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
- package/src/orchestrate/utils/HistoryMessage.ts +41 -0
- package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
- package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
- package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
- package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
- package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
- package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
- package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
- package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
- package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
- package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
- package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
- package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
- package/src/preview/renderPreview.ts +273 -0
- package/src/typings/compiler.ts +47 -0
- package/src/typings/events.ts +155 -0
- package/src/typings/index.ts +10 -0
- package/src/typings/misc.ts +93 -0
- package/src/utils/ArrayUtil.ts +16 -0
- package/src/utils/StringUtil.ts +29 -0
- package/src/utils/classifyEndpoints.test.ts +86 -0
- package/src/utils/classifyEndpoints.ts +291 -0
- package/src/utils/endpointFilter.test.ts +50 -0
- package/src/utils/endpointFilter.ts +0 -0
- package/src/utils/extractFields.test.ts +82 -0
- package/src/utils/extractFields.ts +306 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/normalizeForNestia.test.ts +93 -0
- package/src/utils/normalizeForNestia.ts +139 -0
- package/src/utils/resourcePlan.test.ts +104 -0
- package/src/utils/resourcePlan.ts +180 -0
- package/src/utils/sliceDocument.test.ts +85 -0
- package/src/utils/sliceDocument.ts +119 -0
- package/src/utils/toEndpoints.test.ts +251 -0
- package/src/utils/toEndpoints.ts +343 -0
- package/src/verify/runWorkflows.ts +403 -0
- package/src/verify/workflows.test.ts +117 -0
- package/src/verify/workflows.ts +154 -0
- package/template/CLAUDE.md +140 -0
- package/template/Dockerfile +31 -0
- package/template/PROMPT.md +80 -0
- package/template/SANDBOX.md +70 -0
- package/template/app/api/health/route.ts +10 -0
- package/template/app/globals.css +97 -0
- package/template/app/layout.tsx +30 -0
- package/template/app/page.tsx +19 -0
- package/template/components/AppShell.tsx +114 -0
- package/template/components/auto/CatalogGrid.tsx +159 -0
- package/template/components/auto/ConfirmButton.tsx +67 -0
- package/template/components/auto/EmbeddedCollection.tsx +144 -0
- package/template/components/auto/ResourceDashboard.tsx +104 -0
- package/template/components/auto/ResourceDetail.tsx +93 -0
- package/template/components/auto/ResourceForm.tsx +235 -0
- package/template/components/auto/ResourceIcon.tsx +88 -0
- package/template/components/auto/ResourceLanding.tsx +155 -0
- package/template/components/auto/ResourceTable.tsx +223 -0
- package/template/components/auto/formatValue.tsx +186 -0
- package/template/components/auto/types.ts +42 -0
- package/template/components/ui/badge.tsx +40 -0
- package/template/components/ui/button.tsx +57 -0
- package/template/components/ui/card.tsx +86 -0
- package/template/components/ui/dialog.tsx +119 -0
- package/template/components/ui/input.tsx +23 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/pagination.tsx +117 -0
- package/template/components/ui/select.tsx +92 -0
- package/template/components/ui/sheet.tsx +135 -0
- package/template/components/ui/skeleton.tsx +15 -0
- package/template/components/ui/table.tsx +120 -0
- package/template/components/ui/tabs.tsx +55 -0
- package/template/lib/utils.ts +35 -0
- package/template/next.config.mjs +52 -0
- package/template/package.json +46 -0
- package/template/postcss.config.js +6 -0
- package/template/scripts/start-shopping-backend.sh +56 -0
- package/template/tailwind.config.ts +96 -0
- package/template/tsconfig.json +29 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "child_process";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { createServer } from "net";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ensureCachedNodeModules,
|
|
10
|
+
tryLinkNodeModules,
|
|
11
|
+
} from "../orchestrate/utils/cacheNodeModules";
|
|
12
|
+
import { IWorkflow } from "./workflows";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Verification execution layer — drive the derived user workflows in a real
|
|
16
|
+
* headless browser and report, per step, whether the expected outcome actually
|
|
17
|
+
* happened (data rendered, navigation moved), not merely that nothing errored.
|
|
18
|
+
*
|
|
19
|
+
* Boots the assembled frontend under `next dev` in simulator mode (no live
|
|
20
|
+
* backend needed — the SDK serves typia-random data so every render path runs),
|
|
21
|
+
* launches Chromium via Playwright, and walks each workflow's steps with
|
|
22
|
+
* positive DOM assertions. Returns a structured report the operator (or a fix
|
|
23
|
+
* loop) reads to know exactly which journey/step failed and why.
|
|
24
|
+
*/
|
|
25
|
+
const PLAYWRIGHT_DEP = "playwright@^1.50.0";
|
|
26
|
+
const NEXT_DEV_BOOT_TIMEOUT_MS = 120_000;
|
|
27
|
+
|
|
28
|
+
export interface IWorkflowStepResult {
|
|
29
|
+
label: string;
|
|
30
|
+
expect: string;
|
|
31
|
+
ok: boolean;
|
|
32
|
+
/** What was found — evidence for the report (row count, error text, …). */
|
|
33
|
+
detail: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IWorkflowResult {
|
|
37
|
+
id: string;
|
|
38
|
+
title: string;
|
|
39
|
+
resource: string;
|
|
40
|
+
ok: boolean;
|
|
41
|
+
steps: IWorkflowStepResult[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface IRunWorkflowsResult {
|
|
45
|
+
workflows: IWorkflowResult[];
|
|
46
|
+
/** workflows that passed every step. */
|
|
47
|
+
passed: number;
|
|
48
|
+
/** workflows with at least one failing step. */
|
|
49
|
+
failed: number;
|
|
50
|
+
markdown: string;
|
|
51
|
+
elapsedMs: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runWorkflows(
|
|
55
|
+
files: Record<string, string>,
|
|
56
|
+
workflows: IWorkflow[],
|
|
57
|
+
): Promise<IRunWorkflowsResult> {
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "autoview-verify-"));
|
|
60
|
+
let nextDev: ChildProcess | null = null;
|
|
61
|
+
try {
|
|
62
|
+
await writeFiles(tempDir, files);
|
|
63
|
+
await installEverything(tempDir, files["package.json"]);
|
|
64
|
+
|
|
65
|
+
const port = await getFreePort();
|
|
66
|
+
nextDev = await startNextDev(tempDir, port);
|
|
67
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
68
|
+
|
|
69
|
+
await writeFile(tempDir, "scripts/.verify-runner.cjs", buildRunnerScript());
|
|
70
|
+
const stdout = await spawnCommand(
|
|
71
|
+
"node",
|
|
72
|
+
[
|
|
73
|
+
path.join(tempDir, "scripts", ".verify-runner.cjs"),
|
|
74
|
+
JSON.stringify(workflows),
|
|
75
|
+
baseUrl,
|
|
76
|
+
],
|
|
77
|
+
{ cwd: tempDir },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const results = parseResults(stdout, workflows);
|
|
81
|
+
const passed = results.filter((w) => w.ok).length;
|
|
82
|
+
return {
|
|
83
|
+
workflows: results,
|
|
84
|
+
passed,
|
|
85
|
+
failed: results.length - passed,
|
|
86
|
+
markdown: renderMarkdown(results),
|
|
87
|
+
elapsedMs: Date.now() - startedAt,
|
|
88
|
+
};
|
|
89
|
+
} finally {
|
|
90
|
+
if (nextDev !== null) await killNextDev(nextDev);
|
|
91
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* -------------------------------------------------------------------------- */
|
|
96
|
+
/* runner script (runs inside the temp dir against the installed playwright) */
|
|
97
|
+
/* -------------------------------------------------------------------------- */
|
|
98
|
+
|
|
99
|
+
function buildRunnerScript(): string {
|
|
100
|
+
// CJS, executed in the temp dir. For each workflow step it performs the
|
|
101
|
+
// action, lets the client fetch settle, then asserts the POSITIVE outcome for
|
|
102
|
+
// the expected page kind. A `console.error` / uncaught exception during a
|
|
103
|
+
// step fails it too. Emits a JSON array of workflow results to stdout.
|
|
104
|
+
return String.raw`"use strict";
|
|
105
|
+
const { chromium } = require("playwright");
|
|
106
|
+
|
|
107
|
+
const SETTLE_MS = 4500;
|
|
108
|
+
const VISIT_TIMEOUT = 120000;
|
|
109
|
+
|
|
110
|
+
async function assertExpect(page, expect) {
|
|
111
|
+
// returns { ok, detail }
|
|
112
|
+
const errorCard = await page.locator('text=/Couldn.t load/i').count();
|
|
113
|
+
if (expect === "landing") {
|
|
114
|
+
const links = await page.locator('main a[href]').count();
|
|
115
|
+
return { ok: links > 0, detail: links + " links" };
|
|
116
|
+
}
|
|
117
|
+
if (expect === "table") {
|
|
118
|
+
const rows = await page.locator('table tbody tr').count();
|
|
119
|
+
const empty = await page.locator('text=/came back empty|no .* yet|nested collection/i').count();
|
|
120
|
+
if (errorCard > 0) return { ok: false, detail: "error card shown" };
|
|
121
|
+
if (rows > 0) return { ok: true, detail: rows + " rows" };
|
|
122
|
+
if (empty > 0) return { ok: true, detail: "empty state" };
|
|
123
|
+
return { ok: false, detail: "no rows, no empty state, no error (stuck?)" };
|
|
124
|
+
}
|
|
125
|
+
if (expect === "detail") {
|
|
126
|
+
const fields = await page.locator('dt').count();
|
|
127
|
+
if (errorCard > 0) return { ok: false, detail: "error card shown" };
|
|
128
|
+
if (fields > 0) return { ok: true, detail: fields + " fields" };
|
|
129
|
+
return { ok: false, detail: "no fields rendered" };
|
|
130
|
+
}
|
|
131
|
+
if (expect === "form") {
|
|
132
|
+
const inputs = await page.locator('input, select, textarea, button[type=submit]').count();
|
|
133
|
+
return { ok: inputs > 0, detail: inputs + " inputs" };
|
|
134
|
+
}
|
|
135
|
+
return { ok: false, detail: "unknown expect" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
(async () => {
|
|
139
|
+
const workflows = JSON.parse(process.argv[2]);
|
|
140
|
+
const baseUrl = process.argv[3];
|
|
141
|
+
const browser = await chromium.launch();
|
|
142
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
143
|
+
|
|
144
|
+
// Pre-warm: the first SDK-backed page triggers a one-time on-demand compile of
|
|
145
|
+
// the generated SDK module graph, which on a large API can exceed a normal
|
|
146
|
+
// navigation timeout (~100s+). That is a dev-server cost, not an app defect —
|
|
147
|
+
// warm it once up front (generous budget) so per-workflow navigations are
|
|
148
|
+
// fast and never false-fail on a cold compile.
|
|
149
|
+
let warmPath = null;
|
|
150
|
+
for (const wf of workflows) {
|
|
151
|
+
for (const s of wf.steps) {
|
|
152
|
+
if (s.action.type === "visit" && s.action.path !== "/") { warmPath = s.action.path; break; }
|
|
153
|
+
}
|
|
154
|
+
if (warmPath) break;
|
|
155
|
+
}
|
|
156
|
+
if (warmPath) {
|
|
157
|
+
const wp = await ctx.newPage();
|
|
158
|
+
try { await wp.goto(baseUrl + warmPath, { waitUntil: "domcontentloaded", timeout: 300000 }); } catch (e) {}
|
|
159
|
+
await wp.waitForTimeout(2000).catch(() => {});
|
|
160
|
+
await wp.close().catch(() => {});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const wf of workflows) {
|
|
165
|
+
const page = await ctx.newPage();
|
|
166
|
+
let pageError = null;
|
|
167
|
+
page.on("pageerror", (e) => { pageError = e.message; });
|
|
168
|
+
page.on("console", (m) => { if (m.type() === "error" && !pageError) pageError = "console: " + m.text(); });
|
|
169
|
+
const steps = [];
|
|
170
|
+
let wfOk = true;
|
|
171
|
+
for (const step of wf.steps) {
|
|
172
|
+
let res = { ok: false, detail: "" };
|
|
173
|
+
try {
|
|
174
|
+
const a = step.action;
|
|
175
|
+
if (a.type === "visit") {
|
|
176
|
+
await page.goto(baseUrl + a.path, { waitUntil: "domcontentloaded", timeout: VISIT_TIMEOUT });
|
|
177
|
+
} else if (a.type === "clickFirstRow") {
|
|
178
|
+
const view = page.locator('table tbody tr a:has-text("View")').first();
|
|
179
|
+
if (await view.count() === 0) {
|
|
180
|
+
// No row to open. An empty list is the app behaving correctly (the
|
|
181
|
+
// simulator simply returned zero rows), not a failure — skip the
|
|
182
|
+
// detail step. A real failure (error card / stuck page) is caught by
|
|
183
|
+
// the preceding list step.
|
|
184
|
+
const empty = await page.locator('text=/came back empty|no .* yet|nested collection/i').count();
|
|
185
|
+
const detail = empty > 0 ? "skipped — list is empty" : "no row to open (list did not render)";
|
|
186
|
+
steps.push({ label: step.label, expect: step.expect, ok: empty > 0, detail });
|
|
187
|
+
if (empty === 0) wfOk = false;
|
|
188
|
+
pageError = null;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
await view.click();
|
|
192
|
+
} else if (a.type === "clickLink") {
|
|
193
|
+
await page.locator('a:has-text("' + a.text + '")').first().click();
|
|
194
|
+
}
|
|
195
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
196
|
+
res = await assertExpect(page, step.expect);
|
|
197
|
+
if (pageError) { res = { ok: false, detail: "runtime error: " + String(pageError).slice(0, 120) }; }
|
|
198
|
+
} catch (e) {
|
|
199
|
+
res = { ok: false, detail: "step threw: " + String(e && e.message ? e.message : e).slice(0, 120) };
|
|
200
|
+
}
|
|
201
|
+
steps.push({ label: step.label, expect: step.expect, ok: res.ok, detail: res.detail });
|
|
202
|
+
if (!res.ok) wfOk = false;
|
|
203
|
+
pageError = null;
|
|
204
|
+
}
|
|
205
|
+
out.push({ id: wf.id, title: wf.title, resource: wf.resource, ok: wfOk, steps });
|
|
206
|
+
await page.close().catch(() => {});
|
|
207
|
+
}
|
|
208
|
+
await browser.close().catch(() => {});
|
|
209
|
+
process.stdout.write(JSON.stringify(out));
|
|
210
|
+
})().catch((err) => {
|
|
211
|
+
process.stderr.write(err && err.stack ? err.stack : String(err));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseResults(
|
|
218
|
+
stdout: string,
|
|
219
|
+
workflows: IWorkflow[],
|
|
220
|
+
): IWorkflowResult[] {
|
|
221
|
+
const trimmed = stdout.trim();
|
|
222
|
+
if (trimmed.length === 0) {
|
|
223
|
+
return workflows.map((w) => ({
|
|
224
|
+
id: w.id,
|
|
225
|
+
title: w.title,
|
|
226
|
+
resource: w.resource,
|
|
227
|
+
ok: false,
|
|
228
|
+
steps: w.steps.map((s) => ({
|
|
229
|
+
label: s.label,
|
|
230
|
+
expect: s.expect,
|
|
231
|
+
ok: false,
|
|
232
|
+
detail: "no runner output",
|
|
233
|
+
})),
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
238
|
+
if (!Array.isArray(parsed)) throw new Error("not an array");
|
|
239
|
+
return parsed as IWorkflowResult[];
|
|
240
|
+
} catch {
|
|
241
|
+
return workflows.map((w) => ({
|
|
242
|
+
id: w.id,
|
|
243
|
+
title: w.title,
|
|
244
|
+
resource: w.resource,
|
|
245
|
+
ok: false,
|
|
246
|
+
steps: [{ label: "runner", expect: "-", ok: false, detail: "unparseable runner output" }],
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderMarkdown(results: IWorkflowResult[]): string {
|
|
252
|
+
const passed = results.filter((w) => w.ok).length;
|
|
253
|
+
const lines: string[] = [
|
|
254
|
+
"# Workflow verification",
|
|
255
|
+
"",
|
|
256
|
+
`Ran **${results.length}** user workflow(s) in a real headless browser (simulator mode).`,
|
|
257
|
+
`**${passed} passed**, **${results.length - passed} failed**.`,
|
|
258
|
+
"",
|
|
259
|
+
];
|
|
260
|
+
for (const wf of results) {
|
|
261
|
+
lines.push(`## ${wf.ok ? "✅" : "❌"} ${wf.title}`, "");
|
|
262
|
+
for (const step of wf.steps) {
|
|
263
|
+
lines.push(`- ${step.ok ? "✓" : "✗"} ${step.label} → expect \`${step.expect}\` — ${step.detail}`);
|
|
264
|
+
}
|
|
265
|
+
lines.push("");
|
|
266
|
+
}
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* -------------------------------------------------------------------------- */
|
|
271
|
+
/* dev-server lifecycle (mirrors auditFrontendRuntime's proven boot) */
|
|
272
|
+
/* -------------------------------------------------------------------------- */
|
|
273
|
+
|
|
274
|
+
async function installEverything(
|
|
275
|
+
root: string,
|
|
276
|
+
packageJson: string | undefined,
|
|
277
|
+
): Promise<void> {
|
|
278
|
+
const hash =
|
|
279
|
+
packageJson !== undefined
|
|
280
|
+
? createHash("sha256").update(packageJson).digest("hex").slice(0, 16)
|
|
281
|
+
: "no-package-json";
|
|
282
|
+
const cached =
|
|
283
|
+
packageJson !== undefined
|
|
284
|
+
? await ensureCachedNodeModules({
|
|
285
|
+
cacheNamespace: "runtime-audit",
|
|
286
|
+
hash,
|
|
287
|
+
packageJson,
|
|
288
|
+
extraDeps: [PLAYWRIGHT_DEP],
|
|
289
|
+
})
|
|
290
|
+
: null;
|
|
291
|
+
if (cached !== null && (await tryLinkNodeModules(root, cached))) {
|
|
292
|
+
await installChromium(root);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
await spawnCommand("npm", ["install", "--silent", "--no-audit", "--no-fund"], { cwd: root });
|
|
296
|
+
await spawnCommand(
|
|
297
|
+
"npm",
|
|
298
|
+
["install", "--silent", "--no-audit", "--no-fund", "--no-save", PLAYWRIGHT_DEP],
|
|
299
|
+
{ cwd: root },
|
|
300
|
+
);
|
|
301
|
+
await installChromium(root);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function installChromium(root: string): Promise<void> {
|
|
305
|
+
await spawnCommand(path.join(root, "node_modules", ".bin", "playwright"), ["install", "chromium"], {
|
|
306
|
+
cwd: root,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function writeFiles(root: string, files: Record<string, string>): Promise<void> {
|
|
311
|
+
for (const [relative, content] of Object.entries(files)) {
|
|
312
|
+
await writeFile(root, relative, content);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function writeFile(root: string, relative: string, content: string): Promise<void> {
|
|
317
|
+
const full = path.join(root, relative);
|
|
318
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
319
|
+
await fs.writeFile(full, content, "utf-8");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getFreePort(): Promise<number> {
|
|
323
|
+
return new Promise((resolve, reject) => {
|
|
324
|
+
const server = createServer();
|
|
325
|
+
server.listen(0, "127.0.0.1", () => {
|
|
326
|
+
const addr = server.address();
|
|
327
|
+
if (addr === null || typeof addr === "string") {
|
|
328
|
+
server.close();
|
|
329
|
+
reject(new Error("Failed to allocate a free port"));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const port = addr.port;
|
|
333
|
+
server.close(() => resolve(port));
|
|
334
|
+
});
|
|
335
|
+
server.on("error", reject);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function startNextDev(root: string, port: number): Promise<ChildProcess> {
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
const child = spawn(
|
|
342
|
+
"node",
|
|
343
|
+
[path.join(root, "node_modules", "next", "dist", "bin", "next"), "dev"],
|
|
344
|
+
{
|
|
345
|
+
cwd: root,
|
|
346
|
+
env: {
|
|
347
|
+
...process.env,
|
|
348
|
+
PORT: String(port),
|
|
349
|
+
NEXT_PUBLIC_API_SIMULATE: "true",
|
|
350
|
+
NEXT_PUBLIC_API_TOKEN: "Bearer preview",
|
|
351
|
+
},
|
|
352
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
let buffer = "";
|
|
356
|
+
let settled = false;
|
|
357
|
+
const onData = (chunk: Buffer): void => {
|
|
358
|
+
buffer += chunk.toString("utf-8");
|
|
359
|
+
if (!settled && buffer.includes("Ready in")) {
|
|
360
|
+
settled = true;
|
|
361
|
+
child.stdout?.off("data", onData);
|
|
362
|
+
child.stderr?.off("data", onData);
|
|
363
|
+
resolve(child);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
child.stdout?.on("data", onData);
|
|
367
|
+
child.stderr?.on("data", onData);
|
|
368
|
+
const timer = setTimeout(() => {
|
|
369
|
+
if (settled) return;
|
|
370
|
+
settled = true;
|
|
371
|
+
child.kill("SIGTERM");
|
|
372
|
+
reject(new Error(`next dev did not become ready within ${NEXT_DEV_BOOT_TIMEOUT_MS}ms:\n${buffer.slice(-800)}`));
|
|
373
|
+
}, NEXT_DEV_BOOT_TIMEOUT_MS);
|
|
374
|
+
child.on("exit", (code) => {
|
|
375
|
+
clearTimeout(timer);
|
|
376
|
+
if (settled) return;
|
|
377
|
+
settled = true;
|
|
378
|
+
reject(new Error(`next dev exited with code ${code} before ready:\n${buffer.slice(-800)}`));
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function killNextDev(child: ChildProcess): Promise<void> {
|
|
384
|
+
if (child.exitCode !== null) return;
|
|
385
|
+
child.kill("SIGTERM");
|
|
386
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
387
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function spawnCommand(cmd: string, args: string[], options: { cwd: string }): Promise<string> {
|
|
391
|
+
return new Promise((resolve, reject) => {
|
|
392
|
+
const child = spawn(cmd, args, { cwd: options.cwd, stdio: "pipe" });
|
|
393
|
+
let stdout = "";
|
|
394
|
+
let stderr = "";
|
|
395
|
+
child.stdout.on("data", (c) => (stdout += c.toString("utf-8")));
|
|
396
|
+
child.stderr.on("data", (c) => (stderr += c.toString("utf-8")));
|
|
397
|
+
child.on("error", (err) => reject(new Error(`Failed to spawn \`${cmd}\`: ${err.message}`)));
|
|
398
|
+
child.on("close", (code) => {
|
|
399
|
+
if (code === 0) resolve(stdout);
|
|
400
|
+
else reject(new Error(`\`${cmd} ${args.slice(0, 2).join(" ")}…\` exited ${code}${stderr ? `:\n${stderr.slice(-1200)}` : ""}`));
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { OpenApiConverter } from "@typia/utils";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { buildDeterministicPlan } from "../orchestrate/utils/buildDeterministicPlan";
|
|
5
|
+
import { deriveWorkflows } from "./workflows";
|
|
6
|
+
|
|
7
|
+
function doc(paths: Record<string, unknown>, schemas: Record<string, unknown> = {}) {
|
|
8
|
+
return OpenApiConverter.upgradeDocument({
|
|
9
|
+
openapi: "3.0.0",
|
|
10
|
+
info: { title: "t", version: "1.0.0" },
|
|
11
|
+
paths,
|
|
12
|
+
components: { schemas },
|
|
13
|
+
} as never);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ok = { 200: { description: "ok" } };
|
|
17
|
+
const fixture = doc(
|
|
18
|
+
{
|
|
19
|
+
"/pets": {
|
|
20
|
+
get: {
|
|
21
|
+
operationId: "index",
|
|
22
|
+
responses: {
|
|
23
|
+
200: {
|
|
24
|
+
description: "ok",
|
|
25
|
+
content: {
|
|
26
|
+
"application/json": {
|
|
27
|
+
schema: { type: "array", items: { $ref: "#/components/schemas/Pet" } },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
"/pets/{petId}": {
|
|
35
|
+
get: {
|
|
36
|
+
operationId: "at",
|
|
37
|
+
parameters: [{ name: "petId", in: "path", required: true, schema: { type: "string" } }],
|
|
38
|
+
responses: {
|
|
39
|
+
200: { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } } },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{ Pet: { type: "object", properties: { id: { type: "string" }, name: { type: "string" } } } },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
describe("deriveWorkflows", () => {
|
|
48
|
+
it("derives a home workflow plus a browse-and-open journey per resource", () => {
|
|
49
|
+
const plan = buildDeterministicPlan(fixture, "user");
|
|
50
|
+
const flows = deriveWorkflows(plan);
|
|
51
|
+
|
|
52
|
+
const home = flows.find((f) => f.id === "home")!;
|
|
53
|
+
expect(home.steps).toHaveLength(1);
|
|
54
|
+
expect(home.steps[0]!.expect).toBe("landing");
|
|
55
|
+
|
|
56
|
+
const pets = flows.find((f) => f.title === "Browse Pets")!;
|
|
57
|
+
expect(pets).toBeDefined();
|
|
58
|
+
// list step then open-a-record step (a detail screen exists)
|
|
59
|
+
expect(pets.steps.map((s) => s.expect)).toEqual(["table", "detail"]);
|
|
60
|
+
expect(pets.steps[0]!.action).toEqual({ type: "visit", path: "/pets" });
|
|
61
|
+
expect(pets.steps[1]!.action).toEqual({ type: "clickFirstRow" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("a list with no detail screen yields a single list step (no row click)", () => {
|
|
65
|
+
const readonly = doc({
|
|
66
|
+
"/inventory": {
|
|
67
|
+
get: {
|
|
68
|
+
operationId: "index",
|
|
69
|
+
responses: {
|
|
70
|
+
200: {
|
|
71
|
+
description: "ok",
|
|
72
|
+
content: {
|
|
73
|
+
"application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/I" } } },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}, { I: { type: "object", properties: { count: { type: "integer" } } } });
|
|
80
|
+
const flows = deriveWorkflows(buildDeterministicPlan(readonly, "user"));
|
|
81
|
+
const inv = flows.find((f) => f.title === "Browse Inventory")!;
|
|
82
|
+
expect(inv.steps.map((s) => s.expect)).toEqual(["table"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("derives a form workflow for a top-level create endpoint", () => {
|
|
86
|
+
const creatable = doc(
|
|
87
|
+
{
|
|
88
|
+
"/pets": {
|
|
89
|
+
get: {
|
|
90
|
+
operationId: "index",
|
|
91
|
+
responses: {
|
|
92
|
+
200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Pet" } } } } },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
post: {
|
|
96
|
+
operationId: "create",
|
|
97
|
+
requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } } },
|
|
98
|
+
responses: ok,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{ Pet: { type: "object", properties: { name: { type: "string" } }, required: ["name"] } },
|
|
103
|
+
);
|
|
104
|
+
const flows = deriveWorkflows(buildDeterministicPlan(creatable, "user"));
|
|
105
|
+
const form = flows.find((f) => f.title.includes("form"))!;
|
|
106
|
+
expect(form).toBeDefined();
|
|
107
|
+
expect(form.steps).toHaveLength(1);
|
|
108
|
+
expect(form.steps[0]!.expect).toBe("form");
|
|
109
|
+
expect(form.steps[0]!.action).toEqual({ type: "visit", path: "/pets/new" });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("is deterministic", () => {
|
|
113
|
+
const a = deriveWorkflows(buildDeterministicPlan(fixture, "user"));
|
|
114
|
+
const b = deriveWorkflows(buildDeterministicPlan(fixture, "user"));
|
|
115
|
+
expect(a).toEqual(b);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { IAutoViewProductPlan } from "../orchestrate/structures/IAutoViewProductPlan";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The verification layer — "정확하게 검증한다".
|
|
5
|
+
*
|
|
6
|
+
* A generated frontend that typechecks is not proven to work: a user has to be
|
|
7
|
+
* able to open it, see real data, and move through it. This module derives, from
|
|
8
|
+
* the deterministic product plan, the set of USER WORKFLOWS that a real visitor
|
|
9
|
+
* would perform — each a short journey with an explicit positive expectation at
|
|
10
|
+
* every step (the table actually rendered rows or an honest empty state; the
|
|
11
|
+
* detail actually showed fields; navigation actually moved). The execution layer
|
|
12
|
+
* ({@link ../verify/runWorkflows}) drives these in a real headless browser and
|
|
13
|
+
* reports pass/fail per step, so "it works" is demonstrated, not asserted.
|
|
14
|
+
*
|
|
15
|
+
* Pure and deterministic: same plan → same workflows, no browser needed. This is
|
|
16
|
+
* the spec of WHAT gets verified.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** What a page must be after a step — drives the runtime DOM assertion. */
|
|
20
|
+
export type WorkflowExpect = "landing" | "table" | "detail" | "form";
|
|
21
|
+
|
|
22
|
+
/** One user action plus the outcome it must produce. */
|
|
23
|
+
export interface IWorkflowStep {
|
|
24
|
+
action:
|
|
25
|
+
| { type: "visit"; path: string }
|
|
26
|
+
| { type: "clickFirstRow" }
|
|
27
|
+
| { type: "clickLink"; text: string };
|
|
28
|
+
expect: WorkflowExpect;
|
|
29
|
+
/** Human description, shown in the report. */
|
|
30
|
+
label: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One end-to-end user journey through a resource. */
|
|
34
|
+
export interface IWorkflow {
|
|
35
|
+
/** Stable slug (route-derived). */
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
/** Resource the journey covers, for grouping the report. */
|
|
39
|
+
resource: string;
|
|
40
|
+
steps: IWorkflowStep[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function slug(path: string): string {
|
|
44
|
+
const cleaned = path.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
45
|
+
return cleaned.length > 0 ? cleaned : "home";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A top-level resource screen earns its own browse workflow. */
|
|
49
|
+
function isTopLevelTable(screen: IAutoViewProductPlan.IScreen): boolean {
|
|
50
|
+
if (screen.depth !== undefined && screen.depth !== 1) return false;
|
|
51
|
+
if (screen.path.includes("[")) return false;
|
|
52
|
+
return screen.uiPattern === "table" || screen.uiPattern === "catalog";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Derive the user workflows to verify from the product plan.
|
|
57
|
+
*
|
|
58
|
+
* - Home: the landing hub must open and present its resource links.
|
|
59
|
+
* - Per top-level resource: visit the list (it must render a table or an honest
|
|
60
|
+
* empty state, not an error), then open the first row's detail (it must render
|
|
61
|
+
* the record's fields). This is the core read journey — the one a user does
|
|
62
|
+
* first and the one that exercises data fetch + render + navigation together.
|
|
63
|
+
* - Per top-level singleton read (`/system`): visit it; the detail must render.
|
|
64
|
+
*/
|
|
65
|
+
export function deriveWorkflows(plan: IAutoViewProductPlan): IWorkflow[] {
|
|
66
|
+
const workflows: IWorkflow[] = [];
|
|
67
|
+
const screens = plan.screens;
|
|
68
|
+
|
|
69
|
+
const landing = screens.find((s) => s.uiPattern === "landing");
|
|
70
|
+
if (landing !== undefined) {
|
|
71
|
+
workflows.push({
|
|
72
|
+
id: "home",
|
|
73
|
+
title: "Home hub loads and links to resources",
|
|
74
|
+
resource: "",
|
|
75
|
+
steps: [
|
|
76
|
+
{ action: { type: "visit", path: "/" }, expect: "landing", label: "Open home" },
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const screen of screens) {
|
|
82
|
+
if (isTopLevelTable(screen)) {
|
|
83
|
+
const detail = screens.find(
|
|
84
|
+
(s) => s.uiPattern === "detail" && s.path.startsWith(`${screen.path}/[`),
|
|
85
|
+
);
|
|
86
|
+
const steps: IWorkflowStep[] = [
|
|
87
|
+
{
|
|
88
|
+
action: { type: "visit", path: screen.path },
|
|
89
|
+
expect: "table",
|
|
90
|
+
label: `Open ${screen.title} list`,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
if (detail !== undefined) {
|
|
94
|
+
steps.push({
|
|
95
|
+
action: { type: "clickFirstRow" },
|
|
96
|
+
expect: "detail",
|
|
97
|
+
label: `Open a ${screen.title} record`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
workflows.push({
|
|
101
|
+
id: slug(screen.path),
|
|
102
|
+
title: `Browse ${screen.title}`,
|
|
103
|
+
resource: screen.title,
|
|
104
|
+
steps,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Top-level create form (`/coupons/new`): verify it opens and renders its
|
|
109
|
+
// inputs. Forms do not fetch, so this is a fast, reliable structural check
|
|
110
|
+
// that the request-body schema produced a usable form.
|
|
111
|
+
if (
|
|
112
|
+
screen.uiPattern === "form" &&
|
|
113
|
+
screen.path.endsWith("/new") &&
|
|
114
|
+
!screen.path.slice(0, -4).includes("[") &&
|
|
115
|
+
(screen.depth === undefined || screen.depth === 1)
|
|
116
|
+
) {
|
|
117
|
+
workflows.push({
|
|
118
|
+
id: slug(screen.path),
|
|
119
|
+
title: `Open the ${screen.title} form`,
|
|
120
|
+
resource: screen.title,
|
|
121
|
+
steps: [
|
|
122
|
+
{
|
|
123
|
+
action: { type: "visit", path: screen.path },
|
|
124
|
+
expect: "form",
|
|
125
|
+
label: `Open ${screen.title}`,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Top-level singleton read (`/system`, `/performance`): a bracket-less
|
|
132
|
+
// detail page. Verify it opens and renders.
|
|
133
|
+
if (
|
|
134
|
+
screen.uiPattern === "detail" &&
|
|
135
|
+
!screen.path.includes("[") &&
|
|
136
|
+
(screen.depth === undefined || screen.depth === 1)
|
|
137
|
+
) {
|
|
138
|
+
workflows.push({
|
|
139
|
+
id: slug(screen.path),
|
|
140
|
+
title: `View ${screen.title}`,
|
|
141
|
+
resource: screen.title,
|
|
142
|
+
steps: [
|
|
143
|
+
{
|
|
144
|
+
action: { type: "visit", path: screen.path },
|
|
145
|
+
expect: "detail",
|
|
146
|
+
label: `Open ${screen.title}`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return workflows;
|
|
154
|
+
}
|