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