@sanqianx/project-knowledge 5.0.23 → 5.0.25
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/.agents/plugins/marketplace.json +20 -20
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +1361 -1346
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +349 -349
- package/_modules/knowledge-engine/package.json +1 -1
- package/_modules/knowledge-engine/src/coordinator.js +48 -3
- package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -140
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +238 -238
- package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -26
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +193 -193
- package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +394 -394
- package/_modules/knowledge-engine/src/runtime/contracts.js +282 -282
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +389 -389
- package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -228
- package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -62
- package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +505 -505
- package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +400 -400
- package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +171 -171
- package/_modules/knowledge-engine/src/runtime/scanner.js +298 -298
- package/_modules/knowledge-engine/ui/app.js +23 -2
- package/_site/README.md +30 -30
- package/_site/_test/agent-capture-mcp-ui-test.js +68 -68
- package/_site/_test/agent-capture-ui-test.js +101 -101
- package/_site/_test/ai-profile-resolver-test.js +137 -137
- package/_site/_test/ai-profile-test.js +75 -75
- package/_site/_test/ai-vendor-presets-test.js +91 -91
- package/_site/_test/automatic-context-test.js +70 -70
- package/_site/_test/automation-queue-test.js +14 -14
- package/_site/_test/automation-ui-test.js +40 -40
- package/_site/_test/background-task-registry-test.js +43 -43
- package/_site/_test/baseline-schema-test.js +91 -91
- package/_site/_test/bin-cli-test.js +230 -230
- package/_site/_test/bridge-adapter-test.js +95 -95
- package/_site/_test/bridge-consumer-service-test.js +310 -310
- package/_site/_test/candidate-bin-content-test.js +35 -35
- package/_site/_test/candidate-product-assets-test.js +13 -13
- package/_site/_test/candidate-release-contract-test.js +14 -14
- package/_site/_test/capture-hook-dedup-test.js +158 -158
- package/_site/_test/cdp-browser-failure-test.js +103 -103
- package/_site/_test/chat-claudecodeui-match-test.js +92 -92
- package/_site/_test/claude-executable-discovery-test.js +90 -90
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
- package/_site/_test/claude-workbench-test.js +174 -174
- package/_site/_test/cli-instance-management-test.js +62 -62
- package/_site/_test/client-capture-restart-e2e-test.js +189 -189
- package/_site/_test/codex-conversation-projection-test.js +60 -60
- package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
- package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
- package/_site/_test/codex-notify-fanout-test.js +141 -134
- package/_site/_test/codex-notify-upgrade-test.js +253 -253
- package/_site/_test/commit-boundary-freeze-test.js +147 -147
- package/_site/_test/commit-conversation-binding-test.js +99 -99
- package/_site/_test/commit-evidence-test.js +110 -110
- package/_site/_test/configuration-generation-test.js +22 -22
- package/_site/_test/context-pack-test.js +200 -200
- package/_site/_test/conversation-api-test.js +119 -119
- package/_site/_test/conversation-origin-test.js +15 -15
- package/_site/_test/conversation-store-test.js +138 -138
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
- package/_site/_test/desktop-browser-compat-test.js +28 -28
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
- package/_site/_test/embedding-config-test.js +32 -32
- package/_site/_test/explicit-commit-processor-test.js +37 -37
- package/_site/_test/fixtures/fake-browser-exit.js +10 -10
- package/_site/_test/fixtures/fake-browser-hang.js +10 -10
- package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
- package/_site/_test/fixtures/make-git-repos.js +92 -92
- package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
- package/_site/_test/folder-picker-output-test.js +19 -19
- package/_site/_test/fresh-hook-reimport-test.js +53 -53
- package/_site/_test/fresh-product-data-test.js +56 -56
- package/_site/_test/full-integration-e2e-test.js +167 -167
- package/_site/_test/git-path-compatibility-test.js +80 -80
- package/_site/_test/git-validation-test.js +81 -81
- package/_site/_test/glm-ui-refresh-review-test.js +313 -404
- package/_site/_test/harness-isolation-test.js +138 -0
- package/_site/_test/helpers/cdp-browser.js +383 -0
- package/_site/_test/helpers/cdp-network-capture.js +66 -0
- package/_site/_test/helpers/find-chrome.js +34 -0
- package/_site/_test/helpers/free-port.js +8 -0
- package/_site/_test/helpers/logging-ui-fixture.js +131 -0
- package/_site/_test/helpers/module-fixture.js +282 -0
- package/_site/_test/helpers/parallel-engine-advanced-engine-child.js +45 -0
- package/_site/_test/helpers/parallel-engine-advanced-fault-writer.js +171 -0
- package/_site/_test/helpers/parallel-engine-advanced-harness.js +245 -0
- package/_site/_test/helpers/parallel-engine-advanced-standin-analyzer.js +80 -0
- package/_site/_test/helpers/parallel-engine-advanced-ui-host.js +177 -0
- package/_site/_test/helpers/parallel-engine-final-harness.js +314 -0
- package/_site/_test/helpers/parallel-engine-final-standin-analyzer.js +159 -0
- package/_site/_test/helpers/parallel-knowledge-fixture.js +255 -0
- package/_site/_test/helpers/parallel-logging-acceptance-fixture.js +111 -0
- package/_site/_test/helpers/parallel-logging-acceptance-proxy.js +106 -0
- package/_site/_test/helpers/parallel-logging-fix-r2-proxy.js +205 -0
- package/_site/_test/helpers/parallel-logging-integration-5025-env.js +432 -0
- package/_site/_test/helpers/parallel-memory-analyzer.js +81 -0
- package/_site/_test/helpers/parallel-memory-event-bridge.js +128 -0
- package/_site/_test/helpers/parallel-memory-shell.js +470 -0
- package/_site/_test/helpers/parallel-nav-isolation-env.js +389 -0
- package/_site/_test/helpers/parallel-nav-module-ui.js +186 -0
- package/_site/_test/helpers/parallel-nav-proxy.js +148 -0
- package/_site/_test/helpers/parallel-nav-shell.js +361 -0
- package/_site/_test/helpers/parallel-qa-engine-endpoint-guard.js +206 -0
- package/_site/_test/helpers/parallel-qa-engine-endpoint-isolate.js +257 -0
- package/_site/_test/helpers/parallel-retrieval-followup-corpus.js +293 -0
- package/_site/_test/helpers/parallel-retrieval-followup-next-session.js +70 -0
- package/_site/_test/helpers/parallel-retrieval-followup-r2-shell.js +737 -0
- package/_site/_test/helpers/parallel-retrieval-followup-r2-wb-host.js +82 -0
- package/_site/_test/helpers/parallel-retrieval-followup-shell.js +279 -0
- package/_site/_test/helpers/parallel-workbench-fixture.js +371 -0
- package/_site/_test/helpers/parallel-workbench-host.js +80 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-fixture.js +408 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-harness.js +593 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-host.js +70 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-launcher.js +240 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-runtime-harness.js +630 -0
- package/_site/_test/helpers/parallel-workbench-source-map.js +196 -0
- package/_site/_test/helpers/spawn-server.js +106 -0
- package/_site/_test/helpers/version-visibility-fixture.js +251 -0
- package/_site/_test/hook-runtime-endpoint-test.js +88 -88
- package/_site/_test/hook-status-repair-api-test.js +152 -152
- package/_site/_test/hook-trigger-test.js +85 -85
- package/_site/_test/hook-worktree-test.js +42 -42
- package/_site/_test/import-preflight-api-test.js +234 -234
- package/_site/_test/index-builder-test.js +41 -41
- package/_site/_test/index-error-display-contract-test.js +10 -10
- package/_site/_test/index-writer-concurrency-test.js +135 -135
- package/_site/_test/integration-adapters-test.js +262 -262
- package/_site/_test/integration-auto-repair-test.js +113 -113
- package/_site/_test/integration-capture-setup-test.js +118 -118
- package/_site/_test/integration-intent-test.js +81 -81
- package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
- package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
- package/_site/_test/integration-runtime-binding-test.js +473 -473
- package/_site/_test/integration-status-scan-test.js +29 -29
- package/_site/_test/integration-surface-coverage-test.js +164 -164
- package/_site/_test/kb-framework-test.js +57 -57
- package/_site/_test/kbpath-follow-test.js +63 -63
- package/_site/_test/knowledge-db-test.js +144 -144
- package/_site/_test/knowledge-engine-client-test.js +120 -109
- package/_site/_test/knowledge-language-control-test.js +170 -170
- package/_site/_test/knowledge-maintenance-test.js +40 -40
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
- package/_site/_test/knowledge-query-test.js +54 -54
- package/_site/_test/knowledge-retrieval-service-test.js +77 -77
- package/_site/_test/knowledge-scopes-test.js +37 -37
- package/_site/_test/knowledge-storage-location-test.js +28 -28
- package/_site/_test/knowledge-storage-startup-test.js +52 -52
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
- package/_site/_test/launcher-readiness-test.js +80 -80
- package/_site/_test/legacy-claude-session-alias-test.js +106 -106
- package/_site/_test/legacy-routes-removed-test.js +47 -47
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
- package/_site/_test/live-gateway-test.js +48 -48
- package/_site/_test/local-marketplace-test.js +16 -16
- package/_site/_test/log-redaction-test.js +58 -58
- package/_site/_test/logging-api-test.js +89 -89
- package/_site/_test/logging-sse-no-gap-test.js +88 -88
- package/_site/_test/logging-ui-test.js +107 -107
- package/_site/_test/markdown-delta-overlay-test.js +85 -85
- package/_site/_test/markdown-maintenance-api-test.js +75 -75
- package/_site/_test/markdown-maintenance-test.js +11 -11
- package/_site/_test/mcp-server-test.js +149 -149
- package/_site/_test/model-context-windows-test.js +89 -89
- package/_site/_test/module-agent-start-proxy-test.js +65 -65
- package/_site/_test/module-artifact-integrity-test.js +27 -27
- package/_site/_test/module-boundary-test.js +30 -30
- package/_site/_test/module-bridge-eventbridge-test.js +202 -202
- package/_site/_test/module-dynamic-port-test.js +118 -118
- package/_site/_test/module-index-proxy-test.js +33 -33
- package/_site/_test/module-model-configuration-test.js +44 -44
- package/_site/_test/module-process-lifecycle-test.js +35 -35
- package/_site/_test/module-stream-proxy-test.js +34 -34
- package/_site/_test/module-terminal-restart-test.js +63 -63
- package/_site/_test/module-wiring-unbind-race-test.js +59 -59
- package/_site/_test/native-context-integrations-test.js +86 -86
- package/_site/_test/non-release-ci-test.js +34 -34
- package/_site/_test/offline-boundary-isolation-test.js +32 -32
- package/_site/_test/orphaned-claim-recovery-test.js +80 -80
- package/_site/_test/package-boundary-test.js +43 -43
- package/_site/_test/package-startup-test.js +56 -56
- package/_site/_test/packaged-ui-smoke-test.js +88 -88
- package/_site/_test/parallel-ci-r9-posix-socket-budget-test.js +156 -0
- package/_site/_test/parallel-engine-advanced-eng005-test.js +136 -0
- package/_site/_test/parallel-engine-advanced-eng006-test.js +171 -0
- package/_site/_test/parallel-engine-advanced-eng007-reconcile-test.js +231 -0
- package/_site/_test/parallel-engine-advanced-eng009-test.js +208 -0
- package/_site/_test/parallel-engine-advanced-eng010-test.js +213 -0
- package/_site/_test/parallel-engine-advanced-eng011-ui-probe.js +459 -0
- package/_site/_test/parallel-engine-final-eng002-test.js +255 -0
- package/_site/_test/parallel-engine-final-eng007-test.js +269 -0
- package/_site/_test/parallel-engine-final-eng011-test.js +197 -0
- package/_site/_test/parallel-engine-final-eng012-test.js +354 -0
- package/_site/_test/parallel-knowledge-client-error-code-test.js +184 -0
- package/_site/_test/parallel-knowledge-engine-test.js +353 -0
- package/_site/_test/parallel-knowledge-retrieval-test.js +292 -0
- package/_site/_test/parallel-knowledge-vector-test.js +168 -0
- package/_site/_test/parallel-linux-ci-r7-log007-day-test.js +110 -0
- package/_site/_test/parallel-logging-acceptance-log003-test.js +224 -0
- package/_site/_test/parallel-logging-acceptance-log004-test.js +231 -0
- package/_site/_test/parallel-logging-acceptance-log005-test.js +208 -0
- package/_site/_test/parallel-logging-acceptance-log006-test.js +214 -0
- package/_site/_test/parallel-logging-acceptance-log007-test.js +223 -0
- package/_site/_test/parallel-logging-acceptance-log008-test.js +180 -0
- package/_site/_test/parallel-logging-fix-r2-crash-probe.js +271 -0
- package/_site/_test/parallel-logging-fix-r2-gap-test.js +294 -0
- package/_site/_test/parallel-logging-fix-r2-normalhook-test.js +330 -0
- package/_site/_test/parallel-logging-integration-5025-portability-test.js +554 -0
- package/_site/_test/parallel-memory-candidate-test.js +301 -0
- package/_site/_test/parallel-memory-manual-test.js +197 -0
- package/_site/_test/parallel-memory-scope-test.js +228 -0
- package/_site/_test/parallel-memory-source-test.js +227 -0
- package/_site/_test/parallel-memory-sync-test.js +238 -0
- package/_site/_test/parallel-memory-view-r8-reload-gate-test.js +123 -0
- package/_site/_test/parallel-nav-failure-test.js +119 -0
- package/_site/_test/parallel-nav-isolation-test.js +308 -0
- package/_site/_test/parallel-nav-isolation-vector-path-test.js +90 -0
- package/_site/_test/parallel-nav-offline-test.js +272 -0
- package/_site/_test/parallel-nav-operability-test.js +257 -0
- package/_site/_test/parallel-nav-switch-test.js +247 -0
- package/_site/_test/parallel-nav-theme-test.js +130 -0
- package/_site/_test/parallel-nav-viewport-test.js +310 -0
- package/_site/_test/parallel-qa-engine-endpoint-guard-test.js +273 -0
- package/_site/_test/parallel-retrieval-followup-citation-test.js +219 -0
- package/_site/_test/parallel-retrieval-followup-corpus-test.js +287 -0
- package/_site/_test/parallel-workbench-independent-host.js +262 -0
- package/_site/_test/parallel-workbench-integration-5025-abort-probe.js +340 -0
- package/_site/_test/parallel-workbench-integration-5025-drafts-probe.js +588 -0
- package/_site/_test/parallel-workbench-integration-5025-env-check.js +317 -0
- package/_site/_test/parallel-workbench-integration-5025-model-probe.js +342 -0
- package/_site/_test/parallel-workbench-integration-5025-ordering-probe.js +326 -0
- package/_site/_test/parallel-workbench-model-probe.js +336 -0
- package/_site/_test/parallel-workbench-offline-test.js +169 -0
- package/_site/_test/parallel-workbench-provenance-trace.js +134 -0
- package/_site/_test/parallel-workbench-source-match.js +206 -0
- package/_site/_test/parallel-workbench-standalone-turn.js +349 -0
- package/_site/_test/path-consistency-test.js +151 -151
- package/_site/_test/pending-sweep-test.js +9 -9
- package/_site/_test/post-commit-automation-test.js +87 -87
- package/_site/_test/pr-consumer-contract-test.js +55 -55
- package/_site/_test/product-brand-contract-test.js +25 -25
- package/_site/_test/product-diagnostics-ui-test.js +93 -93
- package/_site/_test/product-empty-state-test.js +148 -0
- package/_site/_test/product-import-ui-test.js +219 -104
- package/_site/_test/product-navigation-test.js +152 -0
- package/_site/_test/product-progress-status-test.js +84 -84
- package/_site/_test/project-delete-recovery-test.js +64 -64
- package/_site/_test/project-domain-create-test.js +91 -0
- package/_site/_test/project-domain-scope-retrieval-test.js +214 -0
- package/_site/_test/project-git-init-confirm-test.js +115 -0
- package/_site/_test/project-goal-editor-test.js +144 -144
- package/_site/_test/project-import-duplicate-alias-test.js +134 -0
- package/_site/_test/project-import-unicode-restart-test.js +140 -0
- package/_site/_test/project-lifecycle-transaction-test.js +106 -106
- package/_site/_test/project-module-partial-recovery-test.js +222 -0
- package/_site/_test/project-remove-running-guard-test.js +79 -79
- package/_site/_test/project-store-test.js +90 -90
- package/_site/_test/project-unbind-late-read-ui-test.js +250 -0
- package/_site/_test/project-unbind-writer-guard-test.js +231 -0
- package/_site/_test/prompt-settings-test.js +115 -115
- package/_site/_test/protected-architecture-gate-test.js +122 -122
- package/_site/_test/refactor-characterization-test.js +36 -36
- package/_site/_test/registry-events-test.js +38 -38
- package/_site/_test/release-version-sync-test.js +115 -115
- package/_site/_test/requirement-binding-test.js +113 -113
- package/_site/_test/requirement-recorder-test.js +173 -173
- package/_site/_test/run-all-tests.js +173 -161
- package/_site/_test/runtime-endpoint-test.js +64 -64
- package/_site/_test/scanner-test.js +83 -83
- package/_site/_test/server-security-test.js +96 -96
- package/_site/_test/sessions-stream-test.js +93 -93
- package/_site/_test/shared-contracts-test.js +58 -58
- package/_site/_test/shared-scope-root-binding-test.js +74 -67
- package/_site/_test/simple-import-test.js +98 -98
- package/_site/_test/startup-analysis-disabled-test.js +16 -16
- package/_site/_test/storage-foundation-test.js +78 -78
- package/_site/_test/structured-logger-test.js +101 -101
- package/_site/_test/t07b-cln-005-coverage-test.js +212 -0
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
- package/_site/_test/team-knowledge-store-test.js +132 -132
- package/_site/_test/test-server-cleanup-test.js +35 -35
- package/_site/_test/test-server-module-isolation-test.js +37 -37
- package/_site/_test/tracking-start-test.js +144 -144
- package/_site/_test/vector-storage-root-test.js +75 -75
- package/_site/_test/version-visibility-browser-probe.js +249 -0
- package/_site/_test/version-visibility-test.js +92 -0
- package/_site/_test/workbench-context-receipts-test.js +22 -22
- package/_site/_test/workbench-permission-test.js +117 -117
- package/_site/_test/zcode-capture-integration-test.js +112 -112
- package/_site/lib/ai-profile-resolver.js +78 -78
- package/_site/lib/ai-vendor-presets.js +83 -83
- package/_site/lib/ai-workspace.js +104 -104
- package/_site/lib/atomic-file.js +204 -204
- package/_site/lib/automation-config.js +140 -140
- package/_site/lib/bridge-adapter.js +260 -260
- package/_site/lib/bridge-consumer-service.js +402 -402
- package/_site/lib/capture-hook-config.js +108 -108
- package/_site/lib/claude-cli-runner.js +1599 -1599
- package/_site/lib/cli-instances.js +43 -43
- package/_site/lib/codex-context-status.js +17 -17
- package/_site/lib/codex-notify-config.js +193 -193
- package/_site/lib/commit-conversation-binder.js +247 -247
- package/_site/lib/commit-processing-ledger.js +27 -27
- package/_site/lib/commit-prompt.js +202 -202
- package/_site/lib/commit-reconciler.js +444 -444
- package/_site/lib/context-pack-builder.js +299 -299
- package/_site/lib/contracts.js +291 -291
- package/_site/lib/conversation-exclusions.js +204 -204
- package/_site/lib/conversation-query-service.js +161 -161
- package/_site/lib/conversation-store.js +387 -387
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -93
- package/_site/lib/engine-knowledge-client.js +76 -72
- package/_site/lib/evidence-bundle.js +228 -228
- package/_site/lib/folder-picker-output.js +24 -24
- package/_site/lib/git-path.js +14 -14
- package/_site/lib/git-runner.js +62 -62
- package/_site/lib/github-team-store.js +1175 -1175
- package/_site/lib/hook-manager.js +203 -203
- package/_site/lib/index-builder.js +157 -157
- package/_site/lib/index-service.js +173 -173
- package/_site/lib/integration-auto-repair.js +77 -77
- package/_site/lib/integration-installer.js +1240 -1240
- package/_site/lib/integration-intent.js +88 -88
- package/_site/lib/integration-status-scan.js +99 -99
- package/_site/lib/integration-status-worker.js +14 -14
- package/_site/lib/kb-framework.js +191 -191
- package/_site/lib/kb-validator.js +124 -124
- package/_site/lib/knowledge-db.js +363 -363
- package/_site/lib/knowledge-promotion.js +435 -435
- package/_site/lib/knowledge-retrieval-service.js +401 -401
- package/_site/lib/knowledge-schema.js +121 -121
- package/_site/lib/knowledge-tool-runtime.js +366 -366
- package/_site/lib/live-gateway.js +78 -78
- package/_site/lib/llm-client.js +173 -173
- package/_site/lib/markdown-knowledge-indexer.js +230 -230
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -874
- package/_site/lib/module-port-resolver.js +57 -57
- package/_site/lib/post-commit-automation.js +141 -141
- package/_site/lib/product-data.js +35 -35
- package/_site/lib/project-lifecycle-service.js +554 -554
- package/_site/lib/project-registry-store.js +129 -129
- package/_site/lib/project-store.js +268 -268
- package/_site/lib/prompt-registry.js +148 -148
- package/_site/lib/public-version-info.js +37 -0
- package/_site/lib/registry-events.js +16 -16
- package/_site/lib/requirement-adapters.js +10 -10
- package/_site/lib/requirement-binder.js +112 -112
- package/_site/lib/requirement-recorder.js +278 -278
- package/_site/lib/runtime-control.js +89 -89
- package/_site/lib/runtime-endpoint.js +160 -160
- package/_site/lib/scanner.js +298 -298
- package/_site/lib/server-app.js +1672 -1664
- package/_site/lib/settings-store.js +153 -153
- package/_site/lib/storage-layout.js +166 -166
- package/_site/lib/structured-logger.js +574 -574
- package/_site/lib/zcode-context-integration.js +60 -60
- package/_site/scripts/codex-notify-fanout.cjs +95 -95
- package/_site/scripts/folder-picker.ps1 +156 -156
- package/_site/scripts/hook-trigger.js +158 -158
- package/_site/scripts/install-module-candidates.js +69 -69
- package/_site/scripts/pack-module-candidates.js +89 -89
- package/_site/scripts/sync-product-content.js +28 -28
- package/_site/scripts/sync-release-version.js +164 -164
- package/_site/scripts/vendor-modules.js +49 -49
- package/_site/scripts/verify-module-runtime.js +50 -50
- package/_site/scripts/verify-product-runtime.js +36 -36
- package/_site/scripts/verify-retrieval-gold.js +83 -83
- package/_site/server.js +15 -15
- package/_site/start.bat +26 -26
- package/_site/stop.bat +11 -11
- package/bin/project-knowledge-integrations.js +254 -254
- package/bin/project-knowledge-kb.js +67 -67
- package/bin/project-knowledge-mcp.js +201 -201
- package/bin/project-knowledge.js +269 -170
- package/claude-prompts.json +21 -21
- package/docs/README.zh-CN.md +207 -205
- package/docs/assets/architecture.svg +181 -181
- package/docs/assets/logo.svg +49 -49
- package/docs/fresh-v5-start.md +44 -44
- package/docs/leader-glm-workbench-playbook.md +122 -122
- package/docs/project-registry-schema.md +33 -33
- package/docs/release-5.0.16-acceptance.md +176 -176
- package/docs/release-5.0.17-acceptance.md +117 -117
- package/docs/release-5.0.19-acceptance.md +39 -39
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/docs/testing-strategy.md +33 -33
- package/module-runtime-manifest.json +70 -67
- package/package.json +113 -110
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/templates/change.md +30 -30
- package/templates/project-readme.md +52 -52
- package/ui/index.html +3 -2
- package/ui/version-info.css +44 -0
- package/ui/version-info.js +24 -0
- package/vendor-manifest.json +55 -55
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// P-LOG-SAFETY-R2 deterministic LOG-004 boundary (snapshot→subscription
|
|
3
|
+
// handoff) acceptance — lane-exclusive prefix parallel-logging-fix-r2-*.
|
|
4
|
+
//
|
|
5
|
+
// The real UI protocol (ui/product.js loadLogs): close old stream → GET
|
|
6
|
+
// snapshot → render → subscribe /api/logs/stream with the SNAPSHOT's
|
|
7
|
+
// streamCursor. The deterministic window is therefore exactly "server has
|
|
8
|
+
// produced the snapshot, browser has not received it yet": during that
|
|
9
|
+
// window the log subscription does not exist yet.
|
|
10
|
+
//
|
|
11
|
+
// This driver proves the handoff has no gap and no duplicate, without any
|
|
12
|
+
// sleep-based guess:
|
|
13
|
+
// 1. The r2 observation proxy holds the REAL snapshot response after the
|
|
14
|
+
// server produced it (original bytes, sha256-recorded).
|
|
15
|
+
// 2. While held: wire evidence shows NO logs/stream subscription yet
|
|
16
|
+
// (proxy frame timeline + real page KnowledgeLive.stats), and a REAL
|
|
17
|
+
// product write (PUT /api/projects/<id>/goal) lands a unique record on
|
|
18
|
+
// disk with id/ts AFTER the snapshot cursor.
|
|
19
|
+
// 3. The original snapshot bytes are released untouched; the page then
|
|
20
|
+
// subscribes carrying exactly the snapshot cursor; the server's
|
|
21
|
+
// streamLogsWithoutGap replays the gap record (replay:true).
|
|
22
|
+
// 4. The unique record appears in DOM / API / disk exactly once each.
|
|
23
|
+
// 5. Steady-state control afterwards: a second write arrives live once.
|
|
24
|
+
//
|
|
25
|
+
// The driver never writes fake log entries, never rewrites response records,
|
|
26
|
+
// and never simulates the product's own subscription.
|
|
27
|
+
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const crypto = require('node:crypto');
|
|
31
|
+
|
|
32
|
+
// Portable isolation (P-QA-LOG-INTEGRATION-5025): the temp root comes from
|
|
33
|
+
// PK_TEST_TEMP_ROOT or os.tmpdir() and must live outside the source tree;
|
|
34
|
+
// HOME/APPDATA/LOCALAPPDATA/TEMP/CODEX_HOME/CLAUDE_CONFIG_DIR and the dual
|
|
35
|
+
// product data roots are redirected before any other module is loaded.
|
|
36
|
+
const { installLoggingTestEnv, resolveEvidenceDir } = require('./helpers/parallel-logging-integration-5025-env');
|
|
37
|
+
const lane = installLoggingTestEnv({ caseId: 'LOGFIXR2-GAP', prefix: 'logfixr2-gap' });
|
|
38
|
+
const laneTmp = lane.laneTmp;
|
|
39
|
+
|
|
40
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
41
|
+
const { findChrome } = require('./helpers/find-chrome');
|
|
42
|
+
const { launchCdpBrowser, waitFor } = require('./helpers/cdp-browser');
|
|
43
|
+
const freePort = require('./helpers/free-port');
|
|
44
|
+
const { createLoggingAcceptanceFixture, writeProjectLogs } = require('./helpers/parallel-logging-acceptance-fixture');
|
|
45
|
+
const { createGapProxy, decodeStreamCursor } = require('./helpers/parallel-logging-fix-r2-proxy');
|
|
46
|
+
|
|
47
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
48
|
+
const ARTIFACTS = resolveEvidenceDir(path.resolve(ROOT, 'qa-autopilot/artifacts/parallel-20261023/logging-fix-r2/evidence/gap'));
|
|
49
|
+
|
|
50
|
+
const PROJECT_A = 'project-logfixr2-gap';
|
|
51
|
+
const run = crypto.randomBytes(6).toString('hex');
|
|
52
|
+
const GAP_BYTES = 1733; // unique goal content length for the boundary write
|
|
53
|
+
const STEADY_BYTES = 1777; // unique length for the steady-state control write
|
|
54
|
+
// Below the run-all per-test budget (PK_TEST_TIMEOUT_MS, default 90s) so a
|
|
55
|
+
// stall still lets this entry write its own evidence before the runner kills
|
|
56
|
+
// the child.
|
|
57
|
+
const HARD_TIMEOUT_MS = 80000;
|
|
58
|
+
|
|
59
|
+
const now = () => new Date().toISOString();
|
|
60
|
+
|
|
61
|
+
function projectDayRecords(dataDir, projectId) {
|
|
62
|
+
const dir = path.join(dataDir, 'logs', 'projects', projectId);
|
|
63
|
+
const records = [];
|
|
64
|
+
if (!fs.existsSync(dir)) return records;
|
|
65
|
+
for (const file of fs.readdirSync(dir).filter(name => name.endsWith('.jsonl')).sort()) {
|
|
66
|
+
for (const line of fs.readFileSync(path.join(dir, file), 'utf8').split('\n')) {
|
|
67
|
+
if (line.trim()) { try { records.push(JSON.parse(line)); } catch {} }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return records;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const padTo = (marker, total) => marker + ' ' + 'x'.repeat(Math.max(0, total - marker.length - 1));
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
fs.mkdirSync(ARTIFACTS, { recursive: true });
|
|
77
|
+
const evidence = { caseId: 'LOG-004 (R2 deterministic boundary)', run, startedAt: now(), phases: {} };
|
|
78
|
+
|
|
79
|
+
const seedMarkers = [`LOGFIXR2-GAP-SEED1-${run}`, `LOGFIXR2-GAP-SEED2-${run}`];
|
|
80
|
+
const fixture = await createLoggingAcceptanceFixture({
|
|
81
|
+
caseId: 'logfixr2gap',
|
|
82
|
+
projects: [{ projectId: PROJECT_A, displayName: 'LOGFIXR2交界项目' }],
|
|
83
|
+
});
|
|
84
|
+
await writeProjectLogs(fixture.logger, PROJECT_A, [
|
|
85
|
+
{ level: 'info', event: 'logfixr2.gap.seed1', message: seedMarkers[0], operationId: `op-logfixr2-seed1-${run}`, component: 'logfixr2-gap' },
|
|
86
|
+
{ level: 'warn', event: 'logfixr2.gap.seed2', message: seedMarkers[1], operationId: `op-logfixr2-seed2-${run}`, component: 'logfixr2-gap' },
|
|
87
|
+
]);
|
|
88
|
+
await fixture.logger.close();
|
|
89
|
+
|
|
90
|
+
const port = await freePort();
|
|
91
|
+
const proxyPort = await freePort();
|
|
92
|
+
const serverBase = `http://127.0.0.1:${port}`;
|
|
93
|
+
const server = spawnServer({ root: ROOT, port, dataDir: fixture.dataDir, tag: 'logfixr2gap', stdio: 'ignore' });
|
|
94
|
+
let browser;
|
|
95
|
+
const proxy = await createGapProxy({ targetPort: port, port: proxyPort });
|
|
96
|
+
try {
|
|
97
|
+
await waitFor(async () => { try { return (await fetch(`${serverBase}/api/health`)).ok; } catch { return false; } }, 'isolated server healthy', 30000);
|
|
98
|
+
evidence.server = { pid: server.child.pid, port, proxyPort, dataDir: fixture.dataDir };
|
|
99
|
+
|
|
100
|
+
browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(laneTmp, 'browser'), url: proxy.url, width: 1440, height: 900 });
|
|
101
|
+
const runExpr = expression => browser.evaluate(expression);
|
|
102
|
+
const click = selector => runExpr(`document.querySelector(${JSON.stringify(selector)}).click()`);
|
|
103
|
+
evidence.browser = await runExpr('navigator.userAgent');
|
|
104
|
+
await waitFor(async () => runExpr(`document.getElementById('projectSelect') && document.getElementById('projectSelect').length >= 1`), 'project registered', 20000);
|
|
105
|
+
await runExpr(`document.getElementById('projectSelect').value = ${JSON.stringify(PROJECT_A)}; document.getElementById('projectSelect').dispatchEvent(new Event('change'))`);
|
|
106
|
+
await waitFor(async () => runExpr(`document.getElementById('projectSelect').value === ${JSON.stringify(PROJECT_A)}`), 'project A selected');
|
|
107
|
+
|
|
108
|
+
// Baseline: page live subscriptions before the diagnostics dialog exists.
|
|
109
|
+
const statsBaseline = JSON.parse(await runExpr(`JSON.stringify(window.KnowledgeLive ? window.KnowledgeLive.stats() : null)`));
|
|
110
|
+
evidence.phases.statsBaseline = statsBaseline;
|
|
111
|
+
|
|
112
|
+
// Arm the barrier, then open the diagnostics dialog (real user path).
|
|
113
|
+
proxy.armSnapshotBarrier();
|
|
114
|
+
await click('#openSystem');
|
|
115
|
+
await click('#openLogs');
|
|
116
|
+
const barrier = await proxy.waitBarrier(10000);
|
|
117
|
+
evidence.phases.barrier = {
|
|
118
|
+
heldAt: barrier.heldAt,
|
|
119
|
+
status: barrier.status,
|
|
120
|
+
entries: barrier.entries,
|
|
121
|
+
newestEntryId: barrier.newestEntryId,
|
|
122
|
+
streamCursor: barrier.streamCursor,
|
|
123
|
+
cursorDecoded: decodeStreamCursor(barrier.streamCursor),
|
|
124
|
+
responseSha256: barrier.sha256,
|
|
125
|
+
byteLength: barrier.byteLength,
|
|
126
|
+
};
|
|
127
|
+
const snapshotCursorDecoded = evidence.phases.barrier.cursorDecoded;
|
|
128
|
+
|
|
129
|
+
// While the snapshot is held: the page has NOT yet subscribed to the log
|
|
130
|
+
// stream (wire truth + page truth), and a real product write lands a
|
|
131
|
+
// unique record after the snapshot cursor.
|
|
132
|
+
const statsWhileHeld = JSON.parse(await runExpr(`JSON.stringify(window.KnowledgeLive ? window.KnowledgeLive.stats() : null)`));
|
|
133
|
+
const wsBeforeRelease = proxy.events.filter(e => e.kind === 'ws-frame' && e.type === 'subscribe');
|
|
134
|
+
const logsSubscribeBeforeRelease = wsBeforeRelease.filter(e => (e.route || '').includes('/api/logs/stream'));
|
|
135
|
+
evidence.phases.whileHeld = {
|
|
136
|
+
statsWhileHeld,
|
|
137
|
+
wsSubscribeFramesBeforeRelease: wsBeforeRelease.map(e => ({ seq: e.seq, route: e.route })),
|
|
138
|
+
logsStreamSubscriptionEstablished: logsSubscribeBeforeRelease.length > 0,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const goalRes = await fetch(`${proxy.url}/api/projects/${PROJECT_A}/goal`, {
|
|
142
|
+
method: 'PUT',
|
|
143
|
+
headers: { 'content-type': 'application/json' },
|
|
144
|
+
body: JSON.stringify({ content: padTo(`LOGFIXR2-GAP-BOUNDARY-${run}`, GAP_BYTES) }),
|
|
145
|
+
});
|
|
146
|
+
evidence.phases.boundaryWrite = { at: now(), status: goalRes.status, contentBytes: GAP_BYTES };
|
|
147
|
+
|
|
148
|
+
proxy.releaseBarrier();
|
|
149
|
+
evidence.phases.releaseAt = now();
|
|
150
|
+
|
|
151
|
+
// Wait for the page's own handoff: snapshot rendered, then subscription
|
|
152
|
+
// established with the snapshot cursor, then the gap record replayed.
|
|
153
|
+
await waitFor(async () => runExpr(`document.getElementById('logsDialog').open === true && document.querySelectorAll('#logsContent .log-entry').length >= 3`), 'snapshot rendered after release', 15000);
|
|
154
|
+
|
|
155
|
+
const domState = () => runExpr(`JSON.stringify((() => {
|
|
156
|
+
const details = [...document.querySelectorAll('#logsContent .log-entry .log-detail')];
|
|
157
|
+
const records = details.map(node => { try { return JSON.parse(node.textContent); } catch { return null; } }).filter(Boolean);
|
|
158
|
+
const ids = records.map(r => r.id);
|
|
159
|
+
return {
|
|
160
|
+
count: document.querySelectorAll('#logsContent .log-entry').length,
|
|
161
|
+
uniqueIds: new Set(ids).size === ids.length,
|
|
162
|
+
boundaryRecords: records.filter(r => r.event === 'goal.updated' && r.context && r.context.bytes === ${GAP_BYTES}).map(r => ({ id: r.id, ts: r.ts })),
|
|
163
|
+
steadyRecords: records.filter(r => r.event === 'goal.updated' && r.context && r.context.bytes === ${STEADY_BYTES}).map(r => ({ id: r.id, ts: r.ts })),
|
|
164
|
+
domCursor: document.getElementById('logsDialog').dataset.streamCursor || '',
|
|
165
|
+
};
|
|
166
|
+
})())`).then(value => (typeof value === 'string' ? JSON.parse(value) : value));
|
|
167
|
+
const afterHandoff = await waitFor(async () => {
|
|
168
|
+
const state = await domState();
|
|
169
|
+
return state.boundaryRecords.length === 1 ? state : null;
|
|
170
|
+
}, 'boundary record visible exactly once in DOM', 20000);
|
|
171
|
+
evidence.phases.domAfterHandoff = afterHandoff;
|
|
172
|
+
|
|
173
|
+
// Wire timeline: exactly one logs/stream subscription, AFTER release,
|
|
174
|
+
// carrying exactly the snapshot cursor; the boundary record delivered by
|
|
175
|
+
// the gateway exactly once (capture its replay flag); logs/ready seen.
|
|
176
|
+
const subscribeFrames = proxy.events.filter(e => e.kind === 'ws-frame' && e.type === 'subscribe' && (e.route || '').includes('/api/logs/stream'));
|
|
177
|
+
const releaseSeq = proxy.events.find(e => e.kind === 'barrier-released')?.seq;
|
|
178
|
+
evidence.phases.handoffWire = {
|
|
179
|
+
releaseSeq,
|
|
180
|
+
subscribeFrames: subscribeFrames.map(e => ({ seq: e.seq, at: e.at, route: e.route })),
|
|
181
|
+
subscribeCursor: (() => {
|
|
182
|
+
const route = subscribeFrames[0]?.route || '';
|
|
183
|
+
const match = /[?&]streamCursor=([^&]+)/.exec(route);
|
|
184
|
+
return match ? { raw: decodeURIComponent(match[1]), decoded: decodeStreamCursor(decodeURIComponent(match[1])) } : null;
|
|
185
|
+
})(),
|
|
186
|
+
boundaryFrames: proxy.events.filter(e => e.kind === 'ws-frame' && e.type === 'event' && e.event === 'logs/appended' && (e.dataSnippet || '').includes(`"bytes":${GAP_BYTES}`)).map(e => ({ seq: e.seq, at: e.at, replay: (e.dataSnippet || '').includes('"replay":true'), snippetHead: (e.dataSnippet || '').slice(0, 220) })),
|
|
187
|
+
readyFrames: proxy.events.filter(e => e.kind === 'ws-frame' && e.type === 'event' && e.event === 'logs/ready').map(e => ({ seq: e.seq, at: e.at, cursorDecoded: decodeStreamCursor((() => { const m = /"streamCursor":"([^"]+)"/.exec(e.dataSnippet || ''); return m ? m[1] : ''; })()) })),
|
|
188
|
+
};
|
|
189
|
+
const statsAfterHandoff = JSON.parse(await runExpr(`JSON.stringify(window.KnowledgeLive ? window.KnowledgeLive.stats() : null)`));
|
|
190
|
+
evidence.phases.statsAfterHandoff = statsAfterHandoff;
|
|
191
|
+
await browser.screenshot(path.join(ARTIFACTS, 'gap-after-handoff.png'));
|
|
192
|
+
|
|
193
|
+
// API truth and disk truth: exactly once each; id strictly newer than the
|
|
194
|
+
// snapshot cursor's newest entry id.
|
|
195
|
+
const apiRes = await fetch(`${serverBase}/api/logs?pageSize=100&projectId=${PROJECT_A}`);
|
|
196
|
+
const apiBody = await apiRes.json();
|
|
197
|
+
const apiBoundary = (apiBody.entries || []).filter(e => e.event === 'goal.updated' && e.context && e.context.bytes === GAP_BYTES);
|
|
198
|
+
const disk = projectDayRecords(fixture.dataDir, PROJECT_A);
|
|
199
|
+
const diskBoundary = disk.filter(r => r.event === 'goal.updated' && r.context && r.context.bytes === GAP_BYTES);
|
|
200
|
+
const snapshotNewestId = evidence.phases.barrier.newestEntryId;
|
|
201
|
+
evidence.phases.parity = {
|
|
202
|
+
apiBoundaryCount: apiBoundary.length,
|
|
203
|
+
apiBoundaryId: apiBoundary[0]?.id || null,
|
|
204
|
+
diskBoundaryCount: diskBoundary.length,
|
|
205
|
+
diskBoundaryId: diskBoundary[0]?.id || null,
|
|
206
|
+
snapshotNewestId,
|
|
207
|
+
boundaryNewerThanSnapshot: Boolean(apiBoundary[0] && snapshotNewestId && apiBoundary[0].id !== snapshotNewestId),
|
|
208
|
+
boundaryRecordTs: apiBoundary[0]?.ts || null,
|
|
209
|
+
cursorAfterTs: snapshotCursorDecoded?.afterTs || null,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Steady-state control (already-accepted half of LOG-004, re-proven on
|
|
213
|
+
// this instance): a second real write arrives live exactly once.
|
|
214
|
+
const steadyRes = await fetch(`${proxy.url}/api/projects/${PROJECT_A}/goal`, {
|
|
215
|
+
method: 'PUT',
|
|
216
|
+
headers: { 'content-type': 'application/json' },
|
|
217
|
+
body: JSON.stringify({ content: padTo(`LOGFIXR2-GAP-STEADY-${run}`, STEADY_BYTES) }),
|
|
218
|
+
});
|
|
219
|
+
evidence.phases.steadyWrite = { at: now(), status: steadyRes.status, contentBytes: STEADY_BYTES };
|
|
220
|
+
const steadyState = await waitFor(async () => {
|
|
221
|
+
const state = await domState();
|
|
222
|
+
return state.steadyRecords.length === 1 ? state : null;
|
|
223
|
+
}, 'steady-state record visible exactly once in DOM', 20000);
|
|
224
|
+
evidence.phases.domSteady = { steadyRecords: steadyState.steadyRecords, uniqueIds: steadyState.uniqueIds };
|
|
225
|
+
evidence.phases.steadyWire = {
|
|
226
|
+
frames: proxy.events.filter(e => e.kind === 'ws-frame' && e.type === 'event' && e.event === 'logs/appended' && (e.dataSnippet || '').includes(`"bytes":${STEADY_BYTES}`)).map(e => ({ seq: e.seq, at: e.at, replay: (e.dataSnippet || '').includes('"replay":true') })),
|
|
227
|
+
};
|
|
228
|
+
await browser.screenshot(path.join(ARTIFACTS, 'gap-steady.png'));
|
|
229
|
+
|
|
230
|
+
if (browser.exceptions.length) throw new Error(`browser exceptions: ${JSON.stringify(browser.exceptions)}`);
|
|
231
|
+
|
|
232
|
+
// Verdicts.
|
|
233
|
+
const heldWindowMs = barrier.heldAt ? Date.parse(evidence.phases.releaseAt) - Date.parse(barrier.heldAt) : null;
|
|
234
|
+
evidence.phases.heldWindowMs = heldWindowMs;
|
|
235
|
+
const boundaryVerdict = {
|
|
236
|
+
subscriptionAbsentWhileHeld: evidence.phases.whileHeld.logsStreamSubscriptionEstablished === false,
|
|
237
|
+
statsSubscriptionCountStableWhileHeld: statsWhileHeld && statsBaseline && statsWhileHeld.subscriptions === statsBaseline.subscriptions,
|
|
238
|
+
boundaryWriteWhileHeld: evidence.phases.boundaryWrite.status === 200,
|
|
239
|
+
releasedOriginalBytes: true,
|
|
240
|
+
subscribeAfterReleaseWithSnapshotCursor: subscribeFrames.length === 1 && subscribeFrames[0].seq > releaseSeq
|
|
241
|
+
&& evidence.phases.handoffWire.subscribeCursor?.raw === barrier.streamCursor,
|
|
242
|
+
boundaryInDomExactlyOnce: afterHandoff.boundaryRecords.length === 1,
|
|
243
|
+
domIdsUnique: afterHandoff.uniqueIds,
|
|
244
|
+
boundaryFrameExactlyOnce: evidence.phases.handoffWire.boundaryFrames.length === 1,
|
|
245
|
+
boundaryFrameIsReplayCatchup: evidence.phases.handoffWire.boundaryFrames[0]?.replay === true,
|
|
246
|
+
apiExactlyOnce: apiBoundary.length === 1,
|
|
247
|
+
diskExactlyOnce: diskBoundary.length === 1,
|
|
248
|
+
recordNewerThanSnapshotCursor: evidence.phases.parity.boundaryNewerThanSnapshot,
|
|
249
|
+
heldWindowUnderUiTimeout: heldWindowMs !== null && heldWindowMs < 2500,
|
|
250
|
+
};
|
|
251
|
+
const steadyVerdict = {
|
|
252
|
+
steadyInDomExactlyOnce: steadyState.steadyRecords.length === 1,
|
|
253
|
+
steadyFrameExactlyOnceLive: evidence.phases.steadyWire.frames.length === 1 && evidence.phases.steadyWire.frames[0].replay === false,
|
|
254
|
+
};
|
|
255
|
+
evidence.verdicts = {
|
|
256
|
+
'LOG-004 交界(快照→订阅)无丢失无重复': Object.values(boundaryVerdict).every(Boolean) ? 'PASS' : 'FAIL',
|
|
257
|
+
'LOG-004 稳态控制(实时一次到达)': Object.values(steadyVerdict).every(Boolean) ? 'PASS' : 'FAIL',
|
|
258
|
+
detail: { boundaryVerdict, steadyVerdict },
|
|
259
|
+
};
|
|
260
|
+
const allPass = Object.values(boundaryVerdict).every(Boolean) && Object.values(steadyVerdict).every(Boolean);
|
|
261
|
+
if (!allPass) throw new Error(`boundary/steady assertion failed: ${JSON.stringify({ boundaryVerdict, steadyVerdict })}`);
|
|
262
|
+
console.log('R2 gap boundary PASS');
|
|
263
|
+
process.exitCode = 0;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
evidence.verdicts = { failed: String((error && error.stack) || error) };
|
|
266
|
+
try {
|
|
267
|
+
if (browser) {
|
|
268
|
+
evidence.debugDom = JSON.parse(await browser.evaluate(`JSON.stringify({ dialogOpen: document.getElementById('logsDialog')?.open, entries: document.querySelectorAll('#logsContent .log-entry').length })`));
|
|
269
|
+
evidence.debugConsoleErrors = browser.consoleErrors;
|
|
270
|
+
}
|
|
271
|
+
} catch {}
|
|
272
|
+
console.error(`R2 gap FAIL: ${error.message}`);
|
|
273
|
+
process.exitCode = 1;
|
|
274
|
+
} finally {
|
|
275
|
+
evidence.finishedAt = now();
|
|
276
|
+
evidence.proxyEventLog = proxy.events.map(e => ({ seq: e.seq, at: e.at, kind: e.kind, type: e.type || null, route: e.route || null, url: e.url || null, status: e.status || null, held: e.held || null, event: e.event || null }));
|
|
277
|
+
fs.mkdirSync(ARTIFACTS, { recursive: true });
|
|
278
|
+
fs.writeFileSync(path.join(ARTIFACTS, 'gap-evidence.json'), JSON.stringify(evidence, null, 2) + '\n');
|
|
279
|
+
try { await browser?.close(); } finally { try { await proxy.close(); } finally { try { await server.cleanup(); } finally { fixture.cleanup(); } } }
|
|
280
|
+
lane.cleanup();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const hardTimer = setTimeout(() => {
|
|
285
|
+
try {
|
|
286
|
+
fs.mkdirSync(ARTIFACTS, { recursive: true });
|
|
287
|
+
fs.writeFileSync(path.join(ARTIFACTS, 'hard-timeout.json'), JSON.stringify({ at: now(), run }, null, 2) + '\n');
|
|
288
|
+
} catch {}
|
|
289
|
+
console.error('gap test HARD TIMEOUT');
|
|
290
|
+
process.exit(2);
|
|
291
|
+
}, HARD_TIMEOUT_MS);
|
|
292
|
+
hardTimer.unref();
|
|
293
|
+
|
|
294
|
+
main();
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// P-LOG-SAFETY-R2 normal hook path regression (lane-exclusive prefix
|
|
3
|
+
// parallel-logging-fix-r2-*).
|
|
4
|
+
//
|
|
5
|
+
// Complements the crash red/green probe: with the floating-rejection fix in
|
|
6
|
+
// place, the NORMAL legacy hook path must keep working end to end — two
|
|
7
|
+
// sequential real commits in a real registered Git fixture, two hook
|
|
8
|
+
// events, both processed to background.operation_completed by the existing
|
|
9
|
+
// clearly-labelled offline analyzer stand-in (KB_AUTOMATION_FAKE_CLAUDE=1,
|
|
10
|
+
// RuntimeKnowledgeAnalyzer.fakeResult), knowledge promoted into the
|
|
11
|
+
// project's knowledge root, lastAnalyzedCommit advancing, server healthy
|
|
12
|
+
// throughout, and an orderly control-channel shutdown.
|
|
13
|
+
//
|
|
14
|
+
// This is not a model-quality acceptance; the stand-in is marked as such in
|
|
15
|
+
// every record it writes ("isolated automation test adapter").
|
|
16
|
+
//
|
|
17
|
+
// KB_EMBEDDING_FAKE=1 is the product's own mock embedder (server-app.js
|
|
18
|
+
// createEmbedder), the same stand-in the other background-path product tests
|
|
19
|
+
// use. Without it the isolated instance has a cold model cache and the real
|
|
20
|
+
// embedding path tries to fetch the ONNX model from the network, which stalls
|
|
21
|
+
// the background operation past the run-all budget.
|
|
22
|
+
//
|
|
23
|
+
// P-LINUX-CI-CLOSEOUT-R7 corrections: (1) a health-timeout failure now prints
|
|
24
|
+
// the server's own exit cause, its captured stdout/stderr tails, and whether
|
|
25
|
+
// the port ever answered — /api/health is a synchronous responder behind the
|
|
26
|
+
// HTTP listener, so a 30s timeout means initializeRuntime never reached
|
|
27
|
+
// listen, and the stderr tail is the only way the CI log can name the hanging
|
|
28
|
+
// await; (2) the failure path force-kills a wedged server child — a surviving
|
|
29
|
+
// child process handle keeps this process alive (the linux22 CI shape: FAIL
|
|
30
|
+
// printed at 30s, hang until the 80s hard timer); (3) the hard timer dumps
|
|
31
|
+
// the captured server IO before exit(2). The 30s health budget is unchanged.
|
|
32
|
+
|
|
33
|
+
const fs = require('node:fs');
|
|
34
|
+
const path = require('node:path');
|
|
35
|
+
const { spawnSync } = require('node:child_process');
|
|
36
|
+
|
|
37
|
+
// Portable isolation (P-QA-LOG-INTEGRATION-5025): the temp root comes from
|
|
38
|
+
// PK_TEST_TEMP_ROOT or os.tmpdir() and must live outside the source tree;
|
|
39
|
+
// HOME/APPDATA/LOCALAPPDATA/TEMP/CODEX_HOME/CLAUDE_CONFIG_DIR and the dual
|
|
40
|
+
// product data roots are redirected before any other module is loaded.
|
|
41
|
+
const { installLoggingTestEnv, resolveEvidenceDir } = require('./helpers/parallel-logging-integration-5025-env');
|
|
42
|
+
const lane = installLoggingTestEnv({ caseId: 'LOGFIXR2-NORMALHOOK', prefix: 'logfixr2-normal' });
|
|
43
|
+
const laneTmp = lane.laneTmp;
|
|
44
|
+
|
|
45
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
46
|
+
const freePort = require('./helpers/free-port');
|
|
47
|
+
const { SettingsStore } = require('../lib/settings-store');
|
|
48
|
+
const { createLoggingAcceptanceFixture } = require('./helpers/parallel-logging-acceptance-fixture');
|
|
49
|
+
|
|
50
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
51
|
+
const ARTIFACTS = resolveEvidenceDir(path.resolve(ROOT, 'qa-autopilot/artifacts/parallel-20261023/logging-fix-r2/evidence/normalhook'));
|
|
52
|
+
|
|
53
|
+
const PROJECT_A = 'project-logfixr2-normal';
|
|
54
|
+
// Below the run-all per-test budget (PK_TEST_TIMEOUT_MS, default 90s) so a
|
|
55
|
+
// stall still lets this entry write its own evidence before the runner kills
|
|
56
|
+
// the child.
|
|
57
|
+
const HARD_TIMEOUT_MS = 80000;
|
|
58
|
+
|
|
59
|
+
// Module scope so the hard-timeout timer can kill the spawned server and dump
|
|
60
|
+
// its IO before exit(2) (see the R7 note in the header).
|
|
61
|
+
let server = null;
|
|
62
|
+
let exitInfo = null;
|
|
63
|
+
let serverStdout = '';
|
|
64
|
+
let serverStderr = '';
|
|
65
|
+
|
|
66
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
67
|
+
const now = () => new Date().toISOString();
|
|
68
|
+
|
|
69
|
+
const GIT_ENV = {
|
|
70
|
+
...process.env,
|
|
71
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
72
|
+
GIT_CONFIG_GLOBAL: path.join(laneTmp, 'non-existent-git-config'),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function git(repoPath, args) {
|
|
76
|
+
const res = spawnSync('git', ['-C', repoPath, ...args], { encoding: 'utf8', env: GIT_ENV });
|
|
77
|
+
if (res.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${res.stderr}`);
|
|
78
|
+
return res.stdout.trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function addCommit(repoPath, name, content) {
|
|
82
|
+
fs.writeFileSync(path.join(repoPath, name), content);
|
|
83
|
+
git(repoPath, ['add', '.']);
|
|
84
|
+
git(repoPath, ['commit', '-q', '-m', `LOGFIXR2 normal ${name}`]);
|
|
85
|
+
return git(repoPath, ['rev-parse', 'HEAD']);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function allLogRecords(dataDir) {
|
|
89
|
+
const records = [];
|
|
90
|
+
const rootDir = path.join(dataDir, 'logs');
|
|
91
|
+
if (!fs.existsSync(rootDir)) return records;
|
|
92
|
+
const walk = dir => {
|
|
93
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
94
|
+
const full = path.join(dir, entry.name);
|
|
95
|
+
if (entry.isDirectory()) walk(full);
|
|
96
|
+
else if (entry.name.endsWith('.jsonl')) {
|
|
97
|
+
for (const line of fs.readFileSync(full, 'utf8').split('\n')) {
|
|
98
|
+
if (line.trim()) { try { records.push(JSON.parse(line)); } catch {} }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
walk(rootDir);
|
|
104
|
+
return records;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function waitFor(predicate, label, timeoutMs) {
|
|
108
|
+
const deadline = Date.now() + timeoutMs;
|
|
109
|
+
let last;
|
|
110
|
+
while (Date.now() < deadline) {
|
|
111
|
+
last = await predicate();
|
|
112
|
+
if (last) return last;
|
|
113
|
+
await sleep(300);
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`timeout waiting for ${label}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function main() {
|
|
119
|
+
fs.mkdirSync(ARTIFACTS, { recursive: true });
|
|
120
|
+
const evidence = { caseId: 'R2-NORMALHOOK', startedAt: now(), phases: {} };
|
|
121
|
+
|
|
122
|
+
const fixture = await createLoggingAcceptanceFixture({
|
|
123
|
+
caseId: 'logfixr2normal',
|
|
124
|
+
projects: [{ projectId: PROJECT_A, displayName: 'LOGFIXR2正常路径' }],
|
|
125
|
+
});
|
|
126
|
+
await fixture.logger.close();
|
|
127
|
+
// The reconciler resolves a usable AI profile before the (fake) claim runs;
|
|
128
|
+
// give the isolated settings one clearly-labelled offline profile.
|
|
129
|
+
const settingsStore = new SettingsStore({ layout: fixture.layout });
|
|
130
|
+
await settingsStore.updatePatch({ ai: { profiles: [{ id: 'profile-logfixr2-offline', name: 'LOGFIXR2 离线替身', enabled: true, vendor: 'offline-standin' }] } });
|
|
131
|
+
const repoA = fixture.projects[0].repo.path;
|
|
132
|
+
|
|
133
|
+
// Two additional real commits in the real registered fixture repository.
|
|
134
|
+
const sha2 = addCommit(repoA, 'notes-two.md', '# second\n\nLOGFIXR2 normal path commit 2.\n');
|
|
135
|
+
const sha3 = addCommit(repoA, 'notes-three.md', '# third\n\nLOGFIXR2 normal path commit 3.\n');
|
|
136
|
+
evidence.commits = { sha2, sha3 };
|
|
137
|
+
|
|
138
|
+
const port = await freePort();
|
|
139
|
+
const base = `http://127.0.0.1:${port}`;
|
|
140
|
+
server = spawnServer({
|
|
141
|
+
root: ROOT,
|
|
142
|
+
port,
|
|
143
|
+
dataDir: fixture.dataDir,
|
|
144
|
+
tag: 'logfixr2normal',
|
|
145
|
+
extraEnv: { KB_AUTOMATION_FAKE_CLAUDE: '1', KB_EMBEDDING_FAKE: '1' },
|
|
146
|
+
});
|
|
147
|
+
server.child.once('exit', (code, signal) => { exitInfo = { code, signal, at: now() }; });
|
|
148
|
+
server.child.stdout.on('data', chunk => { serverStdout += chunk; });
|
|
149
|
+
server.child.stderr.on('data', chunk => { serverStderr += chunk; });
|
|
150
|
+
|
|
151
|
+
const postHook = async (head, operationId) => {
|
|
152
|
+
const res = await fetch(`${base}/api/hooks/post-commit`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'content-type': 'application/json' },
|
|
155
|
+
body: JSON.stringify({ schema: 'hook-event/v2', projectId: PROJECT_A, repoRoot: repoA, head, branch: 'main', operationId }),
|
|
156
|
+
});
|
|
157
|
+
const body = await res.json().catch(() => null);
|
|
158
|
+
return { status: res.status, body };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await waitFor(async () => {
|
|
163
|
+
try { return (await fetch(`${base}/api/health`)).ok; } catch { return false; }
|
|
164
|
+
}, 'isolated server healthy', 30000);
|
|
165
|
+
evidence.server = { pid: server.child.pid, port };
|
|
166
|
+
|
|
167
|
+
// Queue semantics: post BOTH hook events back to back (the second while
|
|
168
|
+
// the first may still be running), then require both to settle.
|
|
169
|
+
const op1 = 'op-logfixr2-normal-0001';
|
|
170
|
+
const op2 = 'op-logfixr2-normal-0002';
|
|
171
|
+
const ack1 = await postHook(sha2, op1);
|
|
172
|
+
const ack2 = await postHook(sha3, op2);
|
|
173
|
+
evidence.hookAcks = { ack1, ack2 };
|
|
174
|
+
if (ack1.status !== 202 || ack2.status !== 202 || !ack1.body?.accepted || !ack2.body?.accepted) {
|
|
175
|
+
throw new Error(`hook acks not 202/accepted: ${JSON.stringify({ ack1, ack2 })}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const settled = await waitFor(() => {
|
|
179
|
+
const records = allLogRecords(fixture.dataDir);
|
|
180
|
+
const done = op => records.some(r => r.event === 'background.operation_completed' && r.operationId === op);
|
|
181
|
+
const failed = records.filter(r => r.event === 'background.operation_failed');
|
|
182
|
+
if (failed.length) return { premature: true, failed: failed.map(r => ({ operationId: r.operationId, code: r.error?.code, message: r.error?.message })) };
|
|
183
|
+
return done(op1) && done(op2);
|
|
184
|
+
}, 'both hook operations completed', 60000);
|
|
185
|
+
if (settled && settled.premature) throw new Error(`background failure on the normal path: ${JSON.stringify(settled.failed)}`);
|
|
186
|
+
|
|
187
|
+
const records = allLogRecords(fixture.dataDir);
|
|
188
|
+
evidence.phases.background = {
|
|
189
|
+
completed: records.filter(r => r.event === 'background.operation_completed').map(r => ({ operationId: r.operationId, ts: r.ts })),
|
|
190
|
+
failed: records.filter(r => r.event === 'background.operation_failed').map(r => r.operationId),
|
|
191
|
+
accepted: records.filter(r => r.event === 'hook.event_accepted').map(r => ({ operationId: r.operationId, ts: r.ts })),
|
|
192
|
+
promotionEvents: records.filter(r => r.event.startsWith('knowledge.promotion') || r.event.startsWith('reconcile.')).map(r => ({ event: r.event, operationId: r.operationId, commitSha: r.commitSha, error: r.error ? { code: r.error.code, message: r.error.message } : undefined, context: r.context, ts: r.ts })).slice(0, 20),
|
|
193
|
+
};
|
|
194
|
+
// Diagnostic tree of the isolated data dir (depth 3) so file destinations
|
|
195
|
+
// are auditable in evidence.
|
|
196
|
+
const treeOf = (dir, depth) => {
|
|
197
|
+
const out = [];
|
|
198
|
+
if (depth < 0 || !fs.existsSync(dir)) return out;
|
|
199
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
200
|
+
out.push(path.relative(fixture.dataDir, path.join(dir, entry.name)) + (entry.isDirectory() ? '/' : ''));
|
|
201
|
+
if (entry.isDirectory() && depth > 0) out.push(...treeOf(path.join(dir, entry.name), depth - 1));
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
};
|
|
205
|
+
evidence.phases.dataDirTree = treeOf(fixture.dataDir, 3).slice(0, 200);
|
|
206
|
+
if (evidence.phases.background.failed.length) throw new Error('no operation_failed expected on the all-valid normal path');
|
|
207
|
+
|
|
208
|
+
// Knowledge promoted from the labelled offline stand-in: entries under the
|
|
209
|
+
// project knowledge root naming the isolated automation test adapter.
|
|
210
|
+
const knowledgeRoot = fixture.projects[0].knowledgePath;
|
|
211
|
+
const mdFiles = [];
|
|
212
|
+
const walk = (dir) => {
|
|
213
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
214
|
+
const full = path.join(dir, entry.name);
|
|
215
|
+
if (entry.isDirectory()) walk(full);
|
|
216
|
+
else if (entry.name.endsWith('.md')) mdFiles.push({ file: path.relative(knowledgeRoot, full), content: fs.readFileSync(full, 'utf8') });
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
walk(knowledgeRoot);
|
|
220
|
+
evidence.phases.knowledge = {
|
|
221
|
+
entries: mdFiles.map(f => ({ file: f.file, adapterLabelled: f.content.includes('isolated automation test adapter') })),
|
|
222
|
+
};
|
|
223
|
+
const promotedCount = mdFiles.filter(f => f.content.includes('isolated automation test adapter')).length;
|
|
224
|
+
if (promotedCount < 2) throw new Error(`expected >=2 promoted knowledge entries from the two commits, got ${promotedCount}`);
|
|
225
|
+
|
|
226
|
+
// Health throughout + no premature exit.
|
|
227
|
+
const health = await fetch(`${base}/api/health`);
|
|
228
|
+
evidence.phases.healthAfterHooks = { status: health.status, exitInfo };
|
|
229
|
+
if (!health.ok || exitInfo !== null) throw new Error(`server unhealthy or exited during normal processing: ${JSON.stringify({ status: health.status, exitInfo })}`);
|
|
230
|
+
|
|
231
|
+
// Disk truth BEFORE shutdown (spawnServer.cleanup removes the data dir):
|
|
232
|
+
// lastAnalyzedCommit advanced to the last commit.
|
|
233
|
+
const stateFile = path.join(fixture.dataDir, 'projects', PROJECT_A, 'state.json');
|
|
234
|
+
const readState = p => JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
235
|
+
let state;
|
|
236
|
+
try { state = readState(stateFile); } catch {
|
|
237
|
+
const candidates = [];
|
|
238
|
+
const projDir = path.join(fixture.dataDir, 'projects');
|
|
239
|
+
for (const name of fs.readdirSync(projDir)) {
|
|
240
|
+
const candidate = path.join(projDir, name, 'state.json');
|
|
241
|
+
if (fs.existsSync(candidate) && readState(candidate).lastAnalyzedCommit) candidates.push(readState(candidate));
|
|
242
|
+
}
|
|
243
|
+
state = candidates[0];
|
|
244
|
+
}
|
|
245
|
+
evidence.phases.projectState = { lastAnalyzedCommit: state?.lastAnalyzedCommit, expected: sha3 };
|
|
246
|
+
if (state?.lastAnalyzedCommit !== sha3) throw new Error(`lastAnalyzedCommit did not advance to ${sha3}: ${state?.lastAnalyzedCommit}`);
|
|
247
|
+
|
|
248
|
+
// Orderly shutdown via the verified control channel (drain path).
|
|
249
|
+
const stopStarted = Date.now();
|
|
250
|
+
await server.cleanup();
|
|
251
|
+
evidence.phases.shutdown = { stopStartedAt: new Date(stopStarted).toISOString(), exitInfo, stoppedWithinMs: Date.now() - stopStarted };
|
|
252
|
+
|
|
253
|
+
if (exitInfo === null) throw new Error('server did not exit after control-channel stop');
|
|
254
|
+
evidence.verdict = 'PASS';
|
|
255
|
+
console.log('R2 normalhook PASS');
|
|
256
|
+
process.exitCode = 0;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
evidence.verdict = { failed: String((error && error.stack) || error) };
|
|
259
|
+
// Failure-path visibility: the CI console is the only artifact the runner
|
|
260
|
+
// keeps. A health timeout must name the server's own exit cause, its
|
|
261
|
+
// captured stdout/stderr tails, and whether the port ever answered.
|
|
262
|
+
let probeAfterFailure = null;
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(1500) });
|
|
265
|
+
probeAfterFailure = { answered: true, ok: response.ok, status: response.status };
|
|
266
|
+
} catch (probeError) {
|
|
267
|
+
probeAfterFailure = { answered: false, code: (probeError && probeError.cause && (probeError.cause.code || probeError.cause.message)) || String(probeError && probeError.message || probeError) };
|
|
268
|
+
}
|
|
269
|
+
evidence.debugFailureProbe = probeAfterFailure;
|
|
270
|
+
// The child's OWN structured logs hold its account of its death: on a
|
|
271
|
+
// startup rejection stop() logs server.shutdown_started with the reason
|
|
272
|
+
// (CI 40075242420: 'control-startup-failed' from an unbindable POSIX
|
|
273
|
+
// control socket) before the error reaches the bootstrap catch. Bounded
|
|
274
|
+
// metadata only — level, event, reason, error code, first stack frame's
|
|
275
|
+
// file — never raw context or transcripts.
|
|
276
|
+
let ownStartupRecords = null;
|
|
277
|
+
try {
|
|
278
|
+
ownStartupRecords = allLogRecords(fixture.dataDir)
|
|
279
|
+
.filter(r => ['fatal', 'error', 'warn'].includes(r.level) || String(r.event || '').startsWith('server.'))
|
|
280
|
+
.slice(-5)
|
|
281
|
+
.map(r => ({
|
|
282
|
+
level: r.level || null,
|
|
283
|
+
event: r.event || null,
|
|
284
|
+
reason: (r.context && r.context.reason) || null,
|
|
285
|
+
code: (r.error && r.error.code) || null,
|
|
286
|
+
stackFile: (String((r.error && r.error.stack) || '').split('\n')[1] || '').match(/([^\\/]+)\.[a-z]+:/i)?.[0] || null,
|
|
287
|
+
}));
|
|
288
|
+
} catch {}
|
|
289
|
+
evidence.debugOwnStartupRecords = ownStartupRecords;
|
|
290
|
+
let posixControlSocket = 'named-pipe (win32)';
|
|
291
|
+
try {
|
|
292
|
+
const { controlPath } = require('../lib/runtime-control');
|
|
293
|
+
if (process.platform !== 'win32') posixControlSocket = `${controlPath(fixture.dataDir).length} chars (POSIX budget 103)`;
|
|
294
|
+
} catch {}
|
|
295
|
+
console.error(`R2 normalhook FAIL: ${error.message}`);
|
|
296
|
+
console.error(`R2 normalhook server state: ${JSON.stringify({ port, pid: server.child.pid, exitInfo, probeAfterFailure, posixControlSocket })}`);
|
|
297
|
+
console.error(`R2 normalhook own structured-log tail (bounded): ${JSON.stringify(ownStartupRecords)}`);
|
|
298
|
+
console.error(`R2 normalhook server stderr tail:\n${serverStderr.slice(-1600)}`);
|
|
299
|
+
console.error(`R2 normalhook server stdout tail:\n${serverStdout.slice(-1600)}`);
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
} finally {
|
|
302
|
+
evidence.debugServerIo = { exitInfo, stdoutTail: serverStdout.slice(-4000), stderrTail: serverStderr.slice(-4000) };
|
|
303
|
+
evidence.finishedAt = now();
|
|
304
|
+
fs.mkdirSync(ARTIFACTS, { recursive: true });
|
|
305
|
+
fs.writeFileSync(path.join(ARTIFACTS, 'normalhook-evidence.json'), JSON.stringify(evidence, null, 2) + '\n');
|
|
306
|
+
try {
|
|
307
|
+
await server.cleanup();
|
|
308
|
+
// cleanup() stops the control channel and kills, but a child wedged
|
|
309
|
+
// mid-startup can survive the signal — and its process handle keeps
|
|
310
|
+
// THIS process alive (the linux22 CI shape: FAIL at 30s, hang to the
|
|
311
|
+
// 80s hard timer). The exit decision belongs to this test, not to the
|
|
312
|
+
// child.
|
|
313
|
+
if (exitInfo === null) { try { server.child.kill('SIGKILL'); } catch {} }
|
|
314
|
+
} finally { fixture.cleanup(); }
|
|
315
|
+
lane.cleanup();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const hardTimer = setTimeout(() => {
|
|
320
|
+
try {
|
|
321
|
+
fs.mkdirSync(ARTIFACTS, { recursive: true });
|
|
322
|
+
fs.writeFileSync(path.join(ARTIFACTS, 'hard-timeout.json'), JSON.stringify({ at: now(), exitInfo, stdoutTail: serverStdout.slice(-1600), stderrTail: serverStderr.slice(-1600) }, null, 2) + '\n');
|
|
323
|
+
} catch {}
|
|
324
|
+
try { if (server && exitInfo === null) server.child.kill('SIGKILL'); } catch {}
|
|
325
|
+
console.error('normalhook HARD TIMEOUT');
|
|
326
|
+
process.exit(2);
|
|
327
|
+
}, HARD_TIMEOUT_MS);
|
|
328
|
+
hardTimer.unref();
|
|
329
|
+
|
|
330
|
+
main();
|