@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,454 @@
|
|
|
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 "./cacheNodeModules";
|
|
12
|
+
|
|
13
|
+
const PLAYWRIGHT_DEP = "playwright@^1.50.0";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-page runtime defect captured by the Playwright audit. `console` covers
|
|
17
|
+
* `console.error` calls the page made during boot; `pageerror` covers uncaught
|
|
18
|
+
* exceptions; `navigation` covers Playwright's own `page.goto` failure
|
|
19
|
+
* (timeout, net::ERR, etc.) so the orchestrator sees the page as broken even
|
|
20
|
+
* when the runtime did not get a chance to fire a console message.
|
|
21
|
+
*/
|
|
22
|
+
export interface IRuntimeAuditDiagnostic {
|
|
23
|
+
/** Screen path the agent emitted (e.g. `/cart`, `/sales/[id]`). */
|
|
24
|
+
pagePath: string;
|
|
25
|
+
/** Category of failure. */
|
|
26
|
+
type: "console" | "pageerror" | "navigation";
|
|
27
|
+
/** Human-readable message — kept short so the retry prompt stays focused. */
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IAuditFrontendRuntimeResult {
|
|
32
|
+
/** Flat list of every diagnostic captured across all visited pages. */
|
|
33
|
+
diagnostics: IRuntimeAuditDiagnostic[];
|
|
34
|
+
/**
|
|
35
|
+
* Convenience map keyed by screen path → diagnostics on that page. Pages that
|
|
36
|
+
* booted without any console / pageerror / navigation issue are absent from
|
|
37
|
+
* the map (use {@link visited} for the full visit list).
|
|
38
|
+
*/
|
|
39
|
+
byPage: Map<string, IRuntimeAuditDiagnostic[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Screen paths that the audit attempted (`length === pages.length` on
|
|
42
|
+
* success).
|
|
43
|
+
*/
|
|
44
|
+
visited: string[];
|
|
45
|
+
/** Markdown body for `wiki/runtime-audit.md`. */
|
|
46
|
+
markdown: string;
|
|
47
|
+
/** Wall-clock duration of the whole audit (install + boot + visits + cleanup). */
|
|
48
|
+
elapsedMs: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const NEXT_DEV_BOOT_TIMEOUT_MS = 90_000;
|
|
52
|
+
const PAGE_VISIT_TIMEOUT_MS = 20_000;
|
|
53
|
+
const PAGE_SETTLE_MS = 2_500;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Boot the assembled frontend under `next dev`, launch a headless Chromium via
|
|
57
|
+
* Playwright, visit every screen the product plan declared, and return the
|
|
58
|
+
* per-page console / pageerror / navigation diagnostics. Used by the
|
|
59
|
+
* orchestrator as Phase 6 (runtime audit) — the layer above `tsc --noEmit` that
|
|
60
|
+
* catches errors typecheck cannot see (`obj?.array.method(...)`
|
|
61
|
+
* undefined-access patterns, missing client-only guards, react render
|
|
62
|
+
* crashes).
|
|
63
|
+
*
|
|
64
|
+
* Operates in `NEXT_PUBLIC_API_SIMULATE=true` so the audit does not require a
|
|
65
|
+
* live backend — the SDK serves typia-random fixture data and pages exercise
|
|
66
|
+
* their real render paths. Live-backend variants can be added later by
|
|
67
|
+
* threading the host through.
|
|
68
|
+
*
|
|
69
|
+
* Throws on infrastructure failure (`npm install` crashed, port allocation
|
|
70
|
+
* failed, Chromium download blocked). Runtime errors on the pages themselves
|
|
71
|
+
* are returned as data; the whole point of this gate is to surface them.
|
|
72
|
+
*/
|
|
73
|
+
export async function auditFrontendRuntime(
|
|
74
|
+
files: Record<string, string>,
|
|
75
|
+
pages: string[],
|
|
76
|
+
): Promise<IAuditFrontendRuntimeResult> {
|
|
77
|
+
const startedAt = Date.now();
|
|
78
|
+
const tempDir = await fs.mkdtemp(
|
|
79
|
+
path.join(os.tmpdir(), "autoview-runtime-audit-"),
|
|
80
|
+
);
|
|
81
|
+
let nextDev: ChildProcess | null = null;
|
|
82
|
+
try {
|
|
83
|
+
await writeFiles(tempDir, files);
|
|
84
|
+
const packageJson = files["package.json"];
|
|
85
|
+
const hash =
|
|
86
|
+
packageJson !== undefined
|
|
87
|
+
? createHash("sha256").update(packageJson).digest("hex").slice(0, 16)
|
|
88
|
+
: "no-package-json";
|
|
89
|
+
const cachedNodeModules =
|
|
90
|
+
packageJson !== undefined
|
|
91
|
+
? await ensureCachedNodeModules({
|
|
92
|
+
cacheNamespace: "runtime-audit",
|
|
93
|
+
hash,
|
|
94
|
+
packageJson,
|
|
95
|
+
extraDeps: [PLAYWRIGHT_DEP],
|
|
96
|
+
})
|
|
97
|
+
: null;
|
|
98
|
+
if (cachedNodeModules !== null) {
|
|
99
|
+
const linked = await tryLinkNodeModules(tempDir, cachedNodeModules);
|
|
100
|
+
if (!linked) {
|
|
101
|
+
await installDependencies(tempDir);
|
|
102
|
+
await installPlaywright(tempDir);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
await installDependencies(tempDir);
|
|
106
|
+
await installPlaywright(tempDir);
|
|
107
|
+
}
|
|
108
|
+
await installChromium(tempDir);
|
|
109
|
+
|
|
110
|
+
const port = await getFreePort();
|
|
111
|
+
nextDev = await startNextDev(tempDir, port);
|
|
112
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
113
|
+
|
|
114
|
+
await writeFile(tempDir, "scripts/.audit-runner.cjs", buildRunnerScript());
|
|
115
|
+
const stdout = await spawnCommand(
|
|
116
|
+
"node",
|
|
117
|
+
[
|
|
118
|
+
path.join(tempDir, "scripts", ".audit-runner.cjs"),
|
|
119
|
+
JSON.stringify(pages),
|
|
120
|
+
baseUrl,
|
|
121
|
+
String(PAGE_VISIT_TIMEOUT_MS),
|
|
122
|
+
String(PAGE_SETTLE_MS),
|
|
123
|
+
],
|
|
124
|
+
{ cwd: tempDir },
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const diagnostics = parseRunnerOutput(stdout);
|
|
128
|
+
const byPage = groupByPage(diagnostics);
|
|
129
|
+
return {
|
|
130
|
+
diagnostics,
|
|
131
|
+
byPage,
|
|
132
|
+
visited: [...pages],
|
|
133
|
+
markdown: renderMarkdownReport(diagnostics, byPage, pages),
|
|
134
|
+
elapsedMs: Date.now() - startedAt,
|
|
135
|
+
};
|
|
136
|
+
} finally {
|
|
137
|
+
if (nextDev !== null) await killNextDev(nextDev);
|
|
138
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* -------------------------------------------------------------------------- */
|
|
143
|
+
/* fs */
|
|
144
|
+
/* -------------------------------------------------------------------------- */
|
|
145
|
+
|
|
146
|
+
async function writeFiles(
|
|
147
|
+
root: string,
|
|
148
|
+
files: Record<string, string>,
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
for (const [relative, content] of Object.entries(files)) {
|
|
151
|
+
await writeFile(root, relative, content);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function writeFile(
|
|
156
|
+
root: string,
|
|
157
|
+
relative: string,
|
|
158
|
+
content: string,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const full = path.join(root, relative);
|
|
161
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
162
|
+
await fs.writeFile(full, content, "utf-8");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* -------------------------------------------------------------------------- */
|
|
166
|
+
/* install */
|
|
167
|
+
/* -------------------------------------------------------------------------- */
|
|
168
|
+
|
|
169
|
+
async function installDependencies(root: string): Promise<void> {
|
|
170
|
+
await spawnCommand(
|
|
171
|
+
"npm",
|
|
172
|
+
["install", "--silent", "--no-audit", "--no-fund"],
|
|
173
|
+
{ cwd: root },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function installPlaywright(root: string): Promise<void> {
|
|
178
|
+
await spawnCommand(
|
|
179
|
+
"npm",
|
|
180
|
+
[
|
|
181
|
+
"install",
|
|
182
|
+
"--silent",
|
|
183
|
+
"--no-audit",
|
|
184
|
+
"--no-fund",
|
|
185
|
+
"--no-save",
|
|
186
|
+
"playwright@^1.50.0",
|
|
187
|
+
],
|
|
188
|
+
{ cwd: root },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function installChromium(root: string): Promise<void> {
|
|
193
|
+
await spawnCommand(
|
|
194
|
+
path.join(root, "node_modules", ".bin", "playwright"),
|
|
195
|
+
["install", "chromium"],
|
|
196
|
+
{ cwd: root },
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* -------------------------------------------------------------------------- */
|
|
201
|
+
/* next dev */
|
|
202
|
+
/* -------------------------------------------------------------------------- */
|
|
203
|
+
|
|
204
|
+
function getFreePort(): Promise<number> {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
const server = createServer();
|
|
207
|
+
server.listen(0, "127.0.0.1", () => {
|
|
208
|
+
const addr = server.address();
|
|
209
|
+
if (addr === null || typeof addr === "string") {
|
|
210
|
+
server.close();
|
|
211
|
+
reject(new Error("Failed to allocate a free port"));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const port = addr.port;
|
|
215
|
+
server.close(() => resolve(port));
|
|
216
|
+
});
|
|
217
|
+
server.on("error", reject);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function startNextDev(root: string, port: number): Promise<ChildProcess> {
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
const child = spawn(
|
|
224
|
+
"node",
|
|
225
|
+
[path.join(root, "node_modules", "next", "dist", "bin", "next"), "dev"],
|
|
226
|
+
{
|
|
227
|
+
cwd: root,
|
|
228
|
+
env: {
|
|
229
|
+
...process.env,
|
|
230
|
+
PORT: String(port),
|
|
231
|
+
NEXT_PUBLIC_API_SIMULATE: "true",
|
|
232
|
+
// Force literal Authorization header so the SDK does not crash
|
|
233
|
+
// when bootstrapAuth is a no-op in simulator mode.
|
|
234
|
+
NEXT_PUBLIC_API_TOKEN: "Bearer preview",
|
|
235
|
+
},
|
|
236
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
let buffer = "";
|
|
241
|
+
let settled = false;
|
|
242
|
+
const onData = (chunk: Buffer): void => {
|
|
243
|
+
buffer += chunk.toString("utf-8");
|
|
244
|
+
if (!settled && buffer.includes("Ready in")) {
|
|
245
|
+
settled = true;
|
|
246
|
+
child.stdout?.off("data", onData);
|
|
247
|
+
child.stderr?.off("data", onData);
|
|
248
|
+
resolve(child);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
child.stdout?.on("data", onData);
|
|
252
|
+
child.stderr?.on("data", onData);
|
|
253
|
+
|
|
254
|
+
const timer = setTimeout(() => {
|
|
255
|
+
if (settled) return;
|
|
256
|
+
settled = true;
|
|
257
|
+
child.kill("SIGTERM");
|
|
258
|
+
reject(
|
|
259
|
+
new Error(
|
|
260
|
+
`next dev did not become ready within ${NEXT_DEV_BOOT_TIMEOUT_MS}ms:\n${buffer.slice(-1000)}`,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
}, NEXT_DEV_BOOT_TIMEOUT_MS);
|
|
264
|
+
|
|
265
|
+
child.on("exit", (code) => {
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
if (settled) return;
|
|
268
|
+
settled = true;
|
|
269
|
+
reject(
|
|
270
|
+
new Error(
|
|
271
|
+
`next dev exited with code ${code} before becoming ready:\n${buffer.slice(-1000)}`,
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function killNextDev(child: ChildProcess): Promise<void> {
|
|
279
|
+
if (child.exitCode !== null) return;
|
|
280
|
+
child.kill("SIGTERM");
|
|
281
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
282
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* -------------------------------------------------------------------------- */
|
|
286
|
+
/* runner script (executed inside the temp dir, requires installed playwright) */
|
|
287
|
+
/* -------------------------------------------------------------------------- */
|
|
288
|
+
|
|
289
|
+
function buildRunnerScript(): string {
|
|
290
|
+
// CommonJS so the temp dir does not need to be ESM-aware. Reads pages +
|
|
291
|
+
// baseUrl + timeouts from argv, emits a JSON array of `{ pagePath, type,
|
|
292
|
+
// message }` to stdout. Failures during navigation become `navigation`
|
|
293
|
+
// diagnostics so the caller never silently misses a broken page.
|
|
294
|
+
return `"use strict";
|
|
295
|
+
const { chromium } = require("playwright");
|
|
296
|
+
|
|
297
|
+
(async () => {
|
|
298
|
+
const pages = JSON.parse(process.argv[2]);
|
|
299
|
+
const baseUrl = process.argv[3];
|
|
300
|
+
const visitTimeout = parseInt(process.argv[4], 10);
|
|
301
|
+
const settleMs = parseInt(process.argv[5], 10);
|
|
302
|
+
const browser = await chromium.launch();
|
|
303
|
+
const context = await browser.newContext({
|
|
304
|
+
viewport: { width: 1280, height: 800 },
|
|
305
|
+
});
|
|
306
|
+
const diagnostics = [];
|
|
307
|
+
for (const pagePath of pages) {
|
|
308
|
+
const resolved = pagePath.replace(/\\[[^\\]]+\\]/g, "demo");
|
|
309
|
+
const url = baseUrl + (resolved.startsWith("/") ? resolved : "/" + resolved);
|
|
310
|
+
const page = await context.newPage();
|
|
311
|
+
const pageErrors = [];
|
|
312
|
+
page.on("console", (msg) => {
|
|
313
|
+
if (msg.type() === "error") {
|
|
314
|
+
pageErrors.push({ pagePath, type: "console", message: msg.text() });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
page.on("pageerror", (err) => {
|
|
318
|
+
pageErrors.push({ pagePath, type: "pageerror", message: err.message });
|
|
319
|
+
});
|
|
320
|
+
try {
|
|
321
|
+
// \`domcontentloaded\` instead of \`networkidle\` because typia random
|
|
322
|
+
// simulator data keeps the SDK firing background requests indefinitely
|
|
323
|
+
// on data-heavy pages — \`networkidle\` then times out at 20s on a
|
|
324
|
+
// page that already rendered cleanly. DOMContentLoaded plus the
|
|
325
|
+
// explicit settle window catches the same render errors without
|
|
326
|
+
// mistaking simulator chatter for a broken page.
|
|
327
|
+
await page.goto(url, {
|
|
328
|
+
waitUntil: "domcontentloaded",
|
|
329
|
+
timeout: visitTimeout,
|
|
330
|
+
});
|
|
331
|
+
await page.waitForTimeout(settleMs);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
const message = err && err.message ? err.message : String(err);
|
|
334
|
+
pageErrors.push({ pagePath, type: "navigation", message });
|
|
335
|
+
}
|
|
336
|
+
for (const d of pageErrors) diagnostics.push(d);
|
|
337
|
+
await page.close().catch(() => {});
|
|
338
|
+
}
|
|
339
|
+
await browser.close().catch(() => {});
|
|
340
|
+
process.stdout.write(JSON.stringify(diagnostics));
|
|
341
|
+
})().catch((err) => {
|
|
342
|
+
process.stderr.write(err && err.stack ? err.stack : String(err));
|
|
343
|
+
process.exit(1);
|
|
344
|
+
});
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseRunnerOutput(stdout: string): IRuntimeAuditDiagnostic[] {
|
|
349
|
+
const trimmed = stdout.trim();
|
|
350
|
+
if (trimmed.length === 0) return [];
|
|
351
|
+
try {
|
|
352
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
353
|
+
if (!Array.isArray(parsed)) return [];
|
|
354
|
+
return parsed.filter(
|
|
355
|
+
(entry): entry is IRuntimeAuditDiagnostic =>
|
|
356
|
+
entry !== null &&
|
|
357
|
+
typeof entry === "object" &&
|
|
358
|
+
typeof (entry as IRuntimeAuditDiagnostic).pagePath === "string" &&
|
|
359
|
+
typeof (entry as IRuntimeAuditDiagnostic).message === "string" &&
|
|
360
|
+
(entry as IRuntimeAuditDiagnostic).type !== undefined,
|
|
361
|
+
);
|
|
362
|
+
} catch {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function groupByPage(
|
|
368
|
+
diagnostics: IRuntimeAuditDiagnostic[],
|
|
369
|
+
): Map<string, IRuntimeAuditDiagnostic[]> {
|
|
370
|
+
const map = new Map<string, IRuntimeAuditDiagnostic[]>();
|
|
371
|
+
for (const d of diagnostics) {
|
|
372
|
+
const bucket = map.get(d.pagePath);
|
|
373
|
+
if (bucket === undefined) map.set(d.pagePath, [d]);
|
|
374
|
+
else bucket.push(d);
|
|
375
|
+
}
|
|
376
|
+
return map;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function renderMarkdownReport(
|
|
380
|
+
diagnostics: IRuntimeAuditDiagnostic[],
|
|
381
|
+
byPage: Map<string, IRuntimeAuditDiagnostic[]>,
|
|
382
|
+
pages: string[],
|
|
383
|
+
): string {
|
|
384
|
+
if (diagnostics.length === 0) {
|
|
385
|
+
return [
|
|
386
|
+
"# Runtime audit",
|
|
387
|
+
"",
|
|
388
|
+
`Visited **${pages.length}** page(s) under \`next dev\` (simulator mode).`,
|
|
389
|
+
"No console errors, page errors, or navigation failures detected.",
|
|
390
|
+
"",
|
|
391
|
+
].join("\n");
|
|
392
|
+
}
|
|
393
|
+
const head = [
|
|
394
|
+
"# Runtime audit",
|
|
395
|
+
"",
|
|
396
|
+
`Visited **${pages.length}** page(s) under \`next dev\` (simulator mode).`,
|
|
397
|
+
`Detected **${diagnostics.length}** diagnostic(s) across **${byPage.size}** page(s).`,
|
|
398
|
+
"",
|
|
399
|
+
];
|
|
400
|
+
const sections = [...byPage.entries()]
|
|
401
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
402
|
+
.map(([pagePath, items]) => {
|
|
403
|
+
const lines = items.map((d) => `- \`${d.type}\` — ${escape(d.message)}`);
|
|
404
|
+
return [`## \`${pagePath}\` (${items.length})`, "", ...lines, ""].join(
|
|
405
|
+
"\n",
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
return [...head, ...sections].join("\n");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function escape(message: string): string {
|
|
412
|
+
return message.replace(/`/g, "\\`").replace(/\n+/g, " ").slice(0, 500);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* -------------------------------------------------------------------------- */
|
|
416
|
+
/* spawn */
|
|
417
|
+
/* -------------------------------------------------------------------------- */
|
|
418
|
+
|
|
419
|
+
function spawnCommand(
|
|
420
|
+
cmd: string,
|
|
421
|
+
args: string[],
|
|
422
|
+
options: { cwd: string },
|
|
423
|
+
): Promise<string> {
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
const child = spawn(cmd, args, { cwd: options.cwd, stdio: "pipe" });
|
|
426
|
+
let stdout = "";
|
|
427
|
+
let stderr = "";
|
|
428
|
+
child.stdout.on("data", (chunk) => {
|
|
429
|
+
stdout += chunk.toString("utf-8");
|
|
430
|
+
});
|
|
431
|
+
child.stderr.on("data", (chunk) => {
|
|
432
|
+
stderr += chunk.toString("utf-8");
|
|
433
|
+
});
|
|
434
|
+
child.on("error", (err) => {
|
|
435
|
+
reject(
|
|
436
|
+
new Error(
|
|
437
|
+
`Failed to spawn \`${cmd} ${args.join(" ")}\`: ${err.message}`,
|
|
438
|
+
),
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
child.on("close", (code) => {
|
|
442
|
+
if (code === 0) {
|
|
443
|
+
resolve(stdout);
|
|
444
|
+
} else {
|
|
445
|
+
reject(
|
|
446
|
+
new Error(
|
|
447
|
+
`\`${cmd} ${args.slice(0, 3).join(" ")}...\` exited with code ${code}` +
|
|
448
|
+
(stderr.length > 0 ? `:\n${stderr.slice(-1500)}` : ""),
|
|
449
|
+
),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { OpenApiConverter } from "@typia/utils";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { toEndpoints } from "../../utils/toEndpoints";
|
|
5
|
+
import { buildDeterministicPlan } from "./buildDeterministicPlan";
|
|
6
|
+
|
|
7
|
+
/** Every operation must be referenced by a screen or appear as an omission. */
|
|
8
|
+
function unaccounted(document: ReturnType<typeof doc>): string[] {
|
|
9
|
+
const plan = buildDeterministicPlan(document, "user");
|
|
10
|
+
const referenced = new Set<string>();
|
|
11
|
+
for (const s of plan.screens) for (const a of s.endpoints) referenced.add(a);
|
|
12
|
+
const omitted = new Set(plan.intentionalOmissions.map((o) => o.target));
|
|
13
|
+
return toEndpoints(document)
|
|
14
|
+
.filter((e) => {
|
|
15
|
+
const acc = e.accessor.join(".");
|
|
16
|
+
const target = `${e.method.toUpperCase()} ${e.path}`;
|
|
17
|
+
return !referenced.has(acc) && !omitted.has(target);
|
|
18
|
+
})
|
|
19
|
+
.map((e) => `${e.method.toUpperCase()} ${e.path}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function doc(paths: Record<string, unknown>, schemas: Record<string, unknown> = {}) {
|
|
23
|
+
return OpenApiConverter.upgradeDocument({
|
|
24
|
+
openapi: "3.0.0",
|
|
25
|
+
info: { title: "t", version: "1.0.0" },
|
|
26
|
+
paths,
|
|
27
|
+
components: { schemas },
|
|
28
|
+
} as never);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ok = { 200: { description: "ok" } };
|
|
32
|
+
const fixture = doc(
|
|
33
|
+
{
|
|
34
|
+
"/pets": {
|
|
35
|
+
get: { operationId: "list", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Pet" } } } } } } },
|
|
36
|
+
post: { operationId: "create", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } } }, responses: ok },
|
|
37
|
+
},
|
|
38
|
+
"/pets/{petId}": {
|
|
39
|
+
get: { operationId: "get", parameters: [{ name: "petId", in: "path", required: true, schema: { type: "string" } }], responses: { 200: { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } } } } },
|
|
40
|
+
put: { operationId: "update", parameters: [{ name: "petId", in: "path", required: true, schema: { type: "string" } }], requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Pet" } } } }, responses: ok },
|
|
41
|
+
delete: { operationId: "erase", parameters: [{ name: "petId", in: "path", required: true, schema: { type: "string" } }], responses: ok },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{ Pet: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
describe("buildDeterministicPlan", () => {
|
|
48
|
+
it("produces a root landing hub + the resource's CRUD screens", () => {
|
|
49
|
+
const plan = buildDeterministicPlan(fixture, "user");
|
|
50
|
+
expect(plan.screens[0]).toMatchObject({ path: "/", uiPattern: "landing" });
|
|
51
|
+
const paths = plan.screens.map((s) => s.path);
|
|
52
|
+
// resource screens are sorted alphabetically by path (after the home hub)
|
|
53
|
+
expect(paths).toEqual(["/", "/pets", "/pets/[petsId]", "/pets/[petsId]/edit", "/pets/new"]);
|
|
54
|
+
expect(plan.navigation[0]!.primary).toContain("/");
|
|
55
|
+
expect(plan.navigation[0]!.primary).toContain("/pets");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("wires each screen to real accessors and hosts write ops on detail", () => {
|
|
59
|
+
const plan = buildDeterministicPlan(fixture, "user");
|
|
60
|
+
const detail = plan.screens.find((s) => s.path === "/pets/[petsId]")!;
|
|
61
|
+
// detail composes its GET plus the update/delete it surfaces as actions
|
|
62
|
+
expect(detail.endpoints.length).toBeGreaterThanOrEqual(2);
|
|
63
|
+
expect(plan.screens.every((s) => s.actor === "user")).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("is reproducible — same swagger yields an identical plan", () => {
|
|
67
|
+
expect(buildDeterministicPlan(fixture, "user")).toEqual(
|
|
68
|
+
buildDeterministicPlan(fixture, "user"),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("accounts for every operation — no silent drops (referenced or omitted)", () => {
|
|
73
|
+
// a resource with TWO creates + TWO deletes — the kind of duplicate-role
|
|
74
|
+
// shape that used to drop the extras on the floor.
|
|
75
|
+
const multi = doc(
|
|
76
|
+
{
|
|
77
|
+
"/orders": {
|
|
78
|
+
post: { operationId: "create", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/O" } } } }, responses: ok },
|
|
79
|
+
patch: { operationId: "index", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/O" } } } } } } },
|
|
80
|
+
},
|
|
81
|
+
"/orders/direct": {
|
|
82
|
+
post: { operationId: "direct", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/O" } } } }, responses: ok },
|
|
83
|
+
},
|
|
84
|
+
"/orders/{id}": {
|
|
85
|
+
delete: { operationId: "erase", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: ok },
|
|
86
|
+
},
|
|
87
|
+
"/orders/{id}/cancel": {
|
|
88
|
+
delete: { operationId: "cancel", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: ok },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{ O: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
92
|
+
);
|
|
93
|
+
expect(unaccounted(multi)).toEqual([]);
|
|
94
|
+
expect(unaccounted(fixture)).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("classifies non-browsable list responses structurally (no domain keywords)", () => {
|
|
98
|
+
const noisy = doc(
|
|
99
|
+
{
|
|
100
|
+
// void GET → omitted
|
|
101
|
+
"/health": { get: { operationId: "health", responses: ok } },
|
|
102
|
+
// single-object GET → singleton read → detail (not a fake table)
|
|
103
|
+
"/system": {
|
|
104
|
+
get: {
|
|
105
|
+
operationId: "system",
|
|
106
|
+
responses: {
|
|
107
|
+
200: {
|
|
108
|
+
description: "ok",
|
|
109
|
+
content: {
|
|
110
|
+
"application/json": { schema: { $ref: "#/components/schemas/Sys" } },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
// single-object mutating verb → action → omitted
|
|
117
|
+
"/refresh": {
|
|
118
|
+
patch: {
|
|
119
|
+
operationId: "refresh",
|
|
120
|
+
requestBody: {
|
|
121
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Sys" } } },
|
|
122
|
+
},
|
|
123
|
+
responses: {
|
|
124
|
+
200: {
|
|
125
|
+
description: "ok",
|
|
126
|
+
content: {
|
|
127
|
+
"application/json": { schema: { $ref: "#/components/schemas/Sys" } },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{ Sys: { type: "object", properties: { uptime: { type: "number" } }, required: ["uptime"] } },
|
|
135
|
+
);
|
|
136
|
+
const plan = buildDeterministicPlan(noisy, "user");
|
|
137
|
+
const byPath = Object.fromEntries(plan.screens.map((s) => [s.path, s]));
|
|
138
|
+
// no fake tables survived
|
|
139
|
+
expect(plan.screens.some((s) => s.uiPattern === "table")).toBe(false);
|
|
140
|
+
// singleton read became a top-level detail page, still rendering its data
|
|
141
|
+
expect(byPath["/system"]?.uiPattern).toBe("detail");
|
|
142
|
+
expect(plan.navigation[0]!.primary).toContain("/system");
|
|
143
|
+
// void + action verb were omitted, not turned into screens
|
|
144
|
+
expect(byPath["/health"]).toBeUndefined();
|
|
145
|
+
expect(byPath["/refresh"]).toBeUndefined();
|
|
146
|
+
const omitted = plan.intentionalOmissions.map((o) => o.target);
|
|
147
|
+
expect(omitted).toContain("GET /health");
|
|
148
|
+
expect(omitted).toContain("PATCH /refresh");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("omits a write-only resource (no list/detail/search to browse) as nav noise", () => {
|
|
152
|
+
const wo = doc(
|
|
153
|
+
{
|
|
154
|
+
// a real, browsable resource
|
|
155
|
+
"/users": { get: { operationId: "list", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/U" } } } } } } } },
|
|
156
|
+
// oauth2 token — POST only, no way to browse it: pure nav noise
|
|
157
|
+
"/token": { post: { operationId: "token", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/U" } } } }, responses: ok } },
|
|
158
|
+
},
|
|
159
|
+
{ U: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
160
|
+
);
|
|
161
|
+
const plan = buildDeterministicPlan(wo, "user");
|
|
162
|
+
// the browsable resource stays; the write-only one has no screen
|
|
163
|
+
expect(plan.navigation[0]!.primary).toContain("/users");
|
|
164
|
+
expect(plan.screens.some((s) => s.path.includes("token"))).toBe(false);
|
|
165
|
+
// ...but it is recorded as an omission, not silently dropped
|
|
166
|
+
expect(plan.intentionalOmissions.some((o) => o.target.includes("/token"))).toBe(true);
|
|
167
|
+
// and coverage stays total
|
|
168
|
+
expect(unaccounted(wo)).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|