@herbertgao/pi-extensions 2026.8.13 → 2026.8.15
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/README.md +19 -14
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
- package/node_modules/@czottmann/pi-automode/README.md +5 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +125 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@tifan/pi-handoff/README.md +36 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
- package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +35 -0
- package/node_modules/pi-mcp-adapter/README.md +5 -2
- package/node_modules/pi-mcp-adapter/cli.js +4 -4
- package/node_modules/pi-mcp-adapter/commands.ts +1 -1
- package/node_modules/pi-mcp-adapter/config.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +90 -6
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
- package/node_modules/pi-mcp-adapter/package.json +1 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
- package/node_modules/pi-mcp-adapter/types.ts +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +42 -0
- package/node_modules/pi-web-access/README.md +26 -10
- package/node_modules/pi-web-access/curator-page.ts +12 -3
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/extract.ts +88 -13
- package/node_modules/pi-web-access/gemini-search.ts +10 -5
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +40 -40
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/node_modules/pi-web-access/xcrawl.ts +264 -0
- package/package.json +19 -22
- package/node_modules/@herbertgao/pi-stash/README.md +0 -34
- package/node_modules/@herbertgao/pi-stash/package.json +0 -51
- package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -10,12 +10,17 @@
|
|
|
10
10
|
* Design: File-level targeted testing — only runs tests for the
|
|
11
11
|
* specific file being edited, not the entire suite.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
+
import { emitBounded } from "./bounded-telemetry.js";
|
|
17
|
+
import { LEDGER_FIELD_MAX } from "./degradation-ledger.js";
|
|
16
18
|
import { minimatch } from "./deps/minimatch.js";
|
|
19
|
+
import { createSubsystemLogger } from "./extension-log.js";
|
|
17
20
|
import { detectFileRole } from "./file-role.js";
|
|
18
21
|
import { findGlobalBinary } from "./package-manager.js";
|
|
22
|
+
import { PathKeyedMap } from "./path-keyed-map.js";
|
|
23
|
+
import { normalizeEphemeralMapKey, normalizeMapKey } from "./path-utils.js";
|
|
19
24
|
import { isMeasuredDuration, toMeasuredDurationMs } from "./run-duration.js";
|
|
20
25
|
import { safeSpawn, safeSpawnAsync } from "./safe-spawn.js";
|
|
21
26
|
// Source file → test file patterns (reverse lookup)
|
|
@@ -194,21 +199,115 @@ export function stripWrapperArgs(binName, args) {
|
|
|
194
199
|
return args;
|
|
195
200
|
}
|
|
196
201
|
// --- Client ---
|
|
202
|
+
const MAX_FAILED_TARGETS_PER_RUNNER = 32;
|
|
203
|
+
const MAX_FAILED_TARGET_CHECKS_PER_SELECTION = 8;
|
|
204
|
+
const FAILED_TARGET_DETAIL_CAP_PER_TURN = 8;
|
|
205
|
+
function canonicalFailedPath(filePath) {
|
|
206
|
+
const absolute = path.resolve(filePath);
|
|
207
|
+
try {
|
|
208
|
+
return normalizeMapKey(fs.realpathSync.native(absolute));
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return normalizeMapKey(absolute);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function canonicalProjectRoot(cwd) {
|
|
215
|
+
const absolute = path.resolve(cwd);
|
|
216
|
+
try {
|
|
217
|
+
return {
|
|
218
|
+
key: normalizeEphemeralMapKey(fs.realpathSync.native(absolute)),
|
|
219
|
+
resolved: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return { key: normalizeEphemeralMapKey(absolute), resolved: false };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const MAX_CANONICAL_ROOT_MEMO_ENTRIES = 512;
|
|
227
|
+
function filesystemErrorCode(error) {
|
|
228
|
+
if (error !== null &&
|
|
229
|
+
typeof error === "object" &&
|
|
230
|
+
"code" in error &&
|
|
231
|
+
typeof error.code === "string") {
|
|
232
|
+
return error.code;
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
197
236
|
export class TestRunnerClient {
|
|
198
237
|
log;
|
|
199
|
-
|
|
238
|
+
// This is an instance-lifetime memo of RESOLVED spellings only, which leaves
|
|
239
|
+
// two temporal edges. A symlink retargeted mid-session keeps its old
|
|
240
|
+
// resolution until a new client instance — acceptable because round 2's
|
|
241
|
+
// evidence re-validation already handles verdict-level staleness (positive
|
|
242
|
+
// verdicts re-stat their config file). A spelling that did NOT resolve is
|
|
243
|
+
// never memoized (#2077): the fallback key is a guess about a path that does
|
|
244
|
+
// not exist yet, so memoizing it would pin an alias probed before its
|
|
245
|
+
// symlink was created to a stale verdict for the instance's life. Re-probing
|
|
246
|
+
// costs one failing realpath per call, and only for a cwd that does not
|
|
247
|
+
// resolve — a state where `detectRunner` already walks node_modules on every
|
|
248
|
+
// call. Keep the memo bounded so pathological spelling churn cannot grow it
|
|
249
|
+
// without limit.
|
|
250
|
+
canonicalRootMemo = new Map();
|
|
251
|
+
availableRunners = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
200
252
|
failedTestsByRunner = new Map();
|
|
253
|
+
failedTargetSequence = 0;
|
|
254
|
+
statFailedTarget;
|
|
201
255
|
// Best-effort vitest config `test.include`/`test.exclude` globs, scraped as
|
|
202
256
|
// plain text (never executed) and cached per cwd so the config file is
|
|
203
257
|
// only read/parsed once, not on every edit. `null` means "no config found
|
|
204
258
|
// or it couldn't be parsed in the simple shape we look for" — callers
|
|
205
259
|
// treat that as "no additional signal" and fall back to naming-convention
|
|
206
260
|
// detection only.
|
|
207
|
-
vitestTestGlobsCache = new
|
|
208
|
-
constructor(verbose = false) {
|
|
209
|
-
this.log = verbose
|
|
210
|
-
|
|
211
|
-
|
|
261
|
+
vitestTestGlobsCache = new PathKeyedMap(normalizeEphemeralMapKey);
|
|
262
|
+
constructor(verbose = false, options = {}) {
|
|
263
|
+
this.log = verbose ? createSubsystemLogger("test-runner") : () => { };
|
|
264
|
+
this.statFailedTarget =
|
|
265
|
+
options.statFailedTarget ?? ((filePath) => void fs.statSync(filePath));
|
|
266
|
+
}
|
|
267
|
+
getCanonicalProjectRoot(cwd) {
|
|
268
|
+
const cached = this.canonicalRootMemo.get(cwd);
|
|
269
|
+
if (cached !== undefined)
|
|
270
|
+
return cached;
|
|
271
|
+
const { key, resolved } = canonicalProjectRoot(cwd);
|
|
272
|
+
if (!resolved)
|
|
273
|
+
return key;
|
|
274
|
+
if (this.canonicalRootMemo.size >= MAX_CANONICAL_ROOT_MEMO_ENTRIES) {
|
|
275
|
+
const oldest = this.canonicalRootMemo.keys().next().value;
|
|
276
|
+
if (oldest !== undefined)
|
|
277
|
+
this.canonicalRootMemo.delete(oldest);
|
|
278
|
+
}
|
|
279
|
+
this.canonicalRootMemo.set(cwd, key);
|
|
280
|
+
return key;
|
|
281
|
+
}
|
|
282
|
+
getRunnerAvailability(byRunner, runner) {
|
|
283
|
+
const cached = byRunner?.get(runner);
|
|
284
|
+
if (!cached)
|
|
285
|
+
return undefined;
|
|
286
|
+
if (cached.available &&
|
|
287
|
+
cached.evidencePath !== undefined &&
|
|
288
|
+
!fs.existsSync(cached.evidencePath)) {
|
|
289
|
+
byRunner?.delete(runner);
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
return cached.available;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* #2252: only a POSITIVE verdict is memoized. A negative one has no
|
|
296
|
+
* `evidencePath` to re-stat, so `getRunnerAvailability` had no way to tell
|
|
297
|
+
* "still absent" from "a config file just appeared" and served the first
|
|
298
|
+
* miss for the client's whole process lifetime — measured live: probe an
|
|
299
|
+
* empty directory, add `vitest.config.ts`, and the SAME client kept
|
|
300
|
+
* answering "no runner". Same precedent as #2242's alias-canonicalization
|
|
301
|
+
* fix (`clients/test-runner-client.ts`'s `getCanonicalProjectRoot`): drop
|
|
302
|
+
* the memo write on the failure branch rather than adding a TTL or a
|
|
303
|
+
* re-arm signal. The cost is bounded and already paid today — a cache miss
|
|
304
|
+
* re-runs the exact same `configFiles.some(fs.existsSync)` walk this
|
|
305
|
+
* method's caller already does on every FIRST probe of a runner.
|
|
306
|
+
*/
|
|
307
|
+
setRunnerAvailability(byRunner, runner, available, evidencePath) {
|
|
308
|
+
if (!available)
|
|
309
|
+
return;
|
|
310
|
+
byRunner.set(runner, { available, evidencePath });
|
|
212
311
|
}
|
|
213
312
|
/**
|
|
214
313
|
* Check if a test runner is available in the project
|
|
@@ -218,15 +317,22 @@ export class TestRunnerClient {
|
|
|
218
317
|
* 3. node_modules presence
|
|
219
318
|
*/
|
|
220
319
|
detectRunner(cwd, sourceFilePath) {
|
|
320
|
+
const rootKey = this.getCanonicalProjectRoot(cwd);
|
|
321
|
+
let byRunner = this.availableRunners.get(rootKey);
|
|
322
|
+
if (!byRunner) {
|
|
323
|
+
byRunner = new Map();
|
|
324
|
+
this.availableRunners.set(rootKey, byRunner);
|
|
325
|
+
}
|
|
221
326
|
// Priority 1: Config files
|
|
222
327
|
for (const [name, config] of Object.entries(RUNNERS)) {
|
|
223
|
-
const
|
|
224
|
-
if (
|
|
225
|
-
if (
|
|
328
|
+
const cached = this.getRunnerAvailability(byRunner, name);
|
|
329
|
+
if (cached !== undefined) {
|
|
330
|
+
if (cached) {
|
|
226
331
|
return { runner: name, config };
|
|
227
332
|
}
|
|
228
333
|
continue;
|
|
229
334
|
}
|
|
335
|
+
let configEvidencePath;
|
|
230
336
|
const found = config.configFiles.some((cf) => {
|
|
231
337
|
if (name === "pytest" && cf === "pyproject.toml") {
|
|
232
338
|
const pyprojectPath = path.join(cwd, cf);
|
|
@@ -234,7 +340,10 @@ export class TestRunnerClient {
|
|
|
234
340
|
return false;
|
|
235
341
|
try {
|
|
236
342
|
const pyproject = fs.readFileSync(pyprojectPath, "utf-8");
|
|
237
|
-
|
|
343
|
+
const matches = pyproject.includes("[tool.pytest.ini_options]");
|
|
344
|
+
if (matches)
|
|
345
|
+
configEvidencePath = pyprojectPath;
|
|
346
|
+
return matches;
|
|
238
347
|
}
|
|
239
348
|
catch {
|
|
240
349
|
return false;
|
|
@@ -250,15 +359,22 @@ export class TestRunnerClient {
|
|
|
250
359
|
...composer.require,
|
|
251
360
|
...composer["require-dev"],
|
|
252
361
|
};
|
|
253
|
-
|
|
362
|
+
const matches = Boolean(allDeps["phpunit/phpunit"]);
|
|
363
|
+
if (matches)
|
|
364
|
+
configEvidencePath = composerPath;
|
|
365
|
+
return matches;
|
|
254
366
|
}
|
|
255
367
|
catch {
|
|
256
368
|
return false;
|
|
257
369
|
}
|
|
258
370
|
}
|
|
259
|
-
|
|
371
|
+
const candidate = path.join(cwd, cf);
|
|
372
|
+
const matches = fs.existsSync(candidate);
|
|
373
|
+
if (matches)
|
|
374
|
+
configEvidencePath = candidate;
|
|
375
|
+
return matches;
|
|
260
376
|
});
|
|
261
|
-
this.
|
|
377
|
+
this.setRunnerAvailability(byRunner, name, found, configEvidencePath);
|
|
262
378
|
if (found) {
|
|
263
379
|
this.log(`Detected runner via config: ${name}`);
|
|
264
380
|
return { runner: name, config };
|
|
@@ -274,17 +390,17 @@ export class TestRunnerClient {
|
|
|
274
390
|
// Check for vitest first (more specific than jest)
|
|
275
391
|
if (allDeps.vitest) {
|
|
276
392
|
this.log("Detected vitest in package.json");
|
|
277
|
-
this.
|
|
393
|
+
this.setRunnerAvailability(byRunner, "vitest", true, packageJsonPath);
|
|
278
394
|
return { runner: "vitest", config: RUNNERS.vitest };
|
|
279
395
|
}
|
|
280
396
|
if (allDeps.jest) {
|
|
281
397
|
this.log("Detected jest in package.json");
|
|
282
|
-
this.
|
|
398
|
+
this.setRunnerAvailability(byRunner, "jest", true, packageJsonPath);
|
|
283
399
|
return { runner: "jest", config: RUNNERS.jest };
|
|
284
400
|
}
|
|
285
401
|
if (allDeps.pytest || allDeps["pytest-cov"]) {
|
|
286
402
|
this.log("Detected pytest in package.json (unusual)");
|
|
287
|
-
this.
|
|
403
|
+
this.setRunnerAvailability(byRunner, "pytest", true, packageJsonPath);
|
|
288
404
|
return { runner: "pytest", config: RUNNERS.pytest };
|
|
289
405
|
}
|
|
290
406
|
}
|
|
@@ -428,7 +544,10 @@ export class TestRunnerClient {
|
|
|
428
544
|
relativeSourceDir(sourceFilePath, cwd) {
|
|
429
545
|
const dir = path.dirname(sourceFilePath);
|
|
430
546
|
const relDir = path.relative(cwd, path.resolve(cwd, dir));
|
|
431
|
-
if (!relDir ||
|
|
547
|
+
if (!relDir ||
|
|
548
|
+
relDir === "." ||
|
|
549
|
+
relDir.startsWith("..") ||
|
|
550
|
+
path.isAbsolute(relDir)) {
|
|
432
551
|
return null;
|
|
433
552
|
}
|
|
434
553
|
return relDir;
|
|
@@ -447,21 +566,40 @@ export class TestRunnerClient {
|
|
|
447
566
|
* template expression) — anything more dynamic than that is out of
|
|
448
567
|
* scope for this heuristic.
|
|
449
568
|
*
|
|
450
|
-
* Cached per `cwd
|
|
451
|
-
*
|
|
569
|
+
* Cached per `cwd`, including a `null` result — #2252 F2: a project with
|
|
570
|
+
* no vitest config, or one this heuristic can't scrape, is a COMMON shape
|
|
571
|
+
* (every non-vitest project pays this on every edit otherwise; measured
|
|
572
|
+
* ~1500x — 0.4µs cached vs. 598.7µs re-reading the candidate list every
|
|
573
|
+
* call). The cache entry is revalidated on every read, the same shape
|
|
574
|
+
* `getRunnerAvailability` uses for a positive verdict: bounded
|
|
575
|
+
* `fs.existsSync` checks against the config file the result was derived
|
|
576
|
+
* from (or, when none existed, against the candidate list itself), never
|
|
577
|
+
* a full re-read/re-parse on a cache hit. So a config file appearing (or
|
|
578
|
+
* a broken one being fixed) still converges — only the FULL parse is
|
|
579
|
+
* paid once, not the existence check.
|
|
452
580
|
*/
|
|
453
581
|
parseVitestTestGlobs(cwd) {
|
|
454
|
-
|
|
455
|
-
return this.vitestTestGlobsCache.get(cwd) ?? null;
|
|
456
|
-
}
|
|
582
|
+
const rootKey = this.getCanonicalProjectRoot(cwd);
|
|
457
583
|
// .mts isn't in RUNNERS.vitest.configFiles (that list drives runner
|
|
458
584
|
// *detection* priority) but is a legal vitest config extension, so it's
|
|
459
585
|
// included here for the scrape even though detectRunner doesn't check it.
|
|
460
586
|
const candidates = [...RUNNERS.vitest.configFiles, "vitest.config.mts"];
|
|
587
|
+
const cached = this.vitestTestGlobsCache.get(rootKey);
|
|
588
|
+
if (cached !== undefined) {
|
|
589
|
+
const stillValid = cached.evidencePath !== undefined
|
|
590
|
+
? fs.existsSync(cached.evidencePath)
|
|
591
|
+
: !candidates.some((cf) => fs.existsSync(path.join(cwd, cf)));
|
|
592
|
+
if (stillValid)
|
|
593
|
+
return cached.result;
|
|
594
|
+
this.vitestTestGlobsCache.delete(rootKey);
|
|
595
|
+
}
|
|
461
596
|
let content = null;
|
|
597
|
+
let foundPath;
|
|
462
598
|
for (const cf of candidates) {
|
|
599
|
+
const candidatePath = path.join(cwd, cf);
|
|
463
600
|
try {
|
|
464
|
-
content = fs.readFileSync(
|
|
601
|
+
content = fs.readFileSync(candidatePath, "utf-8");
|
|
602
|
+
foundPath = candidatePath;
|
|
465
603
|
break;
|
|
466
604
|
}
|
|
467
605
|
catch {
|
|
@@ -480,7 +618,7 @@ export class TestRunnerClient {
|
|
|
480
618
|
result.exclude = exclude;
|
|
481
619
|
}
|
|
482
620
|
}
|
|
483
|
-
this.vitestTestGlobsCache.set(
|
|
621
|
+
this.vitestTestGlobsCache.set(rootKey, { result, evidencePath: foundPath });
|
|
484
622
|
return result;
|
|
485
623
|
}
|
|
486
624
|
/**
|
|
@@ -706,12 +844,11 @@ export class TestRunnerClient {
|
|
|
706
844
|
* 1) If there are known failing tests, rerun those first (fast feedback loop).
|
|
707
845
|
* 2) Otherwise run related tests for the edited file.
|
|
708
846
|
*/
|
|
709
|
-
getTestRunTarget(sourceFilePath, cwd) {
|
|
847
|
+
getTestRunTarget(sourceFilePath, cwd, turnIndex) {
|
|
710
848
|
const detected = this.detectRunner(cwd, sourceFilePath);
|
|
711
849
|
if (!detected)
|
|
712
850
|
return null;
|
|
713
|
-
const
|
|
714
|
-
const failedSet = this.failedTestsByRunner.get(key);
|
|
851
|
+
const failedSet = this.getFailedTargets(cwd, detected.runner);
|
|
715
852
|
// If the edited file is itself a test file, there's no "related test"
|
|
716
853
|
// to discover — running findTestFile on it would strip its own
|
|
717
854
|
// extension and search for nonsense like foo.test.test.ts. Skip
|
|
@@ -721,34 +858,22 @@ export class TestRunnerClient {
|
|
|
721
858
|
? null
|
|
722
859
|
: this.findTestFile(sourceFilePath, cwd, detected.runner);
|
|
723
860
|
if (failedSet && failedSet.size > 0) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
if (failedSet.has(relatedAbs)) {
|
|
727
|
-
return {
|
|
728
|
-
testFile: relatedAbs,
|
|
729
|
-
runner: detected.runner,
|
|
730
|
-
config: detected.config,
|
|
731
|
-
strategy: "failed-first",
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
if (selfIsTest) {
|
|
736
|
-
const selfAbs = path.resolve(sourceFilePath);
|
|
737
|
-
if (failedSet.has(selfAbs)) {
|
|
738
|
-
return {
|
|
739
|
-
testFile: selfAbs,
|
|
740
|
-
runner: detected.runner,
|
|
741
|
-
config: detected.config,
|
|
742
|
-
strategy: "failed-first",
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return {
|
|
747
|
-
testFile: [...failedSet][0],
|
|
861
|
+
const failedFirst = this.retireMissingFailedTargets({
|
|
862
|
+
cwd,
|
|
748
863
|
runner: detected.runner,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
864
|
+
failedTargets: failedSet,
|
|
865
|
+
relatedAbs: related ? path.resolve(related.testFile) : undefined,
|
|
866
|
+
selfAbs: selfIsTest ? path.resolve(sourceFilePath) : undefined,
|
|
867
|
+
turnIndex,
|
|
868
|
+
});
|
|
869
|
+
if (failedFirst) {
|
|
870
|
+
return {
|
|
871
|
+
testFile: failedFirst,
|
|
872
|
+
runner: detected.runner,
|
|
873
|
+
config: detected.config,
|
|
874
|
+
strategy: "failed-first",
|
|
875
|
+
};
|
|
876
|
+
}
|
|
752
877
|
}
|
|
753
878
|
if (selfIsTest) {
|
|
754
879
|
return {
|
|
@@ -767,13 +892,19 @@ export class TestRunnerClient {
|
|
|
767
892
|
strategy: "related",
|
|
768
893
|
};
|
|
769
894
|
}
|
|
770
|
-
|
|
771
|
-
* Run tests for a specific file without blocking the event loop, so LSP
|
|
772
|
-
* messages, other file writes, and all async operations continue while
|
|
773
|
-
* tests run.
|
|
774
|
-
*/
|
|
775
|
-
async runTestFileAsync(testFile, cwd, runner, config) {
|
|
895
|
+
async runTestFileAsync(testFile, cwd, runnerOrRequest, legacyConfig) {
|
|
776
896
|
const absoluteTestFile = path.resolve(testFile);
|
|
897
|
+
let request;
|
|
898
|
+
if (typeof runnerOrRequest === "string") {
|
|
899
|
+
if (!legacyConfig) {
|
|
900
|
+
return this.emptyResult(absoluteTestFile, "", runnerOrRequest, "Runner configuration missing");
|
|
901
|
+
}
|
|
902
|
+
request = { runner: runnerOrRequest, config: legacyConfig };
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
request = runnerOrRequest;
|
|
906
|
+
}
|
|
907
|
+
const { runner, config, turnIndex } = request;
|
|
777
908
|
if (!fs.existsSync(absoluteTestFile)) {
|
|
778
909
|
return this.emptyResult(absoluteTestFile, "", runner, "Test file not found");
|
|
779
910
|
}
|
|
@@ -811,7 +942,13 @@ export class TestRunnerClient {
|
|
|
811
942
|
parsed = this.parseGenericRunnerOutput(stdout, stderr, result.status ?? 0, absoluteTestFile, runner);
|
|
812
943
|
break;
|
|
813
944
|
}
|
|
814
|
-
this.recordResult(
|
|
945
|
+
this.recordResult({
|
|
946
|
+
cwd,
|
|
947
|
+
runner,
|
|
948
|
+
testFile: absoluteTestFile,
|
|
949
|
+
result: parsed,
|
|
950
|
+
turnIndex,
|
|
951
|
+
});
|
|
815
952
|
return parsed;
|
|
816
953
|
}
|
|
817
954
|
catch (err) {
|
|
@@ -819,6 +956,172 @@ export class TestRunnerClient {
|
|
|
819
956
|
return this.emptyResult(absoluteTestFile, "", runner, err.message);
|
|
820
957
|
}
|
|
821
958
|
}
|
|
959
|
+
getFailedTargets(cwd, runner, create = false) {
|
|
960
|
+
let roots = this.failedTestsByRunner.get(runner);
|
|
961
|
+
if (!roots && create) {
|
|
962
|
+
roots = new PathKeyedMap(canonicalFailedPath);
|
|
963
|
+
this.failedTestsByRunner.set(runner, roots);
|
|
964
|
+
}
|
|
965
|
+
if (!roots)
|
|
966
|
+
return undefined;
|
|
967
|
+
const root = canonicalFailedPath(cwd);
|
|
968
|
+
let targets = roots.get(root);
|
|
969
|
+
if (!targets && create) {
|
|
970
|
+
targets = new PathKeyedMap(canonicalFailedPath);
|
|
971
|
+
roots.set(root, targets);
|
|
972
|
+
}
|
|
973
|
+
return targets;
|
|
974
|
+
}
|
|
975
|
+
deleteFailedRoot(cwd, runner) {
|
|
976
|
+
const roots = this.failedTestsByRunner.get(runner);
|
|
977
|
+
if (!roots)
|
|
978
|
+
return;
|
|
979
|
+
roots.delete(canonicalFailedPath(cwd));
|
|
980
|
+
if (roots.size === 0)
|
|
981
|
+
this.failedTestsByRunner.delete(runner);
|
|
982
|
+
}
|
|
983
|
+
failedTargetCount(runner) {
|
|
984
|
+
const roots = this.failedTestsByRunner.get(runner);
|
|
985
|
+
if (!roots)
|
|
986
|
+
return 0;
|
|
987
|
+
let count = 0;
|
|
988
|
+
for (const targets of roots.values())
|
|
989
|
+
count += targets.size;
|
|
990
|
+
return count;
|
|
991
|
+
}
|
|
992
|
+
evictOldestFailedTarget(runner) {
|
|
993
|
+
const roots = this.failedTestsByRunner.get(runner);
|
|
994
|
+
if (!roots)
|
|
995
|
+
return undefined;
|
|
996
|
+
let oldest;
|
|
997
|
+
for (const [root, targets] of roots) {
|
|
998
|
+
for (const [identity, entry] of targets) {
|
|
999
|
+
if (!oldest || entry.sequence < oldest.entry.sequence) {
|
|
1000
|
+
oldest = { root, identity, entry };
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
if (!oldest)
|
|
1005
|
+
return undefined;
|
|
1006
|
+
const targets = roots.get(oldest.root);
|
|
1007
|
+
targets?.delete(oldest.identity);
|
|
1008
|
+
if (targets?.size === 0)
|
|
1009
|
+
roots.delete(oldest.root);
|
|
1010
|
+
if (roots.size === 0)
|
|
1011
|
+
this.failedTestsByRunner.delete(runner);
|
|
1012
|
+
return oldest.entry.displayPath;
|
|
1013
|
+
}
|
|
1014
|
+
classifyFailedTarget(candidate) {
|
|
1015
|
+
try {
|
|
1016
|
+
this.statFailedTarget(candidate);
|
|
1017
|
+
return { status: "present" };
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
const errorCode = filesystemErrorCode(error);
|
|
1021
|
+
if (errorCode === "ENOENT" || errorCode === "ENOTDIR") {
|
|
1022
|
+
return { status: "missing", errorCode };
|
|
1023
|
+
}
|
|
1024
|
+
return { status: "indeterminate", errorCode };
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
recordFailedTargetState(record) {
|
|
1028
|
+
const { outcome, runner, candidate, errorCode, turnIndex } = record;
|
|
1029
|
+
const boundedTarget = candidate.length <= LEDGER_FIELD_MAX
|
|
1030
|
+
? candidate
|
|
1031
|
+
: `…${candidate.slice(1 - LEDGER_FIELD_MAX)}`;
|
|
1032
|
+
const targetIdentity = createHash("sha256")
|
|
1033
|
+
.update(runner)
|
|
1034
|
+
.update("\0")
|
|
1035
|
+
.update(candidate)
|
|
1036
|
+
.digest("hex");
|
|
1037
|
+
const identity = `${outcome}:${targetIdentity}`;
|
|
1038
|
+
const payload = {
|
|
1039
|
+
durationMs: 0,
|
|
1040
|
+
filePath: boundedTarget,
|
|
1041
|
+
metadata: {
|
|
1042
|
+
outcome,
|
|
1043
|
+
runner,
|
|
1044
|
+
errorCode: errorCode ?? "unknown",
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
const options = {
|
|
1048
|
+
ledgerKind: "test-runner-failed-target-state",
|
|
1049
|
+
risingEdgePer: "identity",
|
|
1050
|
+
reason: `${outcome}: ${boundedTarget}`,
|
|
1051
|
+
};
|
|
1052
|
+
emitBounded("test_runner_failed_target_state", identity, payload, turnIndex === undefined
|
|
1053
|
+
? options
|
|
1054
|
+
: {
|
|
1055
|
+
...options,
|
|
1056
|
+
capPerTurn: {
|
|
1057
|
+
limit: FAILED_TARGET_DETAIL_CAP_PER_TURN,
|
|
1058
|
+
turnIndex,
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Retire only confirmed-missing failed-first paths, then return one usable
|
|
1064
|
+
* target. A bounded prefix is checked per selection; remaining candidates
|
|
1065
|
+
* carry over to the next selection instead of adding unbounded synchronous
|
|
1066
|
+
* filesystem work to turn_end. Related/self targets keep priority.
|
|
1067
|
+
*/
|
|
1068
|
+
retireMissingFailedTargets(selection) {
|
|
1069
|
+
const { cwd, runner, failedTargets, relatedAbs, selfAbs, turnIndex } = selection;
|
|
1070
|
+
let checked = 0;
|
|
1071
|
+
const inspected = new Set();
|
|
1072
|
+
const inspect = (identity, entry) => {
|
|
1073
|
+
if (checked >= MAX_FAILED_TARGET_CHECKS_PER_SELECTION)
|
|
1074
|
+
return undefined;
|
|
1075
|
+
checked += 1;
|
|
1076
|
+
inspected.add(identity);
|
|
1077
|
+
const candidate = entry.displayPath;
|
|
1078
|
+
const verdict = this.classifyFailedTarget(candidate);
|
|
1079
|
+
if (verdict.status === "missing") {
|
|
1080
|
+
failedTargets.delete(identity);
|
|
1081
|
+
this.recordFailedTargetState({
|
|
1082
|
+
outcome: "retired-missing",
|
|
1083
|
+
runner,
|
|
1084
|
+
candidate,
|
|
1085
|
+
errorCode: verdict.errorCode,
|
|
1086
|
+
turnIndex,
|
|
1087
|
+
});
|
|
1088
|
+
return undefined;
|
|
1089
|
+
}
|
|
1090
|
+
if (verdict.status === "indeterminate") {
|
|
1091
|
+
this.recordFailedTargetState({
|
|
1092
|
+
outcome: "retained-indeterminate",
|
|
1093
|
+
runner,
|
|
1094
|
+
candidate,
|
|
1095
|
+
errorCode: verdict.errorCode,
|
|
1096
|
+
turnIndex,
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
return candidate;
|
|
1100
|
+
};
|
|
1101
|
+
for (const preferred of [relatedAbs, selfAbs]) {
|
|
1102
|
+
if (!preferred)
|
|
1103
|
+
continue;
|
|
1104
|
+
const identity = canonicalFailedPath(preferred);
|
|
1105
|
+
const entry = failedTargets.get(identity);
|
|
1106
|
+
if (!entry)
|
|
1107
|
+
continue;
|
|
1108
|
+
const selected = inspect(identity, entry);
|
|
1109
|
+
if (selected)
|
|
1110
|
+
return selected;
|
|
1111
|
+
}
|
|
1112
|
+
for (const [identity, entry] of failedTargets) {
|
|
1113
|
+
if (inspected.has(identity))
|
|
1114
|
+
continue;
|
|
1115
|
+
if (checked >= MAX_FAILED_TARGET_CHECKS_PER_SELECTION)
|
|
1116
|
+
break;
|
|
1117
|
+
const selected = inspect(identity, entry);
|
|
1118
|
+
if (selected)
|
|
1119
|
+
return selected;
|
|
1120
|
+
}
|
|
1121
|
+
if (failedTargets.size === 0)
|
|
1122
|
+
this.deleteFailedRoot(cwd, runner);
|
|
1123
|
+
return undefined;
|
|
1124
|
+
}
|
|
822
1125
|
/**
|
|
823
1126
|
* Check if a source file has corresponding tests (without running them)
|
|
824
1127
|
*/
|
|
@@ -1043,7 +1346,9 @@ export class TestRunnerClient {
|
|
|
1043
1346
|
const errorsMatch = output.match(/Errors:\s*(\d+)/i);
|
|
1044
1347
|
const skippedMatch = output.match(/Skipped:\s*(\d+)/i);
|
|
1045
1348
|
const total = testsMatch ? Number.parseInt(testsMatch[1], 10) : 0;
|
|
1046
|
-
const failures = failuresMatch
|
|
1349
|
+
const failures = failuresMatch
|
|
1350
|
+
? Number.parseInt(failuresMatch[1], 10)
|
|
1351
|
+
: 0;
|
|
1047
1352
|
const errors = errorsMatch ? Number.parseInt(errorsMatch[1], 10) : 0;
|
|
1048
1353
|
skipped = skippedMatch ? Number.parseInt(skippedMatch[1], 10) : 0;
|
|
1049
1354
|
failed = failures + errors;
|
|
@@ -1123,7 +1428,9 @@ export class TestRunnerClient {
|
|
|
1123
1428
|
if (summaryMatch) {
|
|
1124
1429
|
const total = Number.parseInt(summaryMatch[1], 10);
|
|
1125
1430
|
failed = Number.parseInt(summaryMatch[2], 10);
|
|
1126
|
-
const excluded = summaryMatch[3]
|
|
1431
|
+
const excluded = summaryMatch[3]
|
|
1432
|
+
? Number.parseInt(summaryMatch[3], 10)
|
|
1433
|
+
: 0;
|
|
1127
1434
|
const skippedCount = summaryMatch[4]
|
|
1128
1435
|
? Number.parseInt(summaryMatch[4], 10)
|
|
1129
1436
|
: 0;
|
|
@@ -1499,9 +1806,7 @@ export class TestRunnerClient {
|
|
|
1499
1806
|
// Package-line counts, like the go duration probe above, take the
|
|
1500
1807
|
// FIRST package summary only — the same known limit already
|
|
1501
1808
|
// documented on `parseGenericRunnerDuration`.
|
|
1502
|
-
const goFailNames = [
|
|
1503
|
-
...output.matchAll(/--- FAIL:[^\S\n]+([^\s(]+)/g),
|
|
1504
|
-
];
|
|
1809
|
+
const goFailNames = [...output.matchAll(/--- FAIL:[^\S\n]+([^\s(]+)/g)];
|
|
1505
1810
|
// #1524-r4: only COUNT unparented failures. go prints a `--- FAIL:`
|
|
1506
1811
|
// line for every level of a failing subtest tree — `TestA` AND
|
|
1507
1812
|
// `TestA/sub` each get their own line for what is really one
|
|
@@ -1595,7 +1900,10 @@ export class TestRunnerClient {
|
|
|
1595
1900
|
// runner error they actually are). They still label a name onto
|
|
1596
1901
|
// `failures` above when `matched` or `goFailNames` already settled
|
|
1597
1902
|
// the question some other way, but they no longer settle it alone.
|
|
1598
|
-
const runnerError = exitCode !== 0 &&
|
|
1903
|
+
const runnerError = exitCode !== 0 &&
|
|
1904
|
+
!matched &&
|
|
1905
|
+
goFailNames.length === 0 &&
|
|
1906
|
+
lower.includes("error")
|
|
1599
1907
|
? `Runner ${runner} exited with ${exitCode}`
|
|
1600
1908
|
: undefined;
|
|
1601
1909
|
// #1524-r4 (tidy): a runner-error result reports through
|
|
@@ -1720,9 +2028,7 @@ export class TestRunnerClient {
|
|
|
1720
2028
|
const relDir = path.relative(cwd, dir);
|
|
1721
2029
|
const segments = relDir.split(path.sep).filter(Boolean);
|
|
1722
2030
|
if (segments.length > 1 && knownSourceRoots.has(segments[0])) {
|
|
1723
|
-
return [
|
|
1724
|
-
path.join(cwd, testDir, ...segments.slice(1), testFilename),
|
|
1725
|
-
];
|
|
2031
|
+
return [path.join(cwd, testDir, ...segments.slice(1), testFilename)];
|
|
1726
2032
|
}
|
|
1727
2033
|
return [];
|
|
1728
2034
|
}
|
|
@@ -1806,12 +2112,18 @@ export class TestRunnerClient {
|
|
|
1806
2112
|
// itself, so the leading wrapper-name arg(s) that named it (e.g. "vitest",
|
|
1807
2113
|
// or "-m pytest") are stripped from args() — see stripWrapperArgs.
|
|
1808
2114
|
if (fs.existsSync(localBin)) {
|
|
1809
|
-
return {
|
|
2115
|
+
return {
|
|
2116
|
+
command: localBin,
|
|
2117
|
+
args: stripWrapperArgs(binName, config.args(testFile, cwd)),
|
|
2118
|
+
};
|
|
1810
2119
|
}
|
|
1811
2120
|
// Any package manager's global bin dir (npm/pnpm/yarn/bun) before npx (#375).
|
|
1812
2121
|
const globalBin = await findGlobalBinary(binName);
|
|
1813
2122
|
if (globalBin) {
|
|
1814
|
-
return {
|
|
2123
|
+
return {
|
|
2124
|
+
command: globalBin,
|
|
2125
|
+
args: stripWrapperArgs(binName, config.args(testFile, cwd)),
|
|
2126
|
+
};
|
|
1815
2127
|
}
|
|
1816
2128
|
return { command: config.command, args: config.args(testFile, cwd) };
|
|
1817
2129
|
}
|
|
@@ -1836,24 +2148,43 @@ export class TestRunnerClient {
|
|
|
1836
2148
|
const lines = stack.split("\n").slice(0, 3);
|
|
1837
2149
|
return lines.join("\n").slice(0, 500);
|
|
1838
2150
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2151
|
+
recordResult(record) {
|
|
2152
|
+
const { cwd, runner, testFile, result, turnIndex } = record;
|
|
2153
|
+
const targetPath = path.resolve(testFile);
|
|
2154
|
+
const target = canonicalFailedPath(targetPath);
|
|
2155
|
+
let failedTargets = this.getFailedTargets(cwd, runner, result.failed > 0);
|
|
2156
|
+
if (!failedTargets)
|
|
2157
|
+
return;
|
|
1846
2158
|
if (result.failed > 0) {
|
|
1847
|
-
|
|
1848
|
-
|
|
2159
|
+
const alreadyRecorded = failedTargets.has(target);
|
|
2160
|
+
if (!alreadyRecorded &&
|
|
2161
|
+
this.failedTargetCount(runner) >= MAX_FAILED_TARGETS_PER_RUNNER) {
|
|
2162
|
+
const evicted = this.evictOldestFailedTarget(runner);
|
|
2163
|
+
if (evicted) {
|
|
2164
|
+
this.recordFailedTargetState({
|
|
2165
|
+
outcome: "capacity-evicted",
|
|
2166
|
+
runner,
|
|
2167
|
+
candidate: evicted,
|
|
2168
|
+
turnIndex,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
// Eviction can remove this root's final prior target. Reacquire the
|
|
2172
|
+
// root map before inserting so the newest failure never lands in a
|
|
2173
|
+
// detached PathKeyedMap.
|
|
2174
|
+
failedTargets = this.getFailedTargets(cwd, runner, true);
|
|
2175
|
+
if (!failedTargets)
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
// Sequence is global across roots, so a refreshed target becomes the
|
|
2179
|
+
// newest failure without relying on one root map's insertion order.
|
|
2180
|
+
failedTargets.set(target, {
|
|
2181
|
+
displayPath: targetPath,
|
|
2182
|
+
sequence: ++this.failedTargetSequence,
|
|
2183
|
+
});
|
|
1849
2184
|
return;
|
|
1850
2185
|
}
|
|
1851
|
-
if (
|
|
1852
|
-
|
|
1853
|
-
if (set.size === 0)
|
|
1854
|
-
this.failedTestsByRunner.delete(key);
|
|
1855
|
-
else
|
|
1856
|
-
this.failedTestsByRunner.set(key, set);
|
|
2186
|
+
if (failedTargets.delete(target) && failedTargets.size === 0) {
|
|
2187
|
+
this.deleteFailedRoot(cwd, runner);
|
|
1857
2188
|
}
|
|
1858
2189
|
}
|
|
1859
2190
|
}
|