forgeos 0.1.0-alpha.4 → 0.1.0-alpha.41
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/.npmignore +10 -0
- package/AGENTS.md +142 -90
- package/CHANGELOG.md +433 -0
- package/LICENSE +21 -0
- package/README.md +67 -8
- package/adapters/go/README.md +23 -0
- package/adapters/go/go.mod +3 -0
- package/adapters/go/http.go +149 -0
- package/adapters/go/registry.go +234 -0
- package/adapters/go/types.go +136 -0
- package/adapters/java/README.md +68 -0
- package/adapters/java/pom.xml +34 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
- package/adapters/java-spring-boot-starter/README.md +32 -0
- package/adapters/java-spring-boot-starter/pom.xml +36 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
- package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
- package/bin/forge.mjs +18 -0
- package/docs/cair-protocol.md +103 -0
- package/docs/changelog.md +548 -0
- package/docs/forge-protocol.md +189 -0
- package/examples/go-billing/go.mod +7 -0
- package/examples/go-billing/main.go +120 -0
- package/examples/java-billing/pom.xml +52 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
- package/package.json +31 -3
- package/schemas/forge-manifest.schema.json +57 -0
- package/src/forge/_generated/releaseManifest.json +1 -2
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/index.ts +1584 -125
- package/src/forge/agent-adapters/types.ts +224 -1
- package/src/forge/agent-memory/bridge.ts +1333 -0
- package/src/forge/agent-memory/context-pack.ts +277 -0
- package/src/forge/agent-memory/hook-runner.ts +312 -0
- package/src/forge/agent-memory/mcp.ts +224 -0
- package/src/forge/agent-memory/normalize.ts +498 -0
- package/src/forge/agent-memory/redaction.ts +103 -0
- package/src/forge/agent-memory/sources/claude-code.ts +51 -0
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +273 -0
- package/src/forge/agent-memory/sources/codex.ts +119 -0
- package/src/forge/agent-memory/sources/cursor.ts +35 -0
- package/src/forge/agent-memory/types.ts +204 -0
- package/src/forge/bench.ts +248 -0
- package/src/forge/brownfield-import/index.ts +801 -0
- package/src/forge/brownfield-import/types.ts +127 -0
- package/src/forge/cair/action-journal.ts +61 -0
- package/src/forge/cair/action-parser.ts +314 -0
- package/src/forge/cair/action-validator.ts +40 -0
- package/src/forge/cair/actions.ts +1818 -0
- package/src/forge/cair/format.ts +77 -0
- package/src/forge/cair/index.ts +106 -0
- package/src/forge/cair/query.ts +478 -0
- package/src/forge/cair/snapshot.ts +315 -0
- package/src/forge/cair/types.ts +248 -0
- package/src/forge/cli/ai.ts +339 -21
- package/src/forge/cli/auth.ts +348 -10
- package/src/forge/cli/authmd.ts +441 -0
- package/src/forge/cli/baseline.ts +112 -0
- package/src/forge/cli/build.ts +20 -4
- package/src/forge/cli/changed.ts +437 -0
- package/src/forge/cli/codex-app-server.ts +877 -0
- package/src/forge/cli/commands.ts +1620 -20
- package/src/forge/cli/db.ts +209 -2
- package/src/forge/cli/deploy.ts +887 -0
- package/src/forge/cli/deps.ts +82 -13
- package/src/forge/cli/dev.ts +961 -46
- package/src/forge/cli/docs.ts +265 -0
- package/src/forge/cli/doctor.ts +297 -0
- package/src/forge/cli/field-test.ts +329 -0
- package/src/forge/cli/handoff.ts +336 -0
- package/src/forge/cli/index.ts +1 -0
- package/src/forge/cli/last-run.ts +84 -0
- package/src/forge/cli/main.ts +107 -3
- package/src/forge/cli/new.ts +152 -13
- package/src/forge/cli/next-actions.ts +23 -0
- package/src/forge/cli/output.ts +389 -6
- package/src/forge/cli/parse.ts +1050 -34
- package/src/forge/cli/progress.ts +51 -0
- package/src/forge/cli/query.ts +32 -0
- package/src/forge/cli/release.ts +35 -11
- package/src/forge/cli/rls.ts +43 -4
- package/src/forge/cli/run.ts +41 -0
- package/src/forge/cli/security.ts +112 -0
- package/src/forge/cli/self-host.ts +56 -14
- package/src/forge/cli/studio.ts +2218 -0
- package/src/forge/cli/verify.ts +1455 -32
- package/src/forge/cli/windows.ts +23 -0
- package/src/forge/cli/workos.ts +1020 -0
- package/src/forge/compiler/agent-contract/build.ts +670 -64
- package/src/forge/compiler/agent-contract/types.ts +45 -2
- package/src/forge/compiler/ai-registry/parse.ts +71 -2
- package/src/forge/compiler/api-surface/build.ts +47 -0
- package/src/forge/compiler/app-graph/build.ts +112 -9
- package/src/forge/compiler/app-graph/extract.ts +107 -0
- package/src/forge/compiler/app-graph/module-graph.ts +73 -78
- package/src/forge/compiler/app-graph/parser.ts +24 -24
- package/src/forge/compiler/app-graph/profile.ts +26 -0
- package/src/forge/compiler/app-graph/versions.ts +1 -1
- package/src/forge/compiler/classifier/capabilities.ts +3 -2
- package/src/forge/compiler/classifier/classify.ts +32 -8
- package/src/forge/compiler/classifier/secrets.ts +3 -2
- package/src/forge/compiler/classifier/signals.ts +91 -1
- package/src/forge/compiler/client-sdk/build-manifest.ts +59 -0
- package/src/forge/compiler/client-sdk/render-client.ts +295 -13
- package/src/forge/compiler/data-graph/parse.ts +20 -4
- package/src/forge/compiler/data-graph/sql/ddl.ts +144 -31
- package/src/forge/compiler/data-graph/sql/serialize.ts +4 -0
- package/src/forge/compiler/data-graph/sql/types.ts +1 -0
- package/src/forge/compiler/diagnostics/codes.ts +30 -0
- package/src/forge/compiler/diagnostics/create.ts +21 -2
- package/src/forge/compiler/diagnostics/index.ts +2 -0
- package/src/forge/compiler/emitter/barrel.ts +3 -0
- package/src/forge/compiler/emitter/render.ts +9 -0
- package/src/forge/compiler/external-manifest/registry.ts +205 -0
- package/src/forge/compiler/external-manifest/types.ts +91 -0
- package/src/forge/compiler/external-manifest/validate.ts +373 -0
- package/src/forge/compiler/frontend-graph/build.ts +142 -14
- package/src/forge/compiler/integration/add.ts +618 -24
- package/src/forge/compiler/integration/plan.ts +43 -12
- package/src/forge/compiler/integration/render.ts +29 -0
- package/src/forge/compiler/integration/snapshot.ts +2 -0
- package/src/forge/compiler/integration/templates/convex.ts +70 -0
- package/src/forge/compiler/integration/templates/index.ts +1 -0
- package/src/forge/compiler/integration/templates/render.ts +38 -0
- package/src/forge/compiler/integration/templates/types.ts +3 -0
- package/src/forge/compiler/integration/templates/workos.ts +1219 -0
- package/src/forge/compiler/make-registry/build.ts +8 -9
- package/src/forge/compiler/orchestrator/fast-check.ts +4 -0
- package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
- package/src/forge/compiler/orchestrator/manifest.ts +1 -1
- package/src/forge/compiler/orchestrator/plan-profile.ts +23 -0
- package/src/forge/compiler/orchestrator/plan.ts +247 -10
- package/src/forge/compiler/orchestrator/profile.ts +65 -0
- package/src/forge/compiler/orchestrator/run.ts +97 -31
- package/src/forge/compiler/orchestrator/serialize.ts +33 -8
- package/src/forge/compiler/orchestrator/types.ts +1 -1
- package/src/forge/compiler/package-graph/compiler.ts +3 -1
- package/src/forge/compiler/package-graph/constants.ts +1 -1
- package/src/forge/compiler/package-manager/adapter.ts +4 -1
- package/src/forge/compiler/package-manager/commands.ts +4 -0
- package/src/forge/compiler/package-manager/executor.ts +30 -1
- package/src/forge/compiler/package-manager/version.ts +36 -12
- package/src/forge/compiler/package-upgrades/planner.ts +155 -18
- package/src/forge/compiler/package-upgrades/types.ts +2 -0
- package/src/forge/compiler/policy-registry/build.ts +47 -2
- package/src/forge/compiler/policy-registry/parse.ts +32 -2
- package/src/forge/compiler/recipes/definitions.ts +63 -0
- package/src/forge/compiler/recipes/index.ts +2 -0
- package/src/forge/compiler/recipes/registry.ts +6 -0
- package/src/forge/compiler/test-graph/build.ts +11 -3
- package/src/forge/compiler/types/app-graph.ts +8 -2
- package/src/forge/compiler/types/cli.ts +75 -1
- package/src/forge/compiler/types/dev-manifest.ts +4 -0
- package/src/forge/compiler/types/emit.ts +2 -0
- package/src/forge/compiler/types/frontend-graph.ts +2 -2
- package/src/forge/compiler/types/integration.ts +1 -0
- package/src/forge/compiler/types/package-graph.ts +6 -0
- package/src/forge/compiler/types/policy-registry.ts +3 -1
- package/src/forge/delta/classifier.ts +52 -0
- package/src/forge/delta/explain.ts +238 -0
- package/src/forge/delta/git-observer.ts +43 -0
- package/src/forge/delta/ids.ts +44 -0
- package/src/forge/delta/index.ts +25 -0
- package/src/forge/delta/recorder.ts +540 -0
- package/src/forge/delta/redaction.ts +50 -0
- package/src/forge/delta/schema.ts +240 -0
- package/src/forge/delta/session.ts +142 -0
- package/src/forge/delta/status.ts +1202 -0
- package/src/forge/delta/store.ts +3284 -0
- package/src/forge/delta/timeline.ts +178 -0
- package/src/forge/dev/server.ts +808 -17
- package/src/forge/dev/types.ts +15 -1
- package/src/forge/dev/watch.ts +17 -7
- package/src/forge/dev-console/cycle.ts +280 -33
- package/src/forge/dev-console/types.ts +47 -1
- package/src/forge/impact/index.ts +204 -9
- package/src/forge/impact/types.ts +33 -1
- package/src/forge/intent/index.ts +35 -16
- package/src/forge/make/fields.ts +26 -0
- package/src/forge/make/index.ts +40 -6
- package/src/forge/make/templates.ts +155 -2
- package/src/forge/make/types.ts +2 -1
- package/src/forge/policy.ts +1 -1
- package/src/forge/react/index.ts +160 -2
- package/src/forge/refactor/index.ts +1 -0
- package/src/forge/repair/rules/index.ts +2 -2
- package/src/forge/review/index.ts +158 -12
- package/src/forge/review/types.ts +15 -0
- package/src/forge/runtime/auth/config.ts +17 -0
- package/src/forge/runtime/auth/evaluate.ts +15 -2
- package/src/forge/runtime/auth/resolve.ts +61 -6
- package/src/forge/runtime/auth/types.ts +1 -0
- package/src/forge/runtime/db/factory.ts +1 -3
- package/src/forge/runtime/db/generated-client.ts +13 -2
- package/src/forge/runtime/db/memory-adapter.ts +139 -32
- package/src/forge/runtime/db/pglite-adapter.ts +263 -2
- package/src/forge/runtime/db/postgres-adapter.ts +6 -3
- package/src/forge/runtime/executor.ts +129 -16
- package/src/forge/runtime/external/bridge.ts +649 -0
- package/src/forge/runtime/runner/run-entry.ts +16 -7
- package/src/forge/runtime/telemetry/buffer.ts +4 -1
- package/src/forge/runtime/telemetry/scrubber.ts +35 -5
- package/src/forge/runtime/webhooks/security.ts +12 -7
- package/src/forge/server.ts +17 -3
- package/src/forge/ui/index.ts +209 -0
- package/src/forge/ui/types.ts +1 -0
- package/src/forge/version.ts +1 -1
- package/src/forge/vue/index.ts +422 -0
- package/src/forge/workspace/baseline.ts +112 -0
- package/src/forge/workspace/change-summary.ts +250 -0
- package/src/forge/workspace/forge-cli.ts +42 -0
- package/src/forge/workspace/git-summary.ts +391 -0
- package/templates/agent-workroom/AGENTS.md +29 -0
- package/templates/agent-workroom/README.md +34 -0
- package/templates/agent-workroom/forge.config.ts +3 -0
- package/templates/agent-workroom/package.json +33 -0
- package/templates/agent-workroom/src/actions/indexAgentSignal.ts +10 -0
- package/templates/agent-workroom/src/commands/openWorkroom.ts +61 -0
- package/templates/agent-workroom/src/commands/recordAgentSignal.ts +119 -0
- package/templates/agent-workroom/src/commands/recordCheckRun.ts +52 -0
- package/templates/agent-workroom/src/forge/schema.ts +54 -0
- package/templates/agent-workroom/src/policies.ts +6 -0
- package/templates/agent-workroom/src/queries/listWorkrooms.ts +11 -0
- package/templates/agent-workroom/src/queries/liveWorkroom.ts +63 -0
- package/templates/agent-workroom/tsconfig.json +16 -0
- package/templates/agent-workroom/web/index.html +12 -0
- package/templates/agent-workroom/web/package.json +21 -0
- package/templates/agent-workroom/web/src/App.tsx +345 -0
- package/templates/agent-workroom/web/src/lib/forge.ts +13 -0
- package/templates/agent-workroom/web/src/main.tsx +13 -0
- package/templates/agent-workroom/web/src/styles.css +545 -0
- package/templates/agent-workroom/web/tsconfig.json +27 -0
- package/templates/b2b-support-web/package.json +1 -0
- package/templates/b2b-support-web/web/package.json +1 -1
- package/templates/minimal-web/package.json +1 -1
- package/templates/minimal-web/web/package.json +2 -2
- package/templates/nuxt-web/.vscode/settings.json +14 -0
- package/templates/nuxt-web/README.md +30 -0
- package/templates/nuxt-web/forge.config.ts +3 -0
- package/templates/nuxt-web/package.json +33 -0
- package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
- package/templates/nuxt-web/src/commands/createNote.ts +26 -0
- package/templates/nuxt-web/src/forge/schema.ts +12 -0
- package/templates/nuxt-web/src/policies.ts +6 -0
- package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
- package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
- package/templates/nuxt-web/tsconfig.json +17 -0
- package/templates/nuxt-web/web/app.vue +67 -0
- package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
- package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
- package/templates/nuxt-web/web/composables/forge.ts +13 -0
- package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
- package/templates/nuxt-web/web/nuxt.config.ts +11 -0
- package/templates/nuxt-web/web/package.json +18 -0
- package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
- package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
- package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
- package/templates/nuxt-web/web/tsconfig.json +6 -0
- package/src/forge/_generated/actionSubscriptions.json +0 -2
- package/src/forge/_generated/actionSubscriptions.ts +0 -10
- package/src/forge/_generated/agentAdapterManifest.json +0 -2
- package/src/forge/_generated/agentAdapterManifest.ts +0 -73
- package/src/forge/_generated/agentContract.json +0 -2
- package/src/forge/_generated/agentContract.ts +0 -7829
- package/src/forge/_generated/agentQuickstart.md +0 -34
- package/src/forge/_generated/agentTools.json +0 -2
- package/src/forge/_generated/agentTools.md +0 -16
- package/src/forge/_generated/agentTools.ts +0 -12
- package/src/forge/_generated/aiContext.ts +0 -125
- package/src/forge/_generated/aiModels.json +0 -2
- package/src/forge/_generated/aiModels.ts +0 -51
- package/src/forge/_generated/aiProviders.json +0 -2
- package/src/forge/_generated/aiProviders.ts +0 -23
- package/src/forge/_generated/aiRegistry.json +0 -2
- package/src/forge/_generated/aiRegistry.ts +0 -31
- package/src/forge/_generated/api.json +0 -2
- package/src/forge/_generated/api.ts +0 -8
- package/src/forge/_generated/appGraph.json +0 -2
- package/src/forge/_generated/appGraph.ts +0 -14919
- package/src/forge/_generated/appMap.md +0 -55
- package/src/forge/_generated/artifactManifest.json +0 -2
- package/src/forge/_generated/artifactManifest.ts +0 -7
- package/src/forge/_generated/authClaims.json +0 -2
- package/src/forge/_generated/authClaims.ts +0 -13
- package/src/forge/_generated/authConfig.json +0 -2
- package/src/forge/_generated/authConfig.ts +0 -17
- package/src/forge/_generated/authContext.ts +0 -23
- package/src/forge/_generated/authRegistry.json +0 -2
- package/src/forge/_generated/authRegistry.ts +0 -25
- package/src/forge/_generated/buildInfo.json +0 -2
- package/src/forge/_generated/buildInfo.ts +0 -9
- package/src/forge/_generated/capabilityMap.json +0 -2
- package/src/forge/_generated/capabilityMap.md +0 -15
- package/src/forge/_generated/capabilityMap.ts +0 -17
- package/src/forge/_generated/client.ts +0 -282
- package/src/forge/_generated/clientApi.ts +0 -9
- package/src/forge/_generated/clientManifest.json +0 -2
- package/src/forge/_generated/clientManifest.ts +0 -39
- package/src/forge/_generated/clientTypes.ts +0 -78
- package/src/forge/_generated/configRegistry.json +0 -2
- package/src/forge/_generated/configRegistry.ts +0 -4
- package/src/forge/_generated/dataGraph.json +0 -2
- package/src/forge/_generated/dataGraph.ts +0 -8
- package/src/forge/_generated/db.json +0 -2
- package/src/forge/_generated/db.ts +0 -2
- package/src/forge/_generated/dbSecurityManifest.json +0 -2
- package/src/forge/_generated/dbSecurityManifest.ts +0 -15
- package/src/forge/_generated/dbSessionContext.json +0 -2
- package/src/forge/_generated/dbSessionContext.ts +0 -39
- package/src/forge/_generated/deployManifest.json +0 -2
- package/src/forge/_generated/deployManifest.ts +0 -14
- package/src/forge/_generated/devManifest.json +0 -2
- package/src/forge/_generated/devManifest.ts +0 -62
- package/src/forge/_generated/envSchema.json +0 -2
- package/src/forge/_generated/envSchema.ts +0 -59
- package/src/forge/_generated/frontendGraph.json +0 -2
- package/src/forge/_generated/frontendGraph.ts +0 -27
- package/src/forge/_generated/importGuards.json +0 -2
- package/src/forge/_generated/importGuards.ts +0 -686
- package/src/forge/_generated/index.ts +0 -68
- package/src/forge/_generated/liveProductionManifest.json +0 -2
- package/src/forge/_generated/liveProductionManifest.ts +0 -23
- package/src/forge/_generated/liveProtocol.json +0 -2
- package/src/forge/_generated/liveProtocol.ts +0 -21
- package/src/forge/_generated/liveQueryRegistry.json +0 -2
- package/src/forge/_generated/liveQueryRegistry.ts +0 -9
- package/src/forge/_generated/liveTransportConfig.json +0 -2
- package/src/forge/_generated/liveTransportConfig.ts +0 -19
- package/src/forge/_generated/makeRegistry.json +0 -2
- package/src/forge/_generated/makeRegistry.ts +0 -177
- package/src/forge/_generated/makeTemplates.json +0 -2
- package/src/forge/_generated/makeTemplates.ts +0 -66
- package/src/forge/_generated/mockMap.json +0 -2
- package/src/forge/_generated/mockMap.ts +0 -7
- package/src/forge/_generated/operationPlaybooks.md +0 -167
- package/src/forge/_generated/packageGraph.json +0 -2
- package/src/forge/_generated/packageGraph.ts +0 -249334
- package/src/forge/_generated/packageUpgradeRegistry.json +0 -2
- package/src/forge/_generated/packageUpgradeRegistry.ts +0 -15
- package/src/forge/_generated/permissionMatrix.json +0 -2
- package/src/forge/_generated/permissionMatrix.ts +0 -7
- package/src/forge/_generated/policyRegistry.json +0 -2
- package/src/forge/_generated/policyRegistry.ts +0 -11
- package/src/forge/_generated/queryRegistry.json +0 -2
- package/src/forge/_generated/queryRegistry.ts +0 -9
- package/src/forge/_generated/react.d.ts +0 -22
- package/src/forge/_generated/react.ts +0 -29
- package/src/forge/_generated/reactManifest.json +0 -2
- package/src/forge/_generated/reactManifest.ts +0 -19
- package/src/forge/_generated/rlsPolicies.json +0 -2
- package/src/forge/_generated/rlsPolicies.sql +0 -34
- package/src/forge/_generated/rlsPolicies.ts +0 -6
- package/src/forge/_generated/runtimeGraph.json +0 -2
- package/src/forge/_generated/runtimeGraph.ts +0 -8
- package/src/forge/_generated/runtimeMatrix.json +0 -2
- package/src/forge/_generated/runtimeMatrix.ts +0 -334130
- package/src/forge/_generated/runtimeRegistry.ts +0 -2
- package/src/forge/_generated/runtimeRules.md +0 -91
- package/src/forge/_generated/secretRegistry.json +0 -2
- package/src/forge/_generated/secretRegistry.ts +0 -50
- package/src/forge/_generated/secretsContext.ts +0 -11
- package/src/forge/_generated/serverApi.ts +0 -10
- package/src/forge/_generated/sourceMapManifest.json +0 -2
- package/src/forge/_generated/sourceMapManifest.ts +0 -7
- package/src/forge/_generated/sqlPlan.json +0 -2
- package/src/forge/_generated/sqlPlan.ts +0 -88
- package/src/forge/_generated/subscriptionManifest.json +0 -2
- package/src/forge/_generated/subscriptionManifest.ts +0 -7
- package/src/forge/_generated/symbolicationManifest.json +0 -2
- package/src/forge/_generated/symbolicationManifest.ts +0 -17
- package/src/forge/_generated/telemetryRegistry.json +0 -2
- package/src/forge/_generated/telemetryRegistry.ts +0 -9
- package/src/forge/_generated/telemetrySinks.json +0 -2
- package/src/forge/_generated/telemetrySinks.ts +0 -11
- package/src/forge/_generated/tenantScope.json +0 -2
- package/src/forge/_generated/tenantScope.ts +0 -8
- package/src/forge/_generated/testGraph.json +0 -2
- package/src/forge/_generated/testGraph.ts +0 -3430
- package/src/forge/_generated/testPlanRegistry.json +0 -2
- package/src/forge/_generated/testPlanRegistry.ts +0 -33
- package/src/forge/_generated/uiRoutes.json +0 -2
- package/src/forge/_generated/uiRoutes.ts +0 -16
- package/src/forge/_generated/uiScenarios.json +0 -2
- package/src/forge/_generated/uiScenarios.ts +0 -30
- package/src/forge/_generated/uiTestManifest.json +0 -2
- package/src/forge/_generated/uiTestManifest.ts +0 -27
- package/src/forge/_generated/workflowRegistry.json +0 -2
- package/src/forge/_generated/workflowRegistry.ts +0 -9
- package/src/forge/_generated/workflowSubscriptions.json +0 -2
- package/src/forge/_generated/workflowSubscriptions.ts +0 -10
package/src/forge/cli/verify.ts
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
1
2
|
import { nodeFileSystem } from "../compiler/fs/index.ts";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { availableParallelism, tmpdir } from "node:os";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
|
|
8
|
+
import { canonicalJson, serializeCanonical } from "../compiler/primitives/serialize.ts";
|
|
4
9
|
import { createDiagnostic } from "../compiler/diagnostics/create.ts";
|
|
5
10
|
import type { Diagnostic } from "../compiler/types/diagnostic.ts";
|
|
6
|
-
import type {
|
|
11
|
+
import type { TestCost, TestGraph } from "../compiler/types/test-graph.ts";
|
|
12
|
+
import type {
|
|
13
|
+
VerifyOptions,
|
|
14
|
+
VerifyProfile,
|
|
15
|
+
VerifyResult,
|
|
16
|
+
VerifyStep,
|
|
17
|
+
VerifyTestGraphDurationSource,
|
|
18
|
+
VerifyTestGraphLane,
|
|
19
|
+
VerifyTestGraphPlan,
|
|
20
|
+
VerifyTestGraphPlanChunk,
|
|
21
|
+
} from "../compiler/types/cli.ts";
|
|
7
22
|
import {
|
|
23
|
+
FORGE_VERIFY_NO_TESTS_SELECTED,
|
|
8
24
|
FORGE_VERIFY_POLICY,
|
|
9
25
|
FORGE_VERIFY_SCRIPT_TIMEOUT,
|
|
10
26
|
} from "../compiler/diagnostics/codes.ts";
|
|
11
27
|
import { detectPackageManager } from "../compiler/package-manager/detect.ts";
|
|
12
|
-
import { resolvePackageManagerArgv } from "../compiler/package-manager/executor.ts";
|
|
28
|
+
import { resolveCommandArgv, resolvePackageManagerArgv } from "../compiler/package-manager/executor.ts";
|
|
13
29
|
import { runCheckCommand, runGenerateCommand } from "./commands.ts";
|
|
14
30
|
import { lintForgeGuards } from "./lint-forge.ts";
|
|
15
31
|
import { runPolicyCommand } from "./policy.ts";
|
|
16
32
|
import { runAuthCommand } from "./auth.ts";
|
|
17
33
|
import { runRlsCommand } from "./rls.ts";
|
|
34
|
+
import { runUiCommand } from "../ui/index.ts";
|
|
18
35
|
import { buildImpactTestPlan, diagnosticsForImpactTestRun, runImpactTestPlan } from "../impact/index.ts";
|
|
36
|
+
import type { TestRunRecord, TestRunStep } from "../impact/types.ts";
|
|
19
37
|
import { runAgentCheck } from "../agent-adapters/index.ts";
|
|
20
38
|
import type { AgentAdapterTarget } from "../agent-adapters/types.ts";
|
|
21
39
|
|
|
@@ -26,6 +44,28 @@ interface PackageScripts {
|
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
const DEFAULT_SCRIPT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
47
|
+
type TypecheckerChoice = "tsc" | "native" | "ts7" | "tsgo" | "auto";
|
|
48
|
+
|
|
49
|
+
interface ScriptRunResult {
|
|
50
|
+
exitCode: number;
|
|
51
|
+
stdout: string;
|
|
52
|
+
stderr: string;
|
|
53
|
+
command: string;
|
|
54
|
+
durationMs: number;
|
|
55
|
+
timedOut: boolean;
|
|
56
|
+
spawnError?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface PackageJsonWithBin {
|
|
60
|
+
version?: unknown;
|
|
61
|
+
bin?: unknown;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface TypecheckCandidate {
|
|
65
|
+
label: string;
|
|
66
|
+
argv: string[];
|
|
67
|
+
command: string;
|
|
68
|
+
}
|
|
29
69
|
|
|
30
70
|
function readPackageScripts(workspaceRoot: string): PackageScripts {
|
|
31
71
|
const packageJsonPath = join(workspaceRoot, "package.json");
|
|
@@ -43,33 +83,78 @@ function readPackageScripts(workspaceRoot: string): PackageScripts {
|
|
|
43
83
|
}
|
|
44
84
|
}
|
|
45
85
|
|
|
86
|
+
function readWorkspacePackageJson(workspaceRoot: string): Record<string, unknown> {
|
|
87
|
+
const packageJsonPath = join(workspaceRoot, "package.json");
|
|
88
|
+
if (!nodeFileSystem.exists(packageJsonPath)) {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(nodeFileSystem.readText(packageJsonPath) ?? "{}") as Record<string, unknown>;
|
|
93
|
+
} catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isForgeOsFrameworkWorkspace(workspaceRoot: string): boolean {
|
|
99
|
+
const pkg = readWorkspacePackageJson(workspaceRoot);
|
|
100
|
+
return (
|
|
101
|
+
pkg.name === "forgeos" &&
|
|
102
|
+
nodeFileSystem.exists(join(workspaceRoot, "src/forge/cli/verify.ts")) &&
|
|
103
|
+
nodeFileSystem.exists(join(workspaceRoot, "bin/forge.mjs"))
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
46
107
|
async function spawnPackageRun(
|
|
47
108
|
workspaceRoot: string,
|
|
48
109
|
scriptName: string,
|
|
49
110
|
timeoutMs: number,
|
|
50
|
-
): Promise<{
|
|
51
|
-
exitCode: number;
|
|
52
|
-
stdout: string;
|
|
53
|
-
stderr: string;
|
|
54
|
-
command: string;
|
|
55
|
-
durationMs: number;
|
|
56
|
-
timedOut: boolean;
|
|
57
|
-
}> {
|
|
111
|
+
): Promise<ScriptRunResult> {
|
|
58
112
|
const packageManager = detectPackageManager(workspaceRoot);
|
|
59
113
|
let argv = resolvePackageManagerArgv([packageManager, "run", scriptName]);
|
|
60
114
|
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(argv[0] ?? "")) {
|
|
61
115
|
argv = [process.env.ComSpec ?? "cmd.exe", "/d", "/c", packageManager, "run", scriptName];
|
|
62
116
|
}
|
|
117
|
+
return spawnArgv(workspaceRoot, argv, timeoutMs, argv.join(" "));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function quoteWindowsCommandArg(value: string): string {
|
|
121
|
+
if (!/[\s"]/u.test(value)) {
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
return `"${value.replace(/"/g, "\"\"")}"`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function wrapWindowsCommandScript(argv: string[]): string[] {
|
|
128
|
+
if (process.platform !== "win32" || !/\.(cmd|bat)$/iu.test(argv[0] ?? "")) {
|
|
129
|
+
return argv;
|
|
130
|
+
}
|
|
131
|
+
return [
|
|
132
|
+
process.env.ComSpec ?? "cmd.exe",
|
|
133
|
+
"/d",
|
|
134
|
+
"/s",
|
|
135
|
+
"/c",
|
|
136
|
+
argv.map(quoteWindowsCommandArg).join(" "),
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function spawnArgv(
|
|
141
|
+
workspaceRoot: string,
|
|
142
|
+
argv: string[],
|
|
143
|
+
timeoutMs: number,
|
|
144
|
+
command = argv.join(" "),
|
|
145
|
+
envOverrides?: Record<string, string>,
|
|
146
|
+
): Promise<ScriptRunResult> {
|
|
63
147
|
const started = Date.now();
|
|
64
148
|
|
|
65
149
|
return new Promise((resolve) => {
|
|
66
150
|
let settled = false;
|
|
67
151
|
let timedOut = false;
|
|
68
152
|
let child: ReturnType<typeof spawn>;
|
|
153
|
+
const spawnCommand = wrapWindowsCommandScript(argv);
|
|
69
154
|
try {
|
|
70
|
-
child = spawn(
|
|
155
|
+
child = spawn(spawnCommand[0]!, spawnCommand.slice(1), {
|
|
71
156
|
cwd: workspaceRoot,
|
|
72
|
-
env: process.env,
|
|
157
|
+
env: envOverrides ? { ...process.env, ...envOverrides } : process.env,
|
|
73
158
|
stdio: ["ignore", "pipe", "pipe"],
|
|
74
159
|
windowsHide: true,
|
|
75
160
|
});
|
|
@@ -78,9 +163,10 @@ async function spawnPackageRun(
|
|
|
78
163
|
exitCode: 1,
|
|
79
164
|
stdout: "",
|
|
80
165
|
stderr: error instanceof Error ? error.message : String(error),
|
|
81
|
-
command
|
|
166
|
+
command,
|
|
82
167
|
durationMs: Date.now() - started,
|
|
83
168
|
timedOut: false,
|
|
169
|
+
spawnError: true,
|
|
84
170
|
});
|
|
85
171
|
return;
|
|
86
172
|
}
|
|
@@ -110,9 +196,10 @@ async function spawnPackageRun(
|
|
|
110
196
|
exitCode: 1,
|
|
111
197
|
stdout,
|
|
112
198
|
stderr: error instanceof Error ? error.message : String(error),
|
|
113
|
-
command
|
|
199
|
+
command,
|
|
114
200
|
durationMs: Date.now() - started,
|
|
115
201
|
timedOut,
|
|
202
|
+
spawnError: true,
|
|
116
203
|
});
|
|
117
204
|
}
|
|
118
205
|
});
|
|
@@ -124,7 +211,7 @@ async function spawnPackageRun(
|
|
|
124
211
|
exitCode: timedOut ? 1 : code ?? 1,
|
|
125
212
|
stdout,
|
|
126
213
|
stderr,
|
|
127
|
-
command
|
|
214
|
+
command,
|
|
128
215
|
durationMs: Date.now() - started,
|
|
129
216
|
timedOut,
|
|
130
217
|
});
|
|
@@ -137,14 +224,7 @@ async function runPackageScript(
|
|
|
137
224
|
workspaceRoot: string,
|
|
138
225
|
scriptName: string,
|
|
139
226
|
timeoutMs: number,
|
|
140
|
-
): Promise<{
|
|
141
|
-
exitCode: number;
|
|
142
|
-
stdout: string;
|
|
143
|
-
stderr: string;
|
|
144
|
-
command: string;
|
|
145
|
-
durationMs: number;
|
|
146
|
-
timedOut: boolean;
|
|
147
|
-
}> {
|
|
227
|
+
): Promise<ScriptRunResult> {
|
|
148
228
|
return spawnPackageRun(workspaceRoot, scriptName, timeoutMs);
|
|
149
229
|
}
|
|
150
230
|
|
|
@@ -229,7 +309,1173 @@ function packageScriptFailureDiagnostic(
|
|
|
229
309
|
});
|
|
230
310
|
}
|
|
231
311
|
|
|
312
|
+
function strictGraphFailureDiagnostic(result: {
|
|
313
|
+
exitCode: number;
|
|
314
|
+
stdout: string;
|
|
315
|
+
stderr: string;
|
|
316
|
+
command: string;
|
|
317
|
+
failedFiles: string[];
|
|
318
|
+
failedChunk?: number;
|
|
319
|
+
reportPath?: string;
|
|
320
|
+
}): Diagnostic {
|
|
321
|
+
const excerpt = outputExcerpt(result.stdout, result.stderr);
|
|
322
|
+
const files = result.failedFiles.slice(0, 8);
|
|
323
|
+
const hidden = Math.max(0, result.failedFiles.length - files.length);
|
|
324
|
+
const fileSummary = files.length > 0
|
|
325
|
+
? `${files.join(", ")}${hidden > 0 ? `, ... +${hidden} more` : ""}`
|
|
326
|
+
: "unknown files";
|
|
327
|
+
const report = result.reportPath ?? ".forge/test-runs/last.json";
|
|
328
|
+
return createDiagnostic({
|
|
329
|
+
severity: "error",
|
|
330
|
+
code: "FORGE_VERIFY_TESTS",
|
|
331
|
+
message: `strict TestGraph failed${result.failedChunk ? ` in chunk ${result.failedChunk}` : ""} with exit code ${result.exitCode}: ${fileSummary}`,
|
|
332
|
+
fixHint: excerpt
|
|
333
|
+
? `Inspect ${report} and rerun the failing files. Last output: ${excerpt}`
|
|
334
|
+
: `Inspect ${report} and rerun the failing files.`,
|
|
335
|
+
suggestedCommands: [
|
|
336
|
+
result.failedFiles.length > 0
|
|
337
|
+
? `bun test ${result.failedFiles.join(" ")}`
|
|
338
|
+
: result.command,
|
|
339
|
+
"forge repair diagnose --from-last-test-run --json",
|
|
340
|
+
"forge verify --strict",
|
|
341
|
+
],
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function resolveTypechecker(options: VerifyOptions): TypecheckerChoice {
|
|
346
|
+
if (options.typechecker) {
|
|
347
|
+
return options.typechecker;
|
|
348
|
+
}
|
|
349
|
+
const fromEnv = process.env.FORGE_TYPECHECKER;
|
|
350
|
+
return (
|
|
351
|
+
fromEnv === "native" ||
|
|
352
|
+
fromEnv === "ts7" ||
|
|
353
|
+
fromEnv === "tsgo" ||
|
|
354
|
+
fromEnv === "auto" ||
|
|
355
|
+
fromEnv === "tsc"
|
|
356
|
+
)
|
|
357
|
+
? fromEnv
|
|
358
|
+
: "tsc";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function nodeModulePackageRoot(workspaceRoot: string, packageName: string): string {
|
|
362
|
+
return join(workspaceRoot, "node_modules", ...packageName.split("/"));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function readNodeModulePackageJson(
|
|
366
|
+
workspaceRoot: string,
|
|
367
|
+
packageName: string,
|
|
368
|
+
): PackageJsonWithBin | undefined {
|
|
369
|
+
const packageJsonPath = join(nodeModulePackageRoot(workspaceRoot, packageName), "package.json");
|
|
370
|
+
if (!nodeFileSystem.exists(packageJsonPath)) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
return JSON.parse(nodeFileSystem.readText(packageJsonPath) ?? "{}") as PackageJsonWithBin;
|
|
376
|
+
} catch {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function packageVersion(workspaceRoot: string, packageName: string): string | undefined {
|
|
382
|
+
const version = readNodeModulePackageJson(workspaceRoot, packageName)?.version;
|
|
383
|
+
return typeof version === "string" ? version : undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function packageMajorVersion(workspaceRoot: string, packageName: string): number | undefined {
|
|
387
|
+
const version = packageVersion(workspaceRoot, packageName);
|
|
388
|
+
const match = version?.match(/^(\d+)/u);
|
|
389
|
+
if (!match) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
const parsed = Number(match[1]);
|
|
393
|
+
return Number.isInteger(parsed) ? parsed : undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function packageBinPath(workspaceRoot: string, packageName: string, binName: string): string | undefined {
|
|
397
|
+
const packageRoot = nodeModulePackageRoot(workspaceRoot, packageName);
|
|
398
|
+
const packageJson = readNodeModulePackageJson(workspaceRoot, packageName);
|
|
399
|
+
if (!packageJson) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let relativeBin: string | undefined;
|
|
404
|
+
if (typeof packageJson.bin === "string") {
|
|
405
|
+
relativeBin = packageJson.bin;
|
|
406
|
+
} else if (packageJson.bin && typeof packageJson.bin === "object") {
|
|
407
|
+
const value = (packageJson.bin as Record<string, unknown>)[binName];
|
|
408
|
+
relativeBin = typeof value === "string" ? value : undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const candidates = [
|
|
412
|
+
relativeBin ? join(packageRoot, relativeBin) : undefined,
|
|
413
|
+
join(packageRoot, "bin", binName),
|
|
414
|
+
].filter((candidate): candidate is string => typeof candidate === "string");
|
|
415
|
+
|
|
416
|
+
return candidates.find((candidate) => nodeFileSystem.exists(candidate));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isLikelyPath(value: string): boolean {
|
|
420
|
+
return value.includes("/") || value.includes("\\") || /^[a-z]:/iu.test(value);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function isNodeRunnableBin(executable: string): boolean {
|
|
424
|
+
if (/\.(cjs|js|mjs)$/iu.test(executable)) {
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
if (!nodeFileSystem.exists(executable)) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
const head = (nodeFileSystem.readText(executable) ?? "").slice(0, 256);
|
|
432
|
+
const firstLine = head.split(/\r?\n/u)[0] ?? "";
|
|
433
|
+
return firstLine.includes("node") || head.includes("require(") || head.includes("import ");
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function argvForExecutable(executable: string, args: string[]): string[] {
|
|
440
|
+
const trimmed = executable.trim();
|
|
441
|
+
if (!isLikelyPath(trimmed)) {
|
|
442
|
+
return resolveCommandArgv([trimmed, ...args]);
|
|
443
|
+
}
|
|
444
|
+
return isNodeRunnableBin(trimmed) ? [process.execPath, trimmed, ...args] : [trimmed, ...args];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function argvForPackageBin(binPath: string, args: string[]): string[] {
|
|
448
|
+
return isNodeRunnableBin(binPath) ? [process.execPath, binPath, ...args] : [binPath, ...args];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function positiveIntegerEnv(name: string): string | undefined {
|
|
452
|
+
const value = process.env[name]?.trim();
|
|
453
|
+
if (!value) {
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
const parsed = Number(value);
|
|
457
|
+
return Number.isInteger(parsed) && parsed >= 1 ? String(parsed) : undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function nativeTypecheckArgs(): string[] {
|
|
461
|
+
const args = ["--noEmit"];
|
|
462
|
+
const checkers = positiveIntegerEnv("FORGE_TS7_CHECKERS");
|
|
463
|
+
const builders = positiveIntegerEnv("FORGE_TS7_BUILDERS");
|
|
464
|
+
const singleThreaded = process.env.FORGE_TS7_SINGLE_THREADED?.trim().toLowerCase();
|
|
465
|
+
if (checkers) {
|
|
466
|
+
args.push("--checkers", checkers);
|
|
467
|
+
}
|
|
468
|
+
if (builders) {
|
|
469
|
+
args.push("--builders", builders);
|
|
470
|
+
}
|
|
471
|
+
if (singleThreaded === "1" || singleThreaded === "true" || singleThreaded === "yes") {
|
|
472
|
+
args.push("--singleThreaded");
|
|
473
|
+
}
|
|
474
|
+
return args;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function nativeTypecheckCandidates(workspaceRoot: string): TypecheckCandidate[] {
|
|
478
|
+
const args = nativeTypecheckArgs();
|
|
479
|
+
const candidates: TypecheckCandidate[] = [];
|
|
480
|
+
const explicitTsc = process.env.FORGE_TS7_TSC?.trim();
|
|
481
|
+
if (explicitTsc) {
|
|
482
|
+
candidates.push({
|
|
483
|
+
label: "FORGE_TS7_TSC",
|
|
484
|
+
argv: argvForExecutable(explicitTsc, args),
|
|
485
|
+
command: `${explicitTsc} ${args.join(" ")}`,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const aliasedTs7 = packageBinPath(workspaceRoot, "typescript-7", "tsc");
|
|
490
|
+
if (aliasedTs7) {
|
|
491
|
+
candidates.push({
|
|
492
|
+
label: "typescript-7",
|
|
493
|
+
argv: argvForPackageBin(aliasedTs7, args),
|
|
494
|
+
command: "typescript-7 tsc --noEmit",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if ((packageMajorVersion(workspaceRoot, "typescript") ?? 0) >= 7) {
|
|
499
|
+
const rootTs = packageBinPath(workspaceRoot, "typescript", "tsc");
|
|
500
|
+
if (rootTs) {
|
|
501
|
+
candidates.push({
|
|
502
|
+
label: "typescript@7",
|
|
503
|
+
argv: argvForPackageBin(rootTs, args),
|
|
504
|
+
command: "typescript@7 tsc --noEmit",
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const nativePreview = packageBinPath(workspaceRoot, "@typescript/native-preview", "tsgo");
|
|
510
|
+
if (nativePreview) {
|
|
511
|
+
candidates.push({
|
|
512
|
+
label: "@typescript/native-preview",
|
|
513
|
+
argv: argvForPackageBin(nativePreview, args),
|
|
514
|
+
command: "@typescript/native-preview tsgo --noEmit",
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return candidates;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function missingNativeTypecheckResult(): ScriptRunResult {
|
|
522
|
+
return {
|
|
523
|
+
exitCode: 1,
|
|
524
|
+
stdout: "",
|
|
525
|
+
stderr: [
|
|
526
|
+
"No TypeScript native checker was found.",
|
|
527
|
+
"Install an aliased RC with `npm install -D typescript-7@npm:typescript@rc`,",
|
|
528
|
+
"set FORGE_TS7_TSC, or install @typescript/native-preview.",
|
|
529
|
+
].join(" "),
|
|
530
|
+
command: "typescript native --noEmit",
|
|
531
|
+
durationMs: 0,
|
|
532
|
+
timedOut: false,
|
|
533
|
+
spawnError: true,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function runTscTypecheck(
|
|
538
|
+
workspaceRoot: string,
|
|
539
|
+
scripts: PackageScripts,
|
|
540
|
+
timeoutMs: number,
|
|
541
|
+
): Promise<ScriptRunResult> {
|
|
542
|
+
if (scripts.typecheck) {
|
|
543
|
+
return runPackageScript(workspaceRoot, "typecheck", timeoutMs);
|
|
544
|
+
}
|
|
545
|
+
const argv = resolveCommandArgv(["tsc", "--noEmit"]);
|
|
546
|
+
return spawnArgv(workspaceRoot, argv, timeoutMs, "tsc --noEmit");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function runNativeTypecheck(workspaceRoot: string, timeoutMs: number): Promise<ScriptRunResult> {
|
|
550
|
+
const [candidate] = nativeTypecheckCandidates(workspaceRoot);
|
|
551
|
+
if (!candidate) {
|
|
552
|
+
return missingNativeTypecheckResult();
|
|
553
|
+
}
|
|
554
|
+
return spawnArgv(workspaceRoot, candidate.argv, timeoutMs, candidate.command);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function runTsgoTypecheck(workspaceRoot: string, timeoutMs: number): Promise<ScriptRunResult> {
|
|
558
|
+
const args = nativeTypecheckArgs();
|
|
559
|
+
const nativePreview = packageBinPath(workspaceRoot, "@typescript/native-preview", "tsgo");
|
|
560
|
+
if (nativePreview) {
|
|
561
|
+
return spawnArgv(
|
|
562
|
+
workspaceRoot,
|
|
563
|
+
argvForPackageBin(nativePreview, args),
|
|
564
|
+
timeoutMs,
|
|
565
|
+
"@typescript/native-preview tsgo --noEmit",
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const argv = resolveCommandArgv(["tsgo", ...args]);
|
|
569
|
+
return spawnArgv(workspaceRoot, argv, timeoutMs, "tsgo --noEmit");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function typecheckAttemptSummary(label: string, result: ScriptRunResult): string {
|
|
573
|
+
if (result.timedOut) {
|
|
574
|
+
return `${label}: timed out`;
|
|
575
|
+
}
|
|
576
|
+
if (result.spawnError) {
|
|
577
|
+
return `${label}: command unavailable`;
|
|
578
|
+
}
|
|
579
|
+
return `${label}: exit code ${result.exitCode}`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function typecheckerFallbackDiagnostic(
|
|
583
|
+
choice: TypecheckerChoice,
|
|
584
|
+
attempts: Array<{ label: string; result: ScriptRunResult }>,
|
|
585
|
+
): Diagnostic {
|
|
586
|
+
const excerpt = attempts
|
|
587
|
+
.map((attempt) => outputExcerpt(attempt.result.stdout, attempt.result.stderr))
|
|
588
|
+
.find(Boolean);
|
|
589
|
+
return createDiagnostic({
|
|
590
|
+
severity: "warning",
|
|
591
|
+
code: "FORGE_VERIFY_TYPECHECKER_FALLBACK",
|
|
592
|
+
message: `${choice} typecheck failed; fell back to tsc (${attempts
|
|
593
|
+
.map((attempt) => typecheckAttemptSummary(attempt.label, attempt.result))
|
|
594
|
+
.join("; ")})`,
|
|
595
|
+
fixHint: excerpt ? `Last native output: ${excerpt}` : undefined,
|
|
596
|
+
suggestedCommands: [
|
|
597
|
+
"npm install -D typescript-7@npm:typescript@rc",
|
|
598
|
+
"npm install -D @typescript/native-preview",
|
|
599
|
+
"forge verify --typechecker tsc",
|
|
600
|
+
],
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function runPreferredTypecheck(
|
|
605
|
+
options: VerifyOptions,
|
|
606
|
+
scripts: PackageScripts,
|
|
607
|
+
timeoutMs: number,
|
|
608
|
+
): Promise<{ result: ScriptRunResult; diagnostics: Diagnostic[]; label: string }> {
|
|
609
|
+
const choice = resolveTypechecker(options);
|
|
610
|
+
if (choice === "tsc") {
|
|
611
|
+
return { result: await runTscTypecheck(options.workspaceRoot, scripts, timeoutMs), diagnostics: [], label: "tsc" };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const attempts: Array<{ label: string; result: ScriptRunResult }> = [];
|
|
615
|
+
if (choice === "native" || choice === "ts7" || choice === "auto") {
|
|
616
|
+
const native = await runNativeTypecheck(options.workspaceRoot, timeoutMs);
|
|
617
|
+
if (native.exitCode === 0) {
|
|
618
|
+
return {
|
|
619
|
+
result: native,
|
|
620
|
+
diagnostics: [],
|
|
621
|
+
label: choice === "auto" ? "auto->native" : choice,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
attempts.push({ label: "native", result: native });
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (choice === "tsgo" || choice === "auto") {
|
|
628
|
+
const tsgo = await runTsgoTypecheck(options.workspaceRoot, timeoutMs);
|
|
629
|
+
if (tsgo.exitCode === 0) {
|
|
630
|
+
return {
|
|
631
|
+
result: tsgo,
|
|
632
|
+
diagnostics: [],
|
|
633
|
+
label: choice === "auto" ? "auto->tsgo" : "tsgo",
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
attempts.push({ label: "tsgo", result: tsgo });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const fallback = await runTscTypecheck(options.workspaceRoot, scripts, timeoutMs);
|
|
640
|
+
return {
|
|
641
|
+
result: fallback,
|
|
642
|
+
diagnostics: [typecheckerFallbackDiagnostic(choice, attempts)],
|
|
643
|
+
label: `${choice}->tsc`,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const STRICT_TEST_COSTS: TestCost[] = ["instant", "fast", "standard", "slow"];
|
|
648
|
+
const STRICT_TEST_CHUNK_SIZE = 12;
|
|
649
|
+
const STRICT_TEST_MAX_DEFAULT_JOBS = 6;
|
|
650
|
+
// The isolated lane (one heavy file per chunk: node-compat, dev-server, CLI) is
|
|
651
|
+
// the makespan bottleneck, so it gets more default concurrency than before. The
|
|
652
|
+
// overall budget is still capped by STRICT_TEST_MAX_DEFAULT_JOBS and CPU count.
|
|
653
|
+
const STRICT_ISOLATED_TEST_MAX_DEFAULT_JOBS = 4;
|
|
654
|
+
const TESTGRAPH_PROFILE_RELATIVE_PATH = ".forge/test-runs/testgraph-profile.json";
|
|
655
|
+
const TEST_COST_FALLBACK_MS: Record<TestCost, number> = {
|
|
656
|
+
instant: 250,
|
|
657
|
+
fast: 1_000,
|
|
658
|
+
standard: 3_000,
|
|
659
|
+
slow: 12_000,
|
|
660
|
+
docker: 60_000,
|
|
661
|
+
browser: 60_000,
|
|
662
|
+
};
|
|
663
|
+
const TEST_COST_RANK: Record<TestCost, number> = {
|
|
664
|
+
instant: 0,
|
|
665
|
+
fast: 1,
|
|
666
|
+
standard: 2,
|
|
667
|
+
slow: 3,
|
|
668
|
+
docker: 4,
|
|
669
|
+
browser: 5,
|
|
670
|
+
};
|
|
671
|
+
const STRICT_TEST_FALLBACK_MS_BY_PATH: Array<{ pattern: RegExp; estimatedMs: number }> = [
|
|
672
|
+
{ pattern: /^tests\/cli\/node-compat\.test\.ts$/, estimatedMs: 12_000 },
|
|
673
|
+
{ pattern: /^tests\/cli\/node-compat-dev-server\.test\.ts$/, estimatedMs: 6_000 },
|
|
674
|
+
{ pattern: /^tests\/cli\/node-compat-new\.test\.ts$/, estimatedMs: 8_000 },
|
|
675
|
+
{ pattern: /^tests\/cli\/cli\.test\.ts$/, estimatedMs: 3_000 },
|
|
676
|
+
{ pattern: /^tests\/cli\/cli-generation\.test\.ts$/, estimatedMs: 12_000 },
|
|
677
|
+
{ pattern: /^tests\/cli\/cli-verify\.test\.ts$/, estimatedMs: 12_000 },
|
|
678
|
+
{ pattern: /^tests\/cli\/cli-verify-changed\.test\.ts$/, estimatedMs: 5_000 },
|
|
679
|
+
{ pattern: /^tests\/db\/pglite-adapter\.test\.ts$/, estimatedMs: 12_000 },
|
|
680
|
+
{ pattern: /^tests\/dev\/dev-workflow-worker\.test\.ts$/, estimatedMs: 6_000 },
|
|
681
|
+
{ pattern: /^tests\/external-manifest\/external-runtime-bridge\.test\.ts$/, estimatedMs: 4_000 },
|
|
682
|
+
{ pattern: /^tests\/external-manifest\/external-runtime-cli\.test\.ts$/, estimatedMs: 6_000 },
|
|
683
|
+
{ pattern: /^tests\/external-manifest\/external-runtime-node-cli\.test\.ts$/, estimatedMs: 12_000 },
|
|
684
|
+
{ pattern: /^tests\/external-manifest\/go-adapter-conformance\.test\.ts$/, estimatedMs: 5_000 },
|
|
685
|
+
{ pattern: /^tests\/external-manifest\/java-adapter-conformance\.test\.ts$/, estimatedMs: 20_000 },
|
|
686
|
+
{ pattern: /^tests\/impact\/h28-impact\.test\.ts$/, estimatedMs: 8_000 },
|
|
687
|
+
{ pattern: /^tests\/impact\/h28-impact-runner\.test\.ts$/, estimatedMs: 7_000 },
|
|
688
|
+
{ pattern: /^tests\/impact\/h28-impact-runner-diagnostics\.test\.ts$/, estimatedMs: 3_000 },
|
|
689
|
+
{ pattern: /^tests\/refactor\/h27-refactor\.test\.ts$/, estimatedMs: 6_000 },
|
|
690
|
+
{ pattern: /^tests\/refactor\/h27-refactor-extract-action-apply\.test\.ts$/, estimatedMs: 10_000 },
|
|
691
|
+
{ pattern: /^tests\/refactor\/h27-refactor-extract-action\.test\.ts$/, estimatedMs: 21_000 },
|
|
692
|
+
{ pattern: /^tests\/refactor\/h27-refactor-extract-action-bindings\.test\.ts$/, estimatedMs: 10_000 },
|
|
693
|
+
{ pattern: /^tests\/release\/h23-release-artifacts\.test\.ts$/, estimatedMs: 8_000 },
|
|
694
|
+
{ pattern: /^tests\/release\/h23-release-self-host\.test\.ts$/, estimatedMs: 4_000 },
|
|
695
|
+
{ pattern: /^tests\/release\/h23-release\.test\.ts$/, estimatedMs: 3_000 },
|
|
696
|
+
{ pattern: /^tests\/templates\/new-b2b-support-web\.test\.ts$/, estimatedMs: 12_000 },
|
|
697
|
+
{ pattern: /^tests\/templates\/new-agent-workroom\.test\.ts$/, estimatedMs: 12_000 },
|
|
698
|
+
{ pattern: /^tests\/templates\/new-minimal-web\.test\.ts$/, estimatedMs: 12_000 },
|
|
699
|
+
{ pattern: /^tests\/templates\/new-nuxt-web\.test\.ts$/, estimatedMs: 12_000 },
|
|
700
|
+
{ pattern: /^tests\/templates\/create-forge-app\.test\.ts$/, estimatedMs: 8_000 },
|
|
701
|
+
];
|
|
702
|
+
const STRICT_ISOLATED_TEST_PATTERNS = [
|
|
703
|
+
/^tests\/ai\//,
|
|
704
|
+
/^tests\/cli\/cli-generation\.test\.ts$/,
|
|
705
|
+
/^tests\/cli\/cli\.test\.ts$/,
|
|
706
|
+
/^tests\/cli\/cli-verify\.test\.ts$/,
|
|
707
|
+
/^tests\/cli\/cli-verify-changed\.test\.ts$/,
|
|
708
|
+
/^tests\/cli\/node-compat-dev-server\.test\.ts$/,
|
|
709
|
+
/^tests\/cli\/node-compat-new\.test\.ts$/,
|
|
710
|
+
/^tests\/cli\/windows\.test\.ts$/,
|
|
711
|
+
/^tests\/client\//,
|
|
712
|
+
/^tests\/db\/pglite-adapter\.test\.ts$/,
|
|
713
|
+
/^tests\/dev\//,
|
|
714
|
+
/^tests\/external-manifest\/external-manifest\.test\.ts$/,
|
|
715
|
+
/^tests\/external-manifest\/go-adapter-conformance\.test\.ts$/,
|
|
716
|
+
/^tests\/external-manifest\/java-adapter-conformance\.test\.ts$/,
|
|
717
|
+
/^tests\/external-manifest\/external-runtime-bridge\.test\.ts$/,
|
|
718
|
+
/^tests\/external-manifest\/external-runtime-node-cli\.test\.ts$/,
|
|
719
|
+
/^tests\/impact\/h28-impact\.test\.ts$/,
|
|
720
|
+
/^tests\/impact\/h28-impact-runner\.test\.ts$/,
|
|
721
|
+
/^tests\/impact\/h28-impact-runner-diagnostics\.test\.ts$/,
|
|
722
|
+
/^tests\/live\//,
|
|
723
|
+
/^tests\/queries\/query-dev-server\.test\.ts$/,
|
|
724
|
+
// refactor extract-action/rename tests use ts.createProgram fresh per call
|
|
725
|
+
// against isolated temp workspaces (no shared global or server state), so they
|
|
726
|
+
// run safely co-located in the parallel lane and share one process warm-up
|
|
727
|
+
// instead of paying a cold start per isolated chunk.
|
|
728
|
+
/^tests\/release\/h23-release-artifacts\.test\.ts$/,
|
|
729
|
+
/^tests\/release\/h23-release-self-host\.test\.ts$/,
|
|
730
|
+
/^tests\/release\/h23-release\.test\.ts$/,
|
|
731
|
+
/^tests\/security\/tenant-isolation\/http-runtime\.test\.ts$/,
|
|
732
|
+
/^tests\/templates\/new-b2b-support-web\.test\.ts$/,
|
|
733
|
+
/^tests\/templates\/new-agent-workroom\.test\.ts$/,
|
|
734
|
+
/^tests\/templates\/new-minimal-web\.test\.ts$/,
|
|
735
|
+
/^tests\/templates\/new-nuxt-web\.test\.ts$/,
|
|
736
|
+
/^tests\/telemetry\/telemetry-dev-server\.test\.ts$/,
|
|
737
|
+
];
|
|
738
|
+
const STRICT_SERIAL_TEST_PATTERNS: RegExp[] = [];
|
|
739
|
+
|
|
740
|
+
interface StrictTestEntry {
|
|
741
|
+
file: string;
|
|
742
|
+
cost: TestCost;
|
|
743
|
+
lane: StrictTestLane;
|
|
744
|
+
estimatedMs: number;
|
|
745
|
+
durationSource: VerifyTestGraphDurationSource;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
interface StrictGraphChunkResult extends ScriptRunResult {
|
|
749
|
+
files: string[];
|
|
750
|
+
lane: StrictTestLane;
|
|
751
|
+
chunkIndex: number;
|
|
752
|
+
chunkCount: number;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
interface TestGraphProfileFile {
|
|
756
|
+
schemaVersion: "0.1.0";
|
|
757
|
+
updatedAt: string;
|
|
758
|
+
files: Record<string, {
|
|
759
|
+
durationMs: number;
|
|
760
|
+
runs: number;
|
|
761
|
+
lane: StrictTestLane;
|
|
762
|
+
sourceHash?: string;
|
|
763
|
+
lastExitCode: number;
|
|
764
|
+
lastRunAt: string;
|
|
765
|
+
}>;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function readTestGraph(workspaceRoot: string): TestGraph | null {
|
|
769
|
+
const raw = nodeFileSystem.readText(join(workspaceRoot, "src/forge/_generated/testGraph.json"));
|
|
770
|
+
if (!raw) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
return JSON.parse(stripDeterministicHeader(raw)) as TestGraph;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function strictTestEntries(workspaceRoot: string): Array<{ file: string; cost: TestCost }> {
|
|
777
|
+
const graph = readTestGraph(workspaceRoot);
|
|
778
|
+
if (!graph) {
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
const byFile = new Map<string, TestCost>();
|
|
782
|
+
for (const test of graph.tests) {
|
|
783
|
+
if (!STRICT_TEST_COSTS.includes(test.cost)) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const existing = byFile.get(test.file);
|
|
787
|
+
if (!existing || TEST_COST_RANK[test.cost] > TEST_COST_RANK[existing]) {
|
|
788
|
+
byFile.set(test.file, test.cost);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return [...byFile.entries()]
|
|
792
|
+
.map(([file, cost]) => ({ file, cost }))
|
|
793
|
+
.sort((left, right) => left.file.localeCompare(right.file));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function chunkFiles(files: string[], size: number): string[][] {
|
|
797
|
+
const chunks: string[][] = [];
|
|
798
|
+
for (let index = 0; index < files.length; index += size) {
|
|
799
|
+
chunks.push(files.slice(index, index + size));
|
|
800
|
+
}
|
|
801
|
+
return chunks;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export function resolveStrictTestJobs(options: {
|
|
805
|
+
requested?: number;
|
|
806
|
+
env?: NodeJS.ProcessEnv;
|
|
807
|
+
chunkCount: number;
|
|
808
|
+
}): number {
|
|
809
|
+
if (options.chunkCount <= 1) {
|
|
810
|
+
return 1;
|
|
811
|
+
}
|
|
812
|
+
const fromEnv = options.env?.FORGE_VERIFY_TEST_JOBS;
|
|
813
|
+
const parsedEnv = fromEnv ? Number(fromEnv) : undefined;
|
|
814
|
+
const requested = options.requested ?? parsedEnv;
|
|
815
|
+
if (requested !== undefined && Number.isInteger(requested) && requested >= 1) {
|
|
816
|
+
return Math.min(requested, options.chunkCount);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const cpuBound = Math.max(2, Math.floor(availableParallelism() / 2));
|
|
820
|
+
return Math.min(STRICT_TEST_MAX_DEFAULT_JOBS, cpuBound, options.chunkCount);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function resolveStrictLaneJobs(options: {
|
|
824
|
+
totalJobs: number;
|
|
825
|
+
parallelChunkCount: number;
|
|
826
|
+
isolatedChunkCount: number;
|
|
827
|
+
env?: NodeJS.ProcessEnv;
|
|
828
|
+
}): { parallelJobs: number; isolatedJobs: number } {
|
|
829
|
+
if (options.parallelChunkCount === 0) {
|
|
830
|
+
return {
|
|
831
|
+
parallelJobs: 0,
|
|
832
|
+
isolatedJobs: resolveStrictIsolatedTestJobs({
|
|
833
|
+
env: options.env,
|
|
834
|
+
chunkCount: Math.min(options.totalJobs, options.isolatedChunkCount),
|
|
835
|
+
}),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
if (options.isolatedChunkCount === 0) {
|
|
839
|
+
return {
|
|
840
|
+
parallelJobs: Math.min(options.totalJobs, options.parallelChunkCount),
|
|
841
|
+
isolatedJobs: 0,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
const requestedIsolated = resolveStrictIsolatedTestJobs({
|
|
845
|
+
env: options.env,
|
|
846
|
+
chunkCount: options.isolatedChunkCount,
|
|
847
|
+
});
|
|
848
|
+
const isolatedJobs = Math.min(
|
|
849
|
+
requestedIsolated,
|
|
850
|
+
options.isolatedChunkCount,
|
|
851
|
+
Math.max(1, options.totalJobs - 1),
|
|
852
|
+
);
|
|
853
|
+
const parallelJobs = Math.min(
|
|
854
|
+
options.parallelChunkCount,
|
|
855
|
+
Math.max(1, options.totalJobs - isolatedJobs),
|
|
856
|
+
);
|
|
857
|
+
return { parallelJobs, isolatedJobs };
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export function resolveStrictIsolatedTestJobs(options: {
|
|
861
|
+
requested?: number;
|
|
862
|
+
env?: NodeJS.ProcessEnv;
|
|
863
|
+
chunkCount: number;
|
|
864
|
+
}): number {
|
|
865
|
+
if (options.chunkCount <= 1) {
|
|
866
|
+
return 1;
|
|
867
|
+
}
|
|
868
|
+
const fromEnv = options.env?.FORGE_VERIFY_ISOLATED_TEST_JOBS;
|
|
869
|
+
const parsedEnv = fromEnv ? Number(fromEnv) : undefined;
|
|
870
|
+
const requested = options.requested ?? parsedEnv;
|
|
871
|
+
if (requested !== undefined && Number.isInteger(requested) && requested >= 1) {
|
|
872
|
+
return Math.min(requested, options.chunkCount);
|
|
873
|
+
}
|
|
874
|
+
return Math.min(STRICT_ISOLATED_TEST_MAX_DEFAULT_JOBS, options.chunkCount);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function normalizeTestPath(file: string): string {
|
|
878
|
+
return file.replace(/\\/g, "/");
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export type StrictTestLane = VerifyTestGraphLane;
|
|
882
|
+
|
|
883
|
+
export function classifyStrictTestFile(file: string): StrictTestLane {
|
|
884
|
+
const normalized = normalizeTestPath(file);
|
|
885
|
+
if (STRICT_SERIAL_TEST_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
886
|
+
return "serial";
|
|
887
|
+
}
|
|
888
|
+
if (STRICT_ISOLATED_TEST_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
889
|
+
return "isolated";
|
|
890
|
+
}
|
|
891
|
+
return "parallel";
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function testGraphProfilePath(workspaceRoot: string): string {
|
|
895
|
+
return join(workspaceRoot, TESTGRAPH_PROFILE_RELATIVE_PATH);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function testFileSourceHash(workspaceRoot: string, file: string): string | null {
|
|
899
|
+
const source = nodeFileSystem.readText(join(workspaceRoot, file));
|
|
900
|
+
if (source === null) {
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
return createHash("sha256")
|
|
904
|
+
.update(normalizeTestPath(file))
|
|
905
|
+
.update("\0")
|
|
906
|
+
.update(source)
|
|
907
|
+
.digest("hex")
|
|
908
|
+
.slice(0, 16);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function readTestGraphProfile(workspaceRoot: string): TestGraphProfileFile | null {
|
|
912
|
+
const raw = nodeFileSystem.readText(testGraphProfilePath(workspaceRoot));
|
|
913
|
+
if (!raw) {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
const parsed = JSON.parse(raw) as TestGraphProfileFile;
|
|
918
|
+
if (parsed.schemaVersion !== "0.1.0" || typeof parsed.files !== "object") {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
return parsed;
|
|
922
|
+
} catch {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function estimateStrictTestEntry(
|
|
928
|
+
workspaceRoot: string,
|
|
929
|
+
file: string,
|
|
930
|
+
cost: TestCost,
|
|
931
|
+
lane: StrictTestLane,
|
|
932
|
+
profile: TestGraphProfileFile | null,
|
|
933
|
+
): { estimatedMs: number; source: VerifyTestGraphDurationSource } {
|
|
934
|
+
const profiled = profile?.files[file];
|
|
935
|
+
const sourceHash = testFileSourceHash(workspaceRoot, file);
|
|
936
|
+
if (
|
|
937
|
+
profiled &&
|
|
938
|
+
sourceHash !== null &&
|
|
939
|
+
profiled.sourceHash === sourceHash &&
|
|
940
|
+
Number.isFinite(profiled.durationMs) &&
|
|
941
|
+
profiled.durationMs > 0
|
|
942
|
+
) {
|
|
943
|
+
return { estimatedMs: Math.max(1, Math.round(profiled.durationMs)), source: "profile" };
|
|
944
|
+
}
|
|
945
|
+
const normalized = normalizeTestPath(file);
|
|
946
|
+
const pathOverride = STRICT_TEST_FALLBACK_MS_BY_PATH.find((entry) => entry.pattern.test(normalized));
|
|
947
|
+
const fallback = pathOverride?.estimatedMs ?? TEST_COST_FALLBACK_MS[cost] ?? TEST_COST_FALLBACK_MS.standard;
|
|
948
|
+
if (lane === "serial") {
|
|
949
|
+
return { estimatedMs: Math.max(fallback, 8_000), source: "fallback" };
|
|
950
|
+
}
|
|
951
|
+
if (lane === "isolated") {
|
|
952
|
+
return { estimatedMs: Math.max(fallback, 3_000), source: "fallback" };
|
|
953
|
+
}
|
|
954
|
+
return { estimatedMs: fallback, source: "fallback" };
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function weightedStrictTestEntries(
|
|
958
|
+
workspaceRoot: string,
|
|
959
|
+
profile: TestGraphProfileFile | null,
|
|
960
|
+
): StrictTestEntry[] {
|
|
961
|
+
return strictTestEntries(workspaceRoot).map(({ file, cost }) => {
|
|
962
|
+
const lane = classifyStrictTestFile(file);
|
|
963
|
+
const estimate = estimateStrictTestEntry(workspaceRoot, file, cost, lane, profile);
|
|
964
|
+
return {
|
|
965
|
+
file,
|
|
966
|
+
cost,
|
|
967
|
+
lane,
|
|
968
|
+
estimatedMs: estimate.estimatedMs,
|
|
969
|
+
durationSource: estimate.source,
|
|
970
|
+
};
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function partitionStrictTestEntries(entries: StrictTestEntry[]): {
|
|
975
|
+
parallel: StrictTestEntry[];
|
|
976
|
+
isolated: StrictTestEntry[];
|
|
977
|
+
serial: StrictTestEntry[];
|
|
978
|
+
} {
|
|
979
|
+
const parallel: StrictTestEntry[] = [];
|
|
980
|
+
const isolated: StrictTestEntry[] = [];
|
|
981
|
+
const serial: StrictTestEntry[] = [];
|
|
982
|
+
for (const entry of entries) {
|
|
983
|
+
const lane = entry.lane;
|
|
984
|
+
if (lane === "serial") {
|
|
985
|
+
serial.push(entry);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
if (lane === "isolated") {
|
|
989
|
+
isolated.push(entry);
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
parallel.push(entry);
|
|
993
|
+
}
|
|
994
|
+
return { parallel, isolated, serial };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export function packWeightedStrictTestChunks(
|
|
998
|
+
entries: Array<{ file: string; estimatedMs: number; durationSource: VerifyTestGraphDurationSource }>,
|
|
999
|
+
size: number,
|
|
1000
|
+
): Array<{ files: string[]; estimatedMs: number; durationSource: VerifyTestGraphDurationSource }> {
|
|
1001
|
+
if (entries.length === 0) {
|
|
1002
|
+
return [];
|
|
1003
|
+
}
|
|
1004
|
+
const binCount = Math.max(1, Math.ceil(entries.length / Math.max(1, size)));
|
|
1005
|
+
const bins = Array.from({ length: binCount }, () => ({
|
|
1006
|
+
files: [] as string[],
|
|
1007
|
+
estimatedMs: 0,
|
|
1008
|
+
durationSource: "profile" as VerifyTestGraphDurationSource,
|
|
1009
|
+
}));
|
|
1010
|
+
const ordered = [...entries].sort((left, right) => {
|
|
1011
|
+
const byEstimate = right.estimatedMs - left.estimatedMs;
|
|
1012
|
+
return byEstimate !== 0 ? byEstimate : left.file.localeCompare(right.file);
|
|
1013
|
+
});
|
|
1014
|
+
for (const entry of ordered) {
|
|
1015
|
+
const target = bins
|
|
1016
|
+
.filter((bin) => bin.files.length < size)
|
|
1017
|
+
.sort((left, right) => {
|
|
1018
|
+
const byEstimate = left.estimatedMs - right.estimatedMs;
|
|
1019
|
+
return byEstimate !== 0 ? byEstimate : left.files.length - right.files.length;
|
|
1020
|
+
})[0] ?? bins[0]!;
|
|
1021
|
+
target.files.push(entry.file);
|
|
1022
|
+
target.files.sort();
|
|
1023
|
+
target.estimatedMs += entry.estimatedMs;
|
|
1024
|
+
if (entry.durationSource === "fallback") {
|
|
1025
|
+
target.durationSource = "fallback";
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return bins.filter((bin) => bin.files.length > 0);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function oneFileChunks(
|
|
1032
|
+
entries: StrictTestEntry[],
|
|
1033
|
+
): Array<{ files: string[]; estimatedMs: number; durationSource: VerifyTestGraphDurationSource }> {
|
|
1034
|
+
return [...entries]
|
|
1035
|
+
.sort((left, right) => {
|
|
1036
|
+
const byEstimate = right.estimatedMs - left.estimatedMs;
|
|
1037
|
+
return byEstimate !== 0 ? byEstimate : left.file.localeCompare(right.file);
|
|
1038
|
+
})
|
|
1039
|
+
.map((entry) => ({
|
|
1040
|
+
files: [entry.file],
|
|
1041
|
+
estimatedMs: entry.estimatedMs,
|
|
1042
|
+
durationSource: entry.durationSource,
|
|
1043
|
+
}));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function laneEstimate(chunks: VerifyTestGraphPlanChunk[], jobs: number): number {
|
|
1047
|
+
const workers = Array.from({ length: Math.max(1, jobs) }, () => 0);
|
|
1048
|
+
for (const chunk of chunks) {
|
|
1049
|
+
workers.sort((left, right) => left - right);
|
|
1050
|
+
workers[0] += chunk.estimatedMs;
|
|
1051
|
+
}
|
|
1052
|
+
return Math.max(...workers, 0);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function strictPlanRecommendations(plan: VerifyTestGraphPlan): string[] {
|
|
1056
|
+
const recommendations: string[] = [];
|
|
1057
|
+
if (!plan.profileFound) {
|
|
1058
|
+
recommendations.push(`Run forge verify --strict once to create ${TESTGRAPH_PROFILE_RELATIVE_PATH}; later plans use measured durations.`);
|
|
1059
|
+
}
|
|
1060
|
+
if (plan.lanes.serial.chunkCount > 0 && plan.lanes.serial.estimatedMs > plan.criticalPathEstimateMs * 0.35) {
|
|
1061
|
+
recommendations.push("Split or de-globalize the slowest serial tests; serial work is now the main critical-path limiter.");
|
|
1062
|
+
} else if (plan.lanes.serial.chunkCount === 0 && plan.lanes.isolated.chunkCount > 0) {
|
|
1063
|
+
recommendations.push("No current strict TestGraph files require the serial lane; optimize isolated runtime/template tests next.");
|
|
1064
|
+
}
|
|
1065
|
+
if (plan.lanes.isolated.chunkCount > 0 && plan.isolatedJobs < STRICT_ISOLATED_TEST_MAX_DEFAULT_JOBS) {
|
|
1066
|
+
recommendations.push(`Set FORGE_VERIFY_ISOLATED_TEST_JOBS=${STRICT_ISOLATED_TEST_MAX_DEFAULT_JOBS} on machines that can run isolated runtime tests concurrently.`);
|
|
1067
|
+
}
|
|
1068
|
+
const slowest = plan.slowestFiles[0];
|
|
1069
|
+
if (slowest) {
|
|
1070
|
+
recommendations.push(`Inspect ${slowest.file}; it is currently the heaviest estimated TestGraph file.`);
|
|
1071
|
+
}
|
|
1072
|
+
return recommendations;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export function buildStrictTestGraphPlan(
|
|
1076
|
+
workspaceRoot: string,
|
|
1077
|
+
testJobs?: number,
|
|
1078
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1079
|
+
): VerifyTestGraphPlan {
|
|
1080
|
+
const profile = readTestGraphProfile(workspaceRoot);
|
|
1081
|
+
const entries = weightedStrictTestEntries(workspaceRoot, profile);
|
|
1082
|
+
const partitioned = partitionStrictTestEntries(entries);
|
|
1083
|
+
const parallelRaw = packWeightedStrictTestChunks(partitioned.parallel, STRICT_TEST_CHUNK_SIZE);
|
|
1084
|
+
const isolatedRaw = oneFileChunks(partitioned.isolated);
|
|
1085
|
+
const serialRaw = oneFileChunks(partitioned.serial);
|
|
1086
|
+
const totalJobs = resolveStrictTestJobs({
|
|
1087
|
+
requested: testJobs,
|
|
1088
|
+
env,
|
|
1089
|
+
chunkCount: parallelRaw.length + isolatedRaw.length,
|
|
1090
|
+
});
|
|
1091
|
+
const { parallelJobs, isolatedJobs } = resolveStrictLaneJobs({
|
|
1092
|
+
totalJobs,
|
|
1093
|
+
parallelChunkCount: parallelRaw.length,
|
|
1094
|
+
isolatedChunkCount: isolatedRaw.length,
|
|
1095
|
+
env,
|
|
1096
|
+
});
|
|
1097
|
+
let index = 1;
|
|
1098
|
+
const toPlanChunks = (
|
|
1099
|
+
lane: StrictTestLane,
|
|
1100
|
+
chunks: Array<{ files: string[]; estimatedMs: number; durationSource: VerifyTestGraphDurationSource }>,
|
|
1101
|
+
): VerifyTestGraphPlanChunk[] => chunks.map((chunk) => ({
|
|
1102
|
+
index: index++,
|
|
1103
|
+
lane,
|
|
1104
|
+
files: chunk.files,
|
|
1105
|
+
estimatedMs: chunk.estimatedMs,
|
|
1106
|
+
durationSource: chunk.durationSource,
|
|
1107
|
+
}));
|
|
1108
|
+
const parallelChunks = toPlanChunks("parallel", parallelRaw);
|
|
1109
|
+
const isolatedChunks = toPlanChunks("isolated", isolatedRaw);
|
|
1110
|
+
const serialChunks = toPlanChunks("serial", serialRaw);
|
|
1111
|
+
const laneMode =
|
|
1112
|
+
totalJobs <= 1 && parallelChunks.length > 0 && isolatedChunks.length > 0
|
|
1113
|
+
? "sequential"
|
|
1114
|
+
: "overlap";
|
|
1115
|
+
const chunks = [...parallelChunks, ...isolatedChunks, ...serialChunks];
|
|
1116
|
+
const lanes = {
|
|
1117
|
+
parallel: {
|
|
1118
|
+
fileCount: partitioned.parallel.length,
|
|
1119
|
+
chunkCount: parallelChunks.length,
|
|
1120
|
+
estimatedMs: parallelChunks.reduce((sum, chunk) => sum + chunk.estimatedMs, 0),
|
|
1121
|
+
},
|
|
1122
|
+
isolated: {
|
|
1123
|
+
fileCount: partitioned.isolated.length,
|
|
1124
|
+
chunkCount: isolatedChunks.length,
|
|
1125
|
+
estimatedMs: isolatedChunks.reduce((sum, chunk) => sum + chunk.estimatedMs, 0),
|
|
1126
|
+
},
|
|
1127
|
+
serial: {
|
|
1128
|
+
fileCount: partitioned.serial.length,
|
|
1129
|
+
chunkCount: serialChunks.length,
|
|
1130
|
+
estimatedMs: serialChunks.reduce((sum, chunk) => sum + chunk.estimatedMs, 0),
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
const plan: VerifyTestGraphPlan = {
|
|
1134
|
+
schemaVersion: "0.1.0",
|
|
1135
|
+
fileCount: entries.length,
|
|
1136
|
+
chunkCount: chunks.length,
|
|
1137
|
+
totalJobs,
|
|
1138
|
+
laneMode,
|
|
1139
|
+
jobs: parallelJobs,
|
|
1140
|
+
isolatedJobs,
|
|
1141
|
+
lanes,
|
|
1142
|
+
chunks,
|
|
1143
|
+
criticalPathEstimateMs:
|
|
1144
|
+
(laneMode === "sequential"
|
|
1145
|
+
? laneEstimate(parallelChunks, parallelJobs) + laneEstimate(isolatedChunks, isolatedJobs)
|
|
1146
|
+
: Math.max(
|
|
1147
|
+
laneEstimate(parallelChunks, parallelJobs),
|
|
1148
|
+
laneEstimate(isolatedChunks, isolatedJobs),
|
|
1149
|
+
)) +
|
|
1150
|
+
lanes.serial.estimatedMs,
|
|
1151
|
+
profilePath: TESTGRAPH_PROFILE_RELATIVE_PATH,
|
|
1152
|
+
profileFound: profile !== null,
|
|
1153
|
+
slowestFiles: [...entries]
|
|
1154
|
+
.sort((left, right) => {
|
|
1155
|
+
const byEstimate = right.estimatedMs - left.estimatedMs;
|
|
1156
|
+
return byEstimate !== 0 ? byEstimate : left.file.localeCompare(right.file);
|
|
1157
|
+
})
|
|
1158
|
+
.slice(0, 10)
|
|
1159
|
+
.map((entry) => ({
|
|
1160
|
+
file: entry.file,
|
|
1161
|
+
lane: entry.lane,
|
|
1162
|
+
estimatedMs: entry.estimatedMs,
|
|
1163
|
+
source: entry.durationSource,
|
|
1164
|
+
})),
|
|
1165
|
+
recommendations: [],
|
|
1166
|
+
};
|
|
1167
|
+
plan.recommendations = strictPlanRecommendations(plan);
|
|
1168
|
+
return plan;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
async function runStrictGraphChunkPool(
|
|
1172
|
+
workspaceRoot: string,
|
|
1173
|
+
chunks: VerifyTestGraphPlanChunk[],
|
|
1174
|
+
timeoutMs: number,
|
|
1175
|
+
jobs: number,
|
|
1176
|
+
totalChunks = chunks.length,
|
|
1177
|
+
): Promise<{ results: Array<StrictGraphChunkResult | undefined>; timedOut: boolean }> {
|
|
1178
|
+
const results: Array<StrictGraphChunkResult | undefined> = [];
|
|
1179
|
+
let nextChunk = 0;
|
|
1180
|
+
let stopScheduling = false;
|
|
1181
|
+
|
|
1182
|
+
async function runNextChunk(): Promise<void> {
|
|
1183
|
+
while (!stopScheduling) {
|
|
1184
|
+
const chunkIndex = nextChunk;
|
|
1185
|
+
nextChunk += 1;
|
|
1186
|
+
const chunk = chunks[chunkIndex];
|
|
1187
|
+
if (!chunk) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const result = await runStrictGraphTestChunk(
|
|
1191
|
+
workspaceRoot,
|
|
1192
|
+
chunk.files,
|
|
1193
|
+
chunk.index - 1,
|
|
1194
|
+
totalChunks,
|
|
1195
|
+
timeoutMs,
|
|
1196
|
+
);
|
|
1197
|
+
results[chunkIndex] = {
|
|
1198
|
+
...result,
|
|
1199
|
+
files: chunk.files,
|
|
1200
|
+
lane: chunk.lane,
|
|
1201
|
+
chunkIndex: chunk.index,
|
|
1202
|
+
chunkCount: totalChunks,
|
|
1203
|
+
};
|
|
1204
|
+
if (result.exitCode !== 0) {
|
|
1205
|
+
stopScheduling = true;
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
await Promise.all(Array.from({ length: jobs }, () => runNextChunk()));
|
|
1212
|
+
return {
|
|
1213
|
+
results,
|
|
1214
|
+
timedOut: results.some((result) => result?.timedOut),
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function writeTestGraphProfile(
|
|
1219
|
+
workspaceRoot: string,
|
|
1220
|
+
results: Array<StrictGraphChunkResult | undefined>,
|
|
1221
|
+
): void {
|
|
1222
|
+
const existing = readTestGraphProfile(workspaceRoot);
|
|
1223
|
+
const now = new Date().toISOString();
|
|
1224
|
+
const files = { ...(existing?.files ?? {}) };
|
|
1225
|
+
for (const result of results) {
|
|
1226
|
+
if (!result || result.files.length === 0) {
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const perFileDuration = Math.max(1, Math.round(result.durationMs / result.files.length));
|
|
1230
|
+
for (const file of result.files) {
|
|
1231
|
+
const previous = files[file];
|
|
1232
|
+
files[file] = {
|
|
1233
|
+
durationMs: perFileDuration,
|
|
1234
|
+
runs: (previous?.runs ?? 0) + 1,
|
|
1235
|
+
lane: result.lane,
|
|
1236
|
+
sourceHash: testFileSourceHash(workspaceRoot, file) ?? previous?.sourceHash,
|
|
1237
|
+
lastExitCode: result.exitCode,
|
|
1238
|
+
lastRunAt: now,
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const profile: TestGraphProfileFile = {
|
|
1243
|
+
schemaVersion: "0.1.0",
|
|
1244
|
+
updatedAt: now,
|
|
1245
|
+
files,
|
|
1246
|
+
};
|
|
1247
|
+
const path = testGraphProfilePath(workspaceRoot);
|
|
1248
|
+
nodeFileSystem.mkdirp(dirname(path));
|
|
1249
|
+
nodeFileSystem.writeText(path, `${JSON.stringify(profile, null, 2)}\n`);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function strictGraphChunkToTestRunStep(result: StrictGraphChunkResult, timeoutMs: number): TestRunStep & {
|
|
1253
|
+
files: string[];
|
|
1254
|
+
lane: StrictTestLane;
|
|
1255
|
+
chunkIndex: number;
|
|
1256
|
+
chunkCount: number;
|
|
1257
|
+
reproduceCommand: string;
|
|
1258
|
+
} {
|
|
1259
|
+
return {
|
|
1260
|
+
command: result.command,
|
|
1261
|
+
ok: result.exitCode === 0,
|
|
1262
|
+
exitCode: result.exitCode,
|
|
1263
|
+
durationMs: result.durationMs,
|
|
1264
|
+
timedOut: result.timedOut,
|
|
1265
|
+
failureKind: packageScriptFailureKind(result),
|
|
1266
|
+
stdout: result.stdout,
|
|
1267
|
+
stderr: result.stderr,
|
|
1268
|
+
files: result.files,
|
|
1269
|
+
lane: result.lane,
|
|
1270
|
+
chunkIndex: result.chunkIndex,
|
|
1271
|
+
chunkCount: result.chunkCount,
|
|
1272
|
+
reproduceCommand: `bun test ${result.files.join(" ")} --timeout ${timeoutMs}`,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function writeStrictGraphTestRunRecord(
|
|
1277
|
+
workspaceRoot: string,
|
|
1278
|
+
plan: VerifyTestGraphPlan,
|
|
1279
|
+
results: StrictGraphChunkResult[],
|
|
1280
|
+
timeoutMs: number,
|
|
1281
|
+
durationMs: number,
|
|
1282
|
+
): TestRunRecord {
|
|
1283
|
+
const commands = results.map((result) => result.command);
|
|
1284
|
+
const record: TestRunRecord = {
|
|
1285
|
+
schemaVersion: "0.1.0",
|
|
1286
|
+
id: `run_${createHash("sha256")
|
|
1287
|
+
.update(`${Date.now()}:${commands.join("|")}:${plan.fileCount}:${plan.chunkCount}`)
|
|
1288
|
+
.digest("hex")
|
|
1289
|
+
.slice(0, 12)}`,
|
|
1290
|
+
changedHash: `sha256:${createHash("sha256").update(canonicalJson(plan.chunks)).digest("hex")}`,
|
|
1291
|
+
planHash: `sha256:${createHash("sha256").update(canonicalJson(plan)).digest("hex")}`,
|
|
1292
|
+
source: { mode: "changed", id: "verify-strict-testgraph" },
|
|
1293
|
+
commands,
|
|
1294
|
+
timeoutMs,
|
|
1295
|
+
results: results.map((result) => strictGraphChunkToTestRunStep(result, timeoutMs)),
|
|
1296
|
+
failed: results.filter((result) => result.exitCode !== 0).map((result) => result.command),
|
|
1297
|
+
durationMs,
|
|
1298
|
+
};
|
|
1299
|
+
const runDir = join(workspaceRoot, ".forge/test-runs");
|
|
1300
|
+
nodeFileSystem.mkdirp(runDir);
|
|
1301
|
+
nodeFileSystem.writeText(join(runDir, "last.json"), serializeCanonical(record));
|
|
1302
|
+
nodeFileSystem.writeText(join(runDir, `${record.id}.json`), serializeCanonical(record));
|
|
1303
|
+
return record;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async function runStrictGraphTests(
|
|
1307
|
+
workspaceRoot: string,
|
|
1308
|
+
timeoutMs: number,
|
|
1309
|
+
testJobs?: number,
|
|
1310
|
+
): Promise<ScriptRunResult & {
|
|
1311
|
+
fileCount: number;
|
|
1312
|
+
chunkCount: number;
|
|
1313
|
+
jobs: number;
|
|
1314
|
+
isolatedJobs: number;
|
|
1315
|
+
plan: VerifyTestGraphPlan;
|
|
1316
|
+
failedFiles: string[];
|
|
1317
|
+
failedChunk?: number;
|
|
1318
|
+
reportPath?: string;
|
|
1319
|
+
}> {
|
|
1320
|
+
const plan = buildStrictTestGraphPlan(workspaceRoot, testJobs);
|
|
1321
|
+
if (plan.fileCount === 0) {
|
|
1322
|
+
return {
|
|
1323
|
+
exitCode: 1,
|
|
1324
|
+
stdout: "",
|
|
1325
|
+
stderr: "TestGraph has no non-docker/browser tests",
|
|
1326
|
+
command: "forge strict TestGraph tests",
|
|
1327
|
+
durationMs: 0,
|
|
1328
|
+
timedOut: false,
|
|
1329
|
+
spawnError: true,
|
|
1330
|
+
fileCount: 0,
|
|
1331
|
+
chunkCount: 0,
|
|
1332
|
+
jobs: 0,
|
|
1333
|
+
isolatedJobs: 0,
|
|
1334
|
+
plan,
|
|
1335
|
+
failedFiles: [],
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const started = Date.now();
|
|
1340
|
+
let stdout = "";
|
|
1341
|
+
let stderr = "";
|
|
1342
|
+
let timedOut = false;
|
|
1343
|
+
let exitCode = 0;
|
|
1344
|
+
let failedOutput: Pick<ScriptRunResult, "stdout" | "stderr"> | null = null;
|
|
1345
|
+
const parallelChunks = plan.chunks.filter((chunk) => chunk.lane === "parallel");
|
|
1346
|
+
const isolatedChunks = plan.chunks.filter((chunk) => chunk.lane === "isolated");
|
|
1347
|
+
const serialChunks = plan.chunks.filter((chunk) => chunk.lane === "serial");
|
|
1348
|
+
|
|
1349
|
+
let command = `bun test <${plan.fileCount} TestGraph files in ${plan.chunkCount} chunks, ${plan.laneMode} lanes, total jobs ${plan.totalJobs}, parallel jobs ${plan.jobs}, isolated jobs ${plan.isolatedJobs}, isolated ${isolatedChunks.length}, serial ${serialChunks.length}> --timeout ${timeoutMs}`;
|
|
1350
|
+
const parallelPool = () => runStrictGraphChunkPool(
|
|
1351
|
+
workspaceRoot,
|
|
1352
|
+
parallelChunks,
|
|
1353
|
+
timeoutMs,
|
|
1354
|
+
plan.jobs,
|
|
1355
|
+
plan.chunkCount,
|
|
1356
|
+
);
|
|
1357
|
+
const isolatedPool = () => runStrictGraphChunkPool(
|
|
1358
|
+
workspaceRoot,
|
|
1359
|
+
isolatedChunks,
|
|
1360
|
+
timeoutMs,
|
|
1361
|
+
plan.isolatedJobs,
|
|
1362
|
+
plan.chunkCount,
|
|
1363
|
+
);
|
|
1364
|
+
const [parallelRun, isolatedRun] = plan.laneMode === "sequential"
|
|
1365
|
+
? [await parallelPool(), await isolatedPool()]
|
|
1366
|
+
: await Promise.all([parallelPool(), isolatedPool()]);
|
|
1367
|
+
const orderedResults: StrictGraphChunkResult[] = [];
|
|
1368
|
+
for (const result of parallelRun.results) {
|
|
1369
|
+
if (result) {
|
|
1370
|
+
orderedResults.push(result);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
for (const result of isolatedRun.results) {
|
|
1374
|
+
if (result) {
|
|
1375
|
+
orderedResults.push(result);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
timedOut = timedOut || parallelRun.timedOut;
|
|
1379
|
+
timedOut = timedOut || isolatedRun.timedOut;
|
|
1380
|
+
|
|
1381
|
+
if (
|
|
1382
|
+
parallelRun.results.every((result) => result?.exitCode === 0) &&
|
|
1383
|
+
isolatedRun.results.every((result) => result?.exitCode === 0)
|
|
1384
|
+
) {
|
|
1385
|
+
for (const chunk of serialChunks) {
|
|
1386
|
+
const result = await runStrictGraphTestChunk(
|
|
1387
|
+
workspaceRoot,
|
|
1388
|
+
chunk.files,
|
|
1389
|
+
chunk.index - 1,
|
|
1390
|
+
plan.chunkCount,
|
|
1391
|
+
timeoutMs,
|
|
1392
|
+
);
|
|
1393
|
+
orderedResults.push({
|
|
1394
|
+
...result,
|
|
1395
|
+
files: chunk.files,
|
|
1396
|
+
lane: chunk.lane,
|
|
1397
|
+
chunkIndex: chunk.index,
|
|
1398
|
+
chunkCount: plan.chunkCount,
|
|
1399
|
+
});
|
|
1400
|
+
timedOut = timedOut || result.timedOut;
|
|
1401
|
+
if (result.exitCode !== 0) {
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
writeTestGraphProfile(workspaceRoot, orderedResults);
|
|
1408
|
+
|
|
1409
|
+
let failedResult: StrictGraphChunkResult | undefined;
|
|
1410
|
+
for (const result of orderedResults) {
|
|
1411
|
+
if (!result) {
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
stdout += result.stdout;
|
|
1415
|
+
stderr += result.stderr;
|
|
1416
|
+
timedOut = timedOut || result.timedOut;
|
|
1417
|
+
if (result.exitCode !== 0) {
|
|
1418
|
+
exitCode = result.exitCode;
|
|
1419
|
+
command = result.command;
|
|
1420
|
+
failedOutput = { stdout: result.stdout, stderr: result.stderr };
|
|
1421
|
+
failedResult = result;
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const report = writeStrictGraphTestRunRecord(
|
|
1427
|
+
workspaceRoot,
|
|
1428
|
+
plan,
|
|
1429
|
+
orderedResults,
|
|
1430
|
+
timeoutMs,
|
|
1431
|
+
Date.now() - started,
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
return {
|
|
1435
|
+
exitCode,
|
|
1436
|
+
stdout: failedOutput?.stdout ?? stdout,
|
|
1437
|
+
stderr: failedOutput?.stderr ?? stderr,
|
|
1438
|
+
command,
|
|
1439
|
+
durationMs: Date.now() - started,
|
|
1440
|
+
timedOut,
|
|
1441
|
+
fileCount: plan.fileCount,
|
|
1442
|
+
chunkCount: plan.chunkCount,
|
|
1443
|
+
jobs: plan.jobs,
|
|
1444
|
+
isolatedJobs: plan.isolatedJobs,
|
|
1445
|
+
plan,
|
|
1446
|
+
failedFiles: failedResult?.files ?? [],
|
|
1447
|
+
failedChunk: failedResult?.chunkIndex,
|
|
1448
|
+
reportPath: `.forge/test-runs/${report.id}.json`,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function runStrictGraphTestChunk(
|
|
1453
|
+
workspaceRoot: string,
|
|
1454
|
+
chunk: string[],
|
|
1455
|
+
chunkIndex: number,
|
|
1456
|
+
chunkCount: number,
|
|
1457
|
+
timeoutMs: number,
|
|
1458
|
+
): Promise<ScriptRunResult> {
|
|
1459
|
+
const argv = resolveCommandArgv(["bun", "test", ...chunk, "--timeout", String(timeoutMs)]);
|
|
1460
|
+
const chunkCommand = `bun test <TestGraph chunk ${chunkIndex + 1}/${chunkCount}, ${chunk.length} files> --timeout ${timeoutMs}`;
|
|
1461
|
+
const chunkTempDir = mkdtempSync(join(tmpdir(), `forge-testgraph-${chunkIndex + 1}-`));
|
|
1462
|
+
return spawnArgv(workspaceRoot, argv, timeoutMs, chunkCommand, {
|
|
1463
|
+
TMP: chunkTempDir,
|
|
1464
|
+
TEMP: chunkTempDir,
|
|
1465
|
+
TMPDIR: chunkTempDir,
|
|
1466
|
+
FORGE_TEST_TMPDIR: chunkTempDir,
|
|
1467
|
+
FORGE_VERIFY_CHUNK_INDEX: String(chunkIndex + 1),
|
|
1468
|
+
FORGE_VERIFY_CHUNK_COUNT: String(chunkCount),
|
|
1469
|
+
FORGE_DEV_PORT: "0",
|
|
1470
|
+
}).finally(() => {
|
|
1471
|
+
rmSync(chunkTempDir, { recursive: true, force: true });
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
232
1475
|
function resolveVerifyProfile(options: VerifyOptions): VerifyProfile {
|
|
1476
|
+
if (options.internal) {
|
|
1477
|
+
return "internal";
|
|
1478
|
+
}
|
|
233
1479
|
if (options.changed) {
|
|
234
1480
|
return "changed";
|
|
235
1481
|
}
|
|
@@ -247,7 +1493,7 @@ function resolveVerifyProfile(options: VerifyOptions): VerifyProfile {
|
|
|
247
1493
|
|
|
248
1494
|
async function runStandardImpactTests(
|
|
249
1495
|
options: VerifyOptions,
|
|
250
|
-
): Promise<{ steps: VerifyStep[]; diagnostics: Diagnostic[] }> {
|
|
1496
|
+
): Promise<{ steps: VerifyStep[]; diagnostics: Diagnostic[]; testCoverageReason?: string }> {
|
|
251
1497
|
const started = Date.now();
|
|
252
1498
|
const diagnostics: Diagnostic[] = [];
|
|
253
1499
|
const steps: VerifyStep[] = [];
|
|
@@ -270,8 +1516,23 @@ async function runStandardImpactTests(
|
|
|
270
1516
|
const commands = impactOnlyPlan.tests.map((test) => test.command);
|
|
271
1517
|
|
|
272
1518
|
if (commands.length === 0) {
|
|
1519
|
+
const reason = plan.derivedOnly
|
|
1520
|
+
? "changed files are derived generated artifacts only"
|
|
1521
|
+
: "impact planner selected no test files for the current changes";
|
|
273
1522
|
steps.push(skippedStep("impact-tests", "no changed files selected an impact test"));
|
|
274
|
-
|
|
1523
|
+
diagnostics.push(
|
|
1524
|
+
createDiagnostic({
|
|
1525
|
+
severity: "warning",
|
|
1526
|
+
code: FORGE_VERIFY_NO_TESTS_SELECTED,
|
|
1527
|
+
message: "standard verification did not select any impact tests; only non-test checks ran",
|
|
1528
|
+
fixHint: "Run forge verify --strict when you need full test-suite coverage.",
|
|
1529
|
+
suggestedCommands: [
|
|
1530
|
+
"forge test plan --changed --json",
|
|
1531
|
+
"forge verify --strict",
|
|
1532
|
+
],
|
|
1533
|
+
}),
|
|
1534
|
+
);
|
|
1535
|
+
return { steps, diagnostics, testCoverageReason: reason };
|
|
275
1536
|
}
|
|
276
1537
|
|
|
277
1538
|
const record = await runImpactTestPlan(options.workspaceRoot, impactOnlyPlan, {
|
|
@@ -331,6 +1592,69 @@ export async function runVerifyCommand(
|
|
|
331
1592
|
const scripts = readPackageScripts(options.workspaceRoot);
|
|
332
1593
|
const scriptTimeoutMs = resolveScriptTimeoutMs(options);
|
|
333
1594
|
const profile = resolveVerifyProfile(options);
|
|
1595
|
+
const frameworkWorkspace = isForgeOsFrameworkWorkspace(options.workspaceRoot);
|
|
1596
|
+
const canRunInternalTests = options.internal || !frameworkWorkspace;
|
|
1597
|
+
let testGraphPlan: VerifyTestGraphPlan | undefined;
|
|
1598
|
+
let testCoverageReason: string | undefined;
|
|
1599
|
+
|
|
1600
|
+
if (options.testPlan) {
|
|
1601
|
+
if (frameworkWorkspace && !options.internal) {
|
|
1602
|
+
steps.push({
|
|
1603
|
+
name: "tests:framework-testgraph-plan",
|
|
1604
|
+
ok: true,
|
|
1605
|
+
skipped: true,
|
|
1606
|
+
skipReason: "ForgeOS framework TestGraph is maintainer-only; use forge verify framework --test-plan --json",
|
|
1607
|
+
});
|
|
1608
|
+
diagnostics.push(
|
|
1609
|
+
createDiagnostic({
|
|
1610
|
+
severity: "warning",
|
|
1611
|
+
code: "FORGE_VERIFY_INTERNAL_TESTS_SKIPPED",
|
|
1612
|
+
message: "Skipped ForgeOS framework TestGraph plan during app-level verify.",
|
|
1613
|
+
fixHint: "Run forge verify framework --test-plan --json when maintaining ForgeOS itself.",
|
|
1614
|
+
suggestedCommands: [
|
|
1615
|
+
"forge verify framework --test-plan --json",
|
|
1616
|
+
"forge verify --standard --json",
|
|
1617
|
+
],
|
|
1618
|
+
}),
|
|
1619
|
+
);
|
|
1620
|
+
return {
|
|
1621
|
+
ok: true,
|
|
1622
|
+
profile,
|
|
1623
|
+
steps,
|
|
1624
|
+
diagnostics,
|
|
1625
|
+
durationMs: Date.now() - started,
|
|
1626
|
+
exitCode: 0,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
testGraphPlan = buildStrictTestGraphPlan(options.workspaceRoot, options.testJobs);
|
|
1630
|
+
steps.push({
|
|
1631
|
+
name: "tests:testgraph-plan",
|
|
1632
|
+
ok: testGraphPlan.fileCount > 0,
|
|
1633
|
+
skipped: false,
|
|
1634
|
+
exitCode: testGraphPlan.fileCount > 0 ? 0 : 1,
|
|
1635
|
+
command: `forge verify --strict --test-plan (${testGraphPlan.fileCount} files, ${testGraphPlan.chunkCount} chunks)`,
|
|
1636
|
+
durationMs: Date.now() - started,
|
|
1637
|
+
});
|
|
1638
|
+
if (testGraphPlan.fileCount === 0) {
|
|
1639
|
+
diagnostics.push(
|
|
1640
|
+
createDiagnostic({
|
|
1641
|
+
severity: "error",
|
|
1642
|
+
code: "FORGE_VERIFY_TESTGRAPH_EMPTY",
|
|
1643
|
+
message: "TestGraph has no non-docker/browser tests",
|
|
1644
|
+
}),
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
const ok = steps.every((step) => step.ok);
|
|
1648
|
+
return {
|
|
1649
|
+
ok,
|
|
1650
|
+
profile,
|
|
1651
|
+
steps,
|
|
1652
|
+
diagnostics,
|
|
1653
|
+
testGraphPlan,
|
|
1654
|
+
durationMs: Date.now() - started,
|
|
1655
|
+
exitCode: ok ? 0 : 1,
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
334
1658
|
|
|
335
1659
|
if (options.changed) {
|
|
336
1660
|
const plan = buildImpactTestPlan({
|
|
@@ -400,7 +1724,7 @@ export async function runVerifyCommand(
|
|
|
400
1724
|
printProgress(options, "verify: forge-check");
|
|
401
1725
|
const checkStarted = Date.now();
|
|
402
1726
|
const forgeCheck = await runCheckCommand(options.workspaceRoot, {
|
|
403
|
-
strictSecrets: options.strict,
|
|
1727
|
+
strictSecrets: options.strict || options.internal === true,
|
|
404
1728
|
});
|
|
405
1729
|
steps.push({
|
|
406
1730
|
name: "forge-check",
|
|
@@ -410,7 +1734,7 @@ export async function runVerifyCommand(
|
|
|
410
1734
|
});
|
|
411
1735
|
diagnostics.push(...forgeCheck.errors, ...forgeCheck.warnings);
|
|
412
1736
|
|
|
413
|
-
if (profile === "strict" || profile === "standard") {
|
|
1737
|
+
if (profile === "strict" || profile === "standard" || profile === "internal") {
|
|
414
1738
|
printProgress(options, "verify: policy-check-strict");
|
|
415
1739
|
const policyStarted = Date.now();
|
|
416
1740
|
const policyCheck = await runPolicyCommand({
|
|
@@ -505,11 +1829,14 @@ export async function runVerifyCommand(
|
|
|
505
1829
|
|
|
506
1830
|
if (options.skipTypecheck) {
|
|
507
1831
|
steps.push(skippedStep("typecheck", "--skip-typecheck"));
|
|
508
|
-
} else if (!scripts.typecheck) {
|
|
509
|
-
steps.push(skippedStep("typecheck", "no typecheck script in package.json"));
|
|
510
1832
|
} else {
|
|
511
|
-
|
|
512
|
-
|
|
1833
|
+
const typechecker = resolveTypechecker(options);
|
|
1834
|
+
printProgress(options, `verify: typecheck (${typechecker}, ${scriptTimeoutMs}ms timeout)`);
|
|
1835
|
+
const { result: typecheck, diagnostics: typecheckDiagnostics } = await runPreferredTypecheck(
|
|
1836
|
+
options,
|
|
1837
|
+
scripts,
|
|
1838
|
+
scriptTimeoutMs,
|
|
1839
|
+
);
|
|
513
1840
|
steps.push({
|
|
514
1841
|
name: "typecheck",
|
|
515
1842
|
ok: typecheck.exitCode === 0,
|
|
@@ -519,6 +1846,7 @@ export async function runVerifyCommand(
|
|
|
519
1846
|
timedOut: typecheck.timedOut,
|
|
520
1847
|
failureKind: packageScriptFailureKind(typecheck),
|
|
521
1848
|
});
|
|
1849
|
+
diagnostics.push(...typecheckDiagnostics);
|
|
522
1850
|
if (typecheck.timedOut) {
|
|
523
1851
|
diagnostics.push(timedOutDiagnostic("typecheck", scriptTimeoutMs));
|
|
524
1852
|
} else if (typecheck.exitCode !== 0) {
|
|
@@ -528,6 +1856,36 @@ export async function runVerifyCommand(
|
|
|
528
1856
|
}
|
|
529
1857
|
}
|
|
530
1858
|
|
|
1859
|
+
if (profile === "smoke" && nodeFileSystem.exists(join(options.workspaceRoot, "web"))) {
|
|
1860
|
+
printProgress(options, "verify: ui-audit");
|
|
1861
|
+
const uiStarted = Date.now();
|
|
1862
|
+
const uiAudit = await runUiCommand({
|
|
1863
|
+
subcommand: "audit",
|
|
1864
|
+
workspaceRoot: options.workspaceRoot,
|
|
1865
|
+
json: false,
|
|
1866
|
+
headed: false,
|
|
1867
|
+
browser: "chromium",
|
|
1868
|
+
trace: "off",
|
|
1869
|
+
screenshot: "off",
|
|
1870
|
+
video: "off",
|
|
1871
|
+
baseUrl: "http://127.0.0.1:3000",
|
|
1872
|
+
runtimeUrl: "http://127.0.0.1:3765",
|
|
1873
|
+
reuseServers: true,
|
|
1874
|
+
startServers: false,
|
|
1875
|
+
all: false,
|
|
1876
|
+
changed: false,
|
|
1877
|
+
ci: false,
|
|
1878
|
+
timeoutMs: 30_000,
|
|
1879
|
+
});
|
|
1880
|
+
steps.push({
|
|
1881
|
+
name: "ui-audit",
|
|
1882
|
+
ok: uiAudit.exitCode === 0,
|
|
1883
|
+
exitCode: uiAudit.exitCode,
|
|
1884
|
+
durationMs: Date.now() - uiStarted,
|
|
1885
|
+
});
|
|
1886
|
+
diagnostics.push(...uiAudit.diagnostics);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
531
1889
|
if (profile === "smoke") {
|
|
532
1890
|
steps.push(skippedStep("tests", "--smoke/--fast"));
|
|
533
1891
|
} else if (options.skipTests) {
|
|
@@ -537,7 +1895,70 @@ export async function runVerifyCommand(
|
|
|
537
1895
|
const impact = await runStandardImpactTests(options);
|
|
538
1896
|
steps.push(...impact.steps);
|
|
539
1897
|
diagnostics.push(...impact.diagnostics);
|
|
1898
|
+
testCoverageReason = impact.testCoverageReason;
|
|
540
1899
|
steps.push(skippedStep("tests", "--standard uses impact-selected tests; use --strict for the full test script"));
|
|
1900
|
+
} else if ((profile === "strict" || profile === "internal") && !options.fullTests) {
|
|
1901
|
+
if (!canRunInternalTests) {
|
|
1902
|
+
steps.push({
|
|
1903
|
+
name: "tests:framework-testgraph",
|
|
1904
|
+
ok: true,
|
|
1905
|
+
skipped: true,
|
|
1906
|
+
skipReason: "ForgeOS framework tests are maintainer-only; use forge verify framework or --internal",
|
|
1907
|
+
});
|
|
1908
|
+
diagnostics.push(
|
|
1909
|
+
createDiagnostic({
|
|
1910
|
+
severity: "warning",
|
|
1911
|
+
code: "FORGE_VERIFY_INTERNAL_TESTS_SKIPPED",
|
|
1912
|
+
message: "Skipped ForgeOS framework tests during app-level verify.",
|
|
1913
|
+
fixHint: "Run forge verify framework when maintaining ForgeOS itself. App projects still run their own TestGraph under forge verify --strict.",
|
|
1914
|
+
suggestedCommands: [
|
|
1915
|
+
"forge verify framework",
|
|
1916
|
+
"forge verify --internal",
|
|
1917
|
+
"forge verify --standard",
|
|
1918
|
+
],
|
|
1919
|
+
}),
|
|
1920
|
+
);
|
|
1921
|
+
} else {
|
|
1922
|
+
printProgress(options, `verify: tests (strict TestGraph, ${scriptTimeoutMs}ms timeout)`);
|
|
1923
|
+
const tests = await runStrictGraphTests(options.workspaceRoot, scriptTimeoutMs, options.testJobs);
|
|
1924
|
+
testGraphPlan = tests.plan;
|
|
1925
|
+
steps.push({
|
|
1926
|
+
name: "tests:testgraph-strict",
|
|
1927
|
+
ok: tests.exitCode === 0,
|
|
1928
|
+
exitCode: tests.exitCode,
|
|
1929
|
+
command: tests.command,
|
|
1930
|
+
durationMs: tests.durationMs,
|
|
1931
|
+
timedOut: tests.timedOut,
|
|
1932
|
+
failureKind: packageScriptFailureKind(tests),
|
|
1933
|
+
});
|
|
1934
|
+
if (tests.timedOut) {
|
|
1935
|
+
diagnostics.push(timedOutDiagnostic("test", scriptTimeoutMs));
|
|
1936
|
+
} else if (tests.exitCode !== 0) {
|
|
1937
|
+
diagnostics.push(
|
|
1938
|
+
strictGraphFailureDiagnostic(tests),
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
} else if ((profile === "strict" || profile === "internal") && options.fullTests && !canRunInternalTests) {
|
|
1943
|
+
steps.push({
|
|
1944
|
+
name: "tests:framework-full",
|
|
1945
|
+
ok: true,
|
|
1946
|
+
skipped: true,
|
|
1947
|
+
skipReason: "ForgeOS framework package tests are maintainer-only; use forge verify framework --full or --internal --full",
|
|
1948
|
+
});
|
|
1949
|
+
diagnostics.push(
|
|
1950
|
+
createDiagnostic({
|
|
1951
|
+
severity: "warning",
|
|
1952
|
+
code: "FORGE_VERIFY_INTERNAL_TESTS_SKIPPED",
|
|
1953
|
+
message: "Skipped ForgeOS framework package tests during app-level verify.",
|
|
1954
|
+
fixHint: "Run forge verify framework --full when maintaining ForgeOS itself.",
|
|
1955
|
+
suggestedCommands: [
|
|
1956
|
+
"forge verify framework --full",
|
|
1957
|
+
"forge verify --internal --full",
|
|
1958
|
+
"forge verify --standard",
|
|
1959
|
+
],
|
|
1960
|
+
}),
|
|
1961
|
+
);
|
|
541
1962
|
} else if (!scripts.test) {
|
|
542
1963
|
steps.push(skippedStep("tests", "no test script in package.json"));
|
|
543
1964
|
} else {
|
|
@@ -581,6 +2002,8 @@ export async function runVerifyCommand(
|
|
|
581
2002
|
profile,
|
|
582
2003
|
steps,
|
|
583
2004
|
diagnostics,
|
|
2005
|
+
testGraphPlan,
|
|
2006
|
+
testCoverageReason,
|
|
584
2007
|
durationMs: Date.now() - started,
|
|
585
2008
|
exitCode: ok ? 0 : 1,
|
|
586
2009
|
};
|