leerness 1.36.299 → 1.36.301
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/CHANGELOG.md +56 -0
- package/CONTRIBUTING.md +2 -0
- package/README.md +4 -4
- package/bin/leerness.js +17 -10
- package/docs/runtime-compatibility-api.md +8 -4
- package/lib/runtime-layout.js +7 -3
- package/lib/workspace-dir.js +134 -35
- package/package.json +9 -5
- package/scripts/ci-shard-probe.js +343 -0
- package/scripts/ci-shard-weights.json +590 -0
- package/scripts/ci-shard.js +394 -0
- package/scripts/e2e-child-diagnostics-probe.js +5 -3
- package/scripts/e2e-concurrency.js +24 -10
- package/scripts/e2e.js +843 -822
- package/scripts/lazy-auto-track-probe.js +87 -0
- package/scripts/lock-probe.js +117 -10
- package/scripts/race-overlap-probe.js +148 -0
- package/scripts/race-overlap.js +28 -0
- package/scripts/release-runtime-probe.js +9 -6
- package/scripts/workspace-migration-heartbeat-probe.js +334 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.301",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"LICENSE"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication && npm run test:ui-review-target && npm run test:ui-review-authorization && npm run test:ui-review-authority-host && npm run test:ui-review-authority-floor && npm run test:ui-review-proof-use && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:agent-chat-stream && npm run test:encoding-diagnostics && npm run test:managed-text && npm run test:plan-text && npm run test:session-close-text && npm run test:brief-text && npm run test:generation-text && npm run test:install-text && npm run test:authoring-language && npm run test:context-reading && npm run test:rule-text && npm run test:mode-text && npm run test:handoff-text && npm run test:audit-text && npm run test:audit-readme-text && npm run test:audit-env-text && npm run test:feature-graph-text && npm run test:env-sync-text && npm run test:teams-text && npm run test:reminder-text && npm run test:handoff-history-text && npm run test:env-snapshot-text && npm run test:context-current && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && node ./scripts/review-control-probe.js && npm run test:ui-review-run && npm run test:rule-verification",
|
|
52
|
-
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication-smoke && npm run test:ui-review-target-smoke && npm run test:ui-review-authorization-smoke && npm run test:ui-review-authority-host-smoke && npm run test:ui-review-authority-floor-smoke && npm run test:ui-review-proof-use-smoke && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:ui-review-run && npm run test:rule-verification",
|
|
53
|
-
"test:commands": "npm run test:hooks && npm run test:e2e-observation && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/command-flags-probe.js && node ./scripts/dead-flags-probe.js && node ./scripts/free-text-args-probe.js && node ./scripts/title-args-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/claim-sweep-git-probe.js && node ./scripts/claim-runtime-cache-probe.js && node ./scripts/runtime-cost-probe.js && node ./scripts/secret-scan-scope-probe.js && node ./scripts/handoff-preservation-probe.js && node ./scripts/next-action-suggestion-probe.js && node ./scripts/api-skill-match-probe.js && node ./scripts/crud-check-probe.js && node ./scripts/claims-baseline-probe.js && node ./scripts/claims-baseline-concurrency-probe.js && node ./scripts/e2e-command-surface.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-request-labels-probe.js && node ./scripts/review-request-signals-probe.js && node ./scripts/store-values-probe.js && node ./scripts/review-snapshot-transport-probe.js && node ./scripts/bugfix-areas-probe.js && node ./scripts/done-status-probe.js && node ./scripts/fix-evidence-language-probe.js && node ./scripts/decision-fence-offset-probe.js && node ./scripts/task-sync-roundtrip-probe.js && node ./scripts/tracker-write-language-probe.js && node ./scripts/write-path-language-probe.js && node ./scripts/write-primitive-language-probe.js && node ./scripts/dry-run-refusal-probe.js",
|
|
51
|
+
"test": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-migration-heartbeat-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/race-overlap-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/ci-shard-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication && npm run test:ui-review-target && npm run test:ui-review-authorization && npm run test:ui-review-authority-host && npm run test:ui-review-authority-floor && npm run test:ui-review-proof-use && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:agent-chat-stream && npm run test:encoding-diagnostics && npm run test:managed-text && npm run test:plan-text && npm run test:session-close-text && npm run test:brief-text && npm run test:generation-text && npm run test:install-text && npm run test:authoring-language && npm run test:context-reading && npm run test:rule-text && npm run test:mode-text && npm run test:handoff-text && npm run test:audit-text && npm run test:audit-readme-text && npm run test:audit-env-text && npm run test:feature-graph-text && npm run test:env-sync-text && npm run test:teams-text && npm run test:reminder-text && npm run test:handoff-history-text && npm run test:env-snapshot-text && npm run test:context-current && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && npm run test:commands && npm run test:installed && node ./scripts/e2e.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && node ./scripts/review-control-probe.js && npm run test:ui-review-run && npm run test:rule-verification",
|
|
52
|
+
"test:core": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-migration-heartbeat-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/race-overlap-probe.js && node ./scripts/e2e-repo-guard.js && node ./scripts/e2e-lived-in-harness.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/verify-code-cross-runtime-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/ci-shard-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:github-login && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-publication-smoke && npm run test:ui-review-target-smoke && npm run test:ui-review-authorization-smoke && npm run test:ui-review-authority-host-smoke && npm run test:ui-review-authority-floor-smoke && npm run test:ui-review-proof-use-smoke && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:review-feedback && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e-core.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/handoff-readonly-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:ui-review-run && npm run test:rule-verification",
|
|
53
|
+
"test:commands": "npm run test:hooks && npm run test:e2e-observation && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/command-flags-probe.js && node ./scripts/dead-flags-probe.js && node ./scripts/free-text-args-probe.js && node ./scripts/title-args-probe.js && node ./scripts/false-claim-probe.js && node ./scripts/claim-sweep-git-probe.js && node ./scripts/claim-runtime-cache-probe.js && node ./scripts/runtime-cost-probe.js && node ./scripts/secret-scan-scope-probe.js && node ./scripts/handoff-preservation-probe.js && node ./scripts/next-action-suggestion-probe.js && node ./scripts/api-skill-match-probe.js && node ./scripts/crud-check-probe.js && node ./scripts/claims-baseline-probe.js && node ./scripts/claims-baseline-concurrency-probe.js && node ./scripts/e2e-command-surface.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-request-labels-probe.js && node ./scripts/review-request-signals-probe.js && node ./scripts/store-values-probe.js && node ./scripts/review-snapshot-transport-probe.js && node ./scripts/bugfix-areas-probe.js && node ./scripts/done-status-probe.js && node ./scripts/fix-evidence-language-probe.js && node ./scripts/decision-fence-offset-probe.js && node ./scripts/task-sync-roundtrip-probe.js && node ./scripts/tracker-write-language-probe.js && node ./scripts/write-path-language-probe.js && node ./scripts/write-primitive-language-probe.js && node ./scripts/dry-run-refusal-probe.js && node ./scripts/lazy-auto-track-probe.js",
|
|
54
54
|
"test:e2e-observation": "node ./scripts/e2e-temp-scope-probe.js && node ./scripts/e2e-child-diagnostics-probe.js",
|
|
55
55
|
"test:next-actions": "node ./scripts/next-action-suggestion-probe.js && node ./scripts/api-skill-match-probe.js",
|
|
56
56
|
"test:installed": "npm run test:installed-lifecycle && node ./scripts/installed-cleanroom-probe.js",
|
|
57
57
|
"test:installed-lifecycle": "node ./scripts/installed-handoff-lifecycle-probe.js",
|
|
58
58
|
"test:handoff": "node ./scripts/handoff-readonly-probe.js",
|
|
59
|
-
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/e2e-repo-guard.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:rule-verification",
|
|
59
|
+
"test:fast": "node ./scripts/lint.js && node ./scripts/workspace-dir-source-ratchet.js && npm run test:state-scopes && npm run test:runtime-compatibility && node ./scripts/workspace-dir-migration-probe.js && node ./scripts/workspace-migration-heartbeat-probe.js && node ./scripts/workspace-dir-lock-order-probe.js && node ./scripts/lock-probe.js && node ./scripts/race-overlap-probe.js && node ./scripts/e2e-repo-guard.js && npm run test:lock-heartbeat && npm run test:temp-cleanup && npm run test:path-setup && npm run test:skill-archive && npm run test:memory-arena-cleanup && node ./scripts/platform-smoke.js && node ./scripts/parent-detect-selftest-probe.js && node ./scripts/release-runtime-probe.js && node ./scripts/ci-shard-probe.js && node ./scripts/which-shim-probe.js && node ./scripts/i18n-priority-surface-probe.js && npm run test:harness-text && npm run test:test-plan && node ./scripts/i18n-next-cluster-probe.js && node ./scripts/handoff-i18n-probe.js && node ./scripts/mutation-integrity-probe.js && node ./scripts/memory-guard-order-probe.js && node ./scripts/memory-input-preflight-probe.js && node ./scripts/memory-utf8-admission-probe.js && node ./scripts/memory-checked-input-probe.js && node ./scripts/memory-input-unit-probe.js && node ./scripts/memory-restore-canonical-probe.js && node ./scripts/memory-archive-retention-probe.js && node ./scripts/memory-archive-check-probe.js && node ./scripts/memory-projection-retention-probe.js && node ./scripts/memory-language-probe.js && node ./scripts/memory-legacy-retention-probe.js && node ./scripts/memory-recovery-receipt-probe.js && node ./scripts/memory-output-unit-probe.js && node ./scripts/memory-output-posix-probe.js && node ./scripts/memory-output-publication-probe.js && node ./scripts/memory-output-parent-race-probe.js && node ./scripts/mcp-presence-probe.js && node ./scripts/provider-capacity-probe.js && npm run test:provider-response && npm run test:provider-dispatch && npm run test:provider-receipt && npm run test:provider-observation && npm run test:ui-review-recovery && node ./scripts/file-lease-probe.js && node ./scripts/role-agent-schema-probe.js && node ./scripts/role-store-loader-probe.js && node ./scripts/role-fallback-probe.js && npm run test:agent-bench-output && npm run test:ledger-sync && npm run test:state-lock && npm run test:history-retention && node ./scripts/false-claim-probe.js && node ./scripts/smoke.js && node ./scripts/rule-applicability-probe.js && node ./scripts/review-checkpoint-probe.js && node ./scripts/review-checkpoint-transport-probe.js && node ./scripts/review-snapshot-probe.js && node ./scripts/review-snapshot-transport-probe.js && npm run test:memory-migration && npm run test:memory-items && npm run test:memory-budget && npm run test:memory-footprint && npm run test:memory-write-budget && npm run test:memory-write-preparation && npm run test:memory-write-admission && npm run test:memory-command-policy && npm run test:memory-publication-policy && npm run test:memory-recovery-transfer && npm run test:ui-boundary && npm run test:rule-verification",
|
|
60
60
|
"test:false-claims": "node ./scripts/false-claim-probe.js",
|
|
61
61
|
"test:claim-sweep": "node ./scripts/claim-sweep-git-probe.js",
|
|
62
62
|
"test:claim-runtime": "node ./scripts/claim-runtime-cache-probe.js",
|
|
@@ -74,6 +74,10 @@
|
|
|
74
74
|
"test:tracker-write-language": "node ./scripts/tracker-write-language-probe.js",
|
|
75
75
|
"test:write-path-language": "node ./scripts/write-path-language-probe.js",
|
|
76
76
|
"test:write-primitive-language": "node ./scripts/write-primitive-language-probe.js",
|
|
77
|
+
"test:migration-heartbeat": "node ./scripts/workspace-migration-heartbeat-probe.js",
|
|
78
|
+
"test:ci-shard": "node ./scripts/ci-shard-probe.js",
|
|
79
|
+
"test:race-overlap": "node ./scripts/race-overlap-probe.js",
|
|
80
|
+
"test:lazy-auto-track": "node ./scripts/lazy-auto-track-probe.js",
|
|
77
81
|
"test:dry-run-refusal": "node ./scripts/dry-run-refusal-probe.js",
|
|
78
82
|
"test:api-skill-match": "node ./scripts/api-skill-match-probe.js",
|
|
79
83
|
"test:crud-check": "node ./scripts/crud-check-probe.js",
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// T-0359: CI runs e2e.js cases and npm-script leaves in parallel shards (scripts/ci-shard.js). A shard that drops a
|
|
4
|
+
// unit fails nothing — the unit silently stops running — so this probe checks the parts that decide what runs:
|
|
5
|
+
// the assignment is a deterministic partition, every leaf of the real sharded scripts parses and lands in exactly
|
|
6
|
+
// one shard, the e2e guard parser and runtime helper catch unguarded, repeated, unreached and unknown cases, and
|
|
7
|
+
// the workflow lists every shard of every configuration. Each detector is also run on a broken input.
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const cs = require('./ci-shard');
|
|
13
|
+
|
|
14
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
15
|
+
const results = [];
|
|
16
|
+
const check = (name, fn) => {
|
|
17
|
+
try { fn(); results.push({ name, ok: true }); console.log(`PASS ${name}`); }
|
|
18
|
+
catch (e) { results.push({ name, ok: false }); console.log(`FAIL ${name}: ${e && e.message}`); }
|
|
19
|
+
};
|
|
20
|
+
// Deterministic pseudo-random weights (no Math.random: a failure must reproduce).
|
|
21
|
+
const prng = seed => () => { seed = (seed * 1103515245 + 12345) % 2147483648; return seed / 2147483648; };
|
|
22
|
+
const workflowText = fs.readFileSync(path.join(ROOT, '.github', 'workflows', 'ci.yml'), 'utf8');
|
|
23
|
+
const scripts = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).scripts;
|
|
24
|
+
const weights = cs.loadWeights();
|
|
25
|
+
const e2eSource = fs.readFileSync(path.join(__dirname, 'e2e.js'), 'utf8');
|
|
26
|
+
const arena = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ci-shard-probe-'));
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
check('parseShard accepts i/k with 1 <= i <= k and refuses the rest', () => {
|
|
30
|
+
assert.deepStrictEqual(cs.parseShard('3/4'), { index: 3, count: 4 });
|
|
31
|
+
assert.deepStrictEqual(cs.parseShard(' 1/1 '), { index: 1, count: 1 });
|
|
32
|
+
for (const bad of ['', '0/2', '3/2', '1/0', 'a/b', '1/2/3', '2', '-1/2', '1.5/2', undefined, null]) {
|
|
33
|
+
assert.throws(() => cs.parseShard(bad), /shard must be/, `accepted ${bad}`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
check('assign is a deterministic partition with the greedy load bound', () => {
|
|
38
|
+
const rand = prng(7);
|
|
39
|
+
for (let round = 0; round < 200; round++) {
|
|
40
|
+
const n = 1 + Math.floor(rand() * 60), k = 1 + Math.floor(rand() * 8);
|
|
41
|
+
const units = Array.from({ length: n }, (_, i) => ({ key: `u${i}`, weight: Math.floor(rand() * 1000) / 10 }));
|
|
42
|
+
const a = cs.assign(units, k), b = cs.assign(units.map(u => ({ ...u })), k);
|
|
43
|
+
assert.deepStrictEqual(a.shardOf, b.shardOf, 'same input, different assignment');
|
|
44
|
+
assert.strictEqual(a.shardOf.length, n);
|
|
45
|
+
assert(a.shardOf.every(s => Number.isInteger(s) && s >= 1 && s <= k), 'shard out of range');
|
|
46
|
+
const load = new Array(k).fill(0);
|
|
47
|
+
a.shardOf.forEach((s, i) => { load[s - 1] += units[i].weight; });
|
|
48
|
+
load.forEach((l, i) => assert(Math.abs(l - a.load[i]) < 1e-9, 'reported load differs'));
|
|
49
|
+
const sum = units.reduce((s, u) => s + u.weight, 0), max = Math.max(...units.map(u => u.weight));
|
|
50
|
+
assert(Math.max(...load) <= sum / k + max + 1e-9, 'greedy bound exceeded');
|
|
51
|
+
}
|
|
52
|
+
// Equal weights: source order, round robin from shard 1 (ties to the earlier unit and the lower shard).
|
|
53
|
+
const even = cs.assign(Array.from({ length: 7 }, (_, i) => ({ key: `e${i}`, weight: 5 })), 3).shardOf;
|
|
54
|
+
assert.deepStrictEqual(even, [1, 2, 3, 1, 2, 3, 1]);
|
|
55
|
+
for (const w of [NaN, -1, Infinity, '3', null]) assert.throws(() => cs.assign([{ key: 'x', weight: w }], 2), /bad weight/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
check('weights file has both tables; an unmeasured unit weighs the median', () => {
|
|
59
|
+
assert(Object.keys(weights.e2e).length > 0 && Object.keys(weights.npm).length > 0, 'empty weights table');
|
|
60
|
+
const units = cs.weigh(['a', 'b #2', 'zzz'], { a: 1, b: 10, c: 4 });
|
|
61
|
+
assert.deepStrictEqual(units.map(u => u.weight), [1, 10, 4]);
|
|
62
|
+
const before = { _about: 'x', e2e: { b: 2, a: 1 }, npm: { n: 3 } };
|
|
63
|
+
const { data, updated } = cs.withMeasured(before, 'e2e', [{ c: 4.04, 'a #2': 7 }, { b: 0 }]);
|
|
64
|
+
assert.strictEqual(updated, 3);
|
|
65
|
+
assert.deepStrictEqual(data, { _about: 'x', e2e: { a: 7, b: 0, c: 4 }, npm: { n: 3 } });
|
|
66
|
+
assert.deepStrictEqual(Object.keys(data.e2e), ['a', 'b', 'c'], 'table not sorted');
|
|
67
|
+
assert.deepStrictEqual(before.e2e, { b: 2, a: 1 }, 'input mutated');
|
|
68
|
+
assert.throws(() => cs.withMeasured(before, 'npm', [{ x: -1 }]), /bad seconds/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
check('npm leaves: nested expansion in order, refused shapes, cycles', () => {
|
|
72
|
+
const fixture = {
|
|
73
|
+
all: 'node a.js && npm run inner && node ./c.js --x=1',
|
|
74
|
+
inner: 'node b.js && npm run deeper',
|
|
75
|
+
deeper: 'node d.js selftest',
|
|
76
|
+
loop1: 'npm run loop2', loop2: 'npm run loop1',
|
|
77
|
+
};
|
|
78
|
+
assert.deepStrictEqual(cs.npmLeaves(fixture, 'all').map(l => l.text),
|
|
79
|
+
['node a.js', 'node b.js', 'node d.js selftest', 'node ./c.js --x=1']);
|
|
80
|
+
assert.throws(() => cs.npmLeaves(fixture, 'loop1'), /cycle: loop1 -> loop2 -> loop1$/);
|
|
81
|
+
assert.throws(() => cs.npmLeaves(fixture, 'missing'), /not found/);
|
|
82
|
+
// `npm run X` also runs preX and postX; the runner refuses a script with one, at any depth.
|
|
83
|
+
for (const [hooks, hook] of [[{ preinner: 'node pre.js' }, 'preinner'], [{ postdeeper: 'node post.js' }, 'postdeeper'],
|
|
84
|
+
[{ preall: 'node pre.js' }, 'preall'], [{ postall: 'node post.js' }, 'postall']]) {
|
|
85
|
+
assert.throws(() => cs.npmLeaves({ ...fixture, ...hooks }, 'all'), new RegExp(`lifecycle script ${hook}`), `hook ${hook} not refused`);
|
|
86
|
+
}
|
|
87
|
+
for (const step of ['echo hi', 'node a.js | tee x', 'cross-env A=1 node a.js', 'node "a b.js"', 'npm run all -- --y',
|
|
88
|
+
'node --inspect a.js', 'node', 'node a.js; node b.js', 'node a.js > out', 'A=1 node a.js', 'node $FILE', 'npm test']) {
|
|
89
|
+
assert.throws(() => cs.npmLeaves({ s: step }, 's'), /unsupported step|cycle/, `accepted: ${step}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// The sharded scripts are the ones the workflow names in its ci-shard.js run lines.
|
|
94
|
+
const jobs = cs.workflowJobs(workflowText);
|
|
95
|
+
const sharded = [];
|
|
96
|
+
for (const job of Object.values(jobs)) {
|
|
97
|
+
for (const run of job.runs) {
|
|
98
|
+
const m = /^node \.\/scripts\/ci-shard\.js npm (\S+) \$\{\{ matrix\.shard \}\}\/\$\{\{ matrix\.shards \}\}$/.exec(run);
|
|
99
|
+
if (m) sharded.push(m[1]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
check('every leaf of each sharded npm script runs in exactly one shard, in script order', () => {
|
|
104
|
+
assert(sharded.length >= 2, `expected the workflow to shard at least two npm scripts, found ${sharded.join(', ')}`);
|
|
105
|
+
for (const name of sharded) {
|
|
106
|
+
const full = cs.npmLeaves(scripts, name).map(l => l.text);
|
|
107
|
+
assert(full.length > 0, `${name} has no leaves`);
|
|
108
|
+
for (let k = 1; k <= 6; k++) {
|
|
109
|
+
const plan = cs.planNpm(scripts, name, k, weights);
|
|
110
|
+
const union = [];
|
|
111
|
+
for (let i = 1; i <= k; i++) union.push(...plan.leaves.filter(l => l.shard === i).map(l => l.key));
|
|
112
|
+
assert.deepStrictEqual([...union].sort(), cs.uniqueKeys(full).sort(), `${name} ${k} shards: leaves lost or repeated`);
|
|
113
|
+
assert.deepStrictEqual(plan.leaves.map(l => l.text), full, `${name}: plan changed the leaf order`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
check('runNpm runs only its shard, keeps going after a failure, and reports it', () => {
|
|
119
|
+
const dir = path.join(arena, 'npm');
|
|
120
|
+
fs.mkdirSync(dir);
|
|
121
|
+
for (const [name, code] of [['a', 0], ['b', 3], ['c', 0], ['d', 0]]) {
|
|
122
|
+
fs.writeFileSync(path.join(dir, `${name}.js`), `require('fs').writeFileSync(${JSON.stringify(path.join(dir, `${name}.ran`))}, '1'); process.exit(${code});\n`);
|
|
123
|
+
}
|
|
124
|
+
const fixture = { suite: 'node ./a.js && node ./b.js && npm run more', more: 'node ./c.js && node ./d.js' };
|
|
125
|
+
const w = { e2e: {}, npm: { 'node ./a.js': 4, 'node ./b.js': 3, 'node ./c.js': 2, 'node ./d.js': 1 } };
|
|
126
|
+
const lines = [];
|
|
127
|
+
const quiet = { root: dir, scripts: fixture, weights: w, stdio: 'ignore', env: { ...process.env, GITHUB_ACTIONS: '' }, log: l => lines.push(l) };
|
|
128
|
+
const one = cs.runNpm('suite', { index: 1, count: 1 }, quiet);
|
|
129
|
+
assert.strictEqual(one.ran, 4); assert.deepStrictEqual(one.failed, ['node ./b.js']);
|
|
130
|
+
assert(['a', 'b', 'c', 'd'].every(n => fs.existsSync(path.join(dir, `${n}.ran`))), 'a leaf after the failure did not run');
|
|
131
|
+
assert(lines.some(l => /FAIL .*node \.\/b\.js \(exit 3\)/.test(l)), 'failure line missing');
|
|
132
|
+
for (const n of ['a', 'b', 'c', 'd']) fs.unlinkSync(path.join(dir, `${n}.ran`));
|
|
133
|
+
const s1 = cs.runNpm('suite', { index: 1, count: 2 }, quiet), s2 = cs.runNpm('suite', { index: 2, count: 2 }, quiet);
|
|
134
|
+
const ran = [...s1.results, ...s2.results].map(r => r.key).sort();
|
|
135
|
+
assert.deepStrictEqual(ran, ['node ./a.js', 'node ./b.js', 'node ./c.js', 'node ./d.js']);
|
|
136
|
+
assert.deepStrictEqual(s1.results.map(r => r.key), ['node ./a.js', 'node ./d.js']);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const CREATE = names => `const e2eShard = require('./ci-shard').e2eShard(__filename, [${names.map(n => `'${n}'`).join(', ')}]);`;
|
|
140
|
+
const START = '// e2e-shard: cases start here';
|
|
141
|
+
|
|
142
|
+
check('e2e guard parser: the real file is clean; unguarded, repeated and malformed guards are reported', () => {
|
|
143
|
+
const real = cs.e2eCases(e2eSource);
|
|
144
|
+
assert.deepStrictEqual(real.problems, []);
|
|
145
|
+
assert(real.keys.length >= 400, `only ${real.keys.length} cases found`);
|
|
146
|
+
const bad = [
|
|
147
|
+
CREATE(['fx']), // 1
|
|
148
|
+
'if (setupNeeded) {', ' total++;', '}', // 2-4: preamble, above the marker: allowed
|
|
149
|
+
START, // 5
|
|
150
|
+
"if (e2eShard.take('a')) {", ' total++;', '}', // 6-8
|
|
151
|
+
'{ // accidentally unguarded case', '}', // 9-10: a block opened with a comment
|
|
152
|
+
"if (e2eShard.take('a')) {", '}', // 11-12: repeated key
|
|
153
|
+
'if (e2eShard.take("b")) {', '}', // 13-14: another guard form
|
|
154
|
+
"// if (e2eShard.take('in a comment')) {", // 15
|
|
155
|
+
' {', ' }', // 16-17: an indented block between cases
|
|
156
|
+
'if (flag) {', '}', // 18-19: a control-flow block
|
|
157
|
+
'(() => {', '})();', // 20-21: an immediately invoked function
|
|
158
|
+
'total++;', // 22: a counter outside a case
|
|
159
|
+
'run("smoke", []);', // 23: allowed between cases
|
|
160
|
+
' const x = `', '{', '`;', // 24-26: a column-0 "{" in a template literal
|
|
161
|
+
'setImmediate(() => {', ' total++; // e2e-shard: run-wide check', ' if (ok) {', ' }', // 27-30: run-wide: allowed
|
|
162
|
+
' total++;', // 31: a run-wide counter without its mark
|
|
163
|
+
'}); // end of the run', // 32: the closer, with a comment
|
|
164
|
+
'run("late", []);', // 33: code after the run-wide section
|
|
165
|
+
].join('\n');
|
|
166
|
+
const r = cs.e2eCases(bad);
|
|
167
|
+
assert.deepStrictEqual(r.keys, ['a']);
|
|
168
|
+
const expected = [
|
|
169
|
+
/line 9: a block outside a case/, /key "a" repeats/, /line 13: e2eShard\.take outside the guard form/,
|
|
170
|
+
/line 16: a block outside a case/, /line 18: a control-flow block outside a case/,
|
|
171
|
+
/line 20: an immediately invoked function outside a case/, /line 22: a case counter outside a case/,
|
|
172
|
+
// The parser reads lines, not JavaScript: a column-0 "{" inside a template literal is reported (a false alarm
|
|
173
|
+
// that fails loudly, never a dropped case).
|
|
174
|
+
/line 25: a block outside a case/, /line 31: a case counter in the run-wide section without/,
|
|
175
|
+
/line 33: code after the run-wide section/,
|
|
176
|
+
];
|
|
177
|
+
assert.strictEqual(r.problems.length, expected.length, r.problems.join(' | '));
|
|
178
|
+
for (const re of expected) assert(r.problems.some(p => re.test(p)), `missing ${re}: ${r.problems.join(' | ')}`);
|
|
179
|
+
// The reviewers' cases on the real file. Round 1: one guard replaced by a block opened with a comment. Round 2:
|
|
180
|
+
// the first guard replaced by `if (false) {`; the preamble then ended at the next guard and the case vanished.
|
|
181
|
+
// Other wrappers too: whatever wraps a case, its counter is outside a guard.
|
|
182
|
+
const first = "if (e2eShard.take('update --check (offline): 조회 실패를')) {", middle = "if (e2eShard.take('B(1.9.29)')) {";
|
|
183
|
+
for (const [anchor, wrap] of [[middle, '{ // accidentally unguarded case'], [first, 'if (false) {'],
|
|
184
|
+
[first, 'function never() {'], [middle, 'skip: {'], [middle, 'class Never { static {']]) {
|
|
185
|
+
assert.strictEqual(e2eSource.split(anchor).length, 2, 'anchor case missing: ' + anchor);
|
|
186
|
+
const broken = cs.e2eCases(e2eSource.replace(anchor, wrap));
|
|
187
|
+
assert.strictEqual(broken.keys.length, real.keys.length - 1);
|
|
188
|
+
assert(broken.problems.some(p => /outside a case/.test(p)), `${wrap}: ${broken.problems.join(' | ')}`);
|
|
189
|
+
}
|
|
190
|
+
// Round 3: a comment on the run-wide closer left the section open, and a case appended after it was exempt.
|
|
191
|
+
const closer = e2eSource.lastIndexOf('\n});');
|
|
192
|
+
assert(closer > 0 && /^\s*$/.test(e2eSource.slice(closer + 4)), 'run-wide closer not at the end of e2e.js');
|
|
193
|
+
const appended = e2eSource.slice(0, closer) + '\n}); // end\nif (false) {\n total++;\n}\n';
|
|
194
|
+
const late = cs.e2eCases(appended);
|
|
195
|
+
assert(late.problems.some(p => /code after the run-wide section/.test(p)), late.problems.join(' | '));
|
|
196
|
+
const unclosed = cs.e2eCases(e2eSource.slice(0, closer) + '\n');
|
|
197
|
+
assert(unclosed.problems.some(p => /never closes/.test(p)), unclosed.problems.join(' | '));
|
|
198
|
+
const noCreate = cs.e2eCases("if (e2eShard.take('a')) {\n}\n");
|
|
199
|
+
assert(noCreate.problems.some(p => /no line "const e2eShard = require/.test(p)), noCreate.problems.join(' | '));
|
|
200
|
+
assert(noCreate.problems.some(p => /no line "\/\/ e2e-shard: cases start here"/.test(p)), noCreate.problems.join(' | '));
|
|
201
|
+
assert(noCreate.problems.some(p => /before the line "\/\/ e2e-shard: cases start here"/.test(p)), noCreate.problems.join(' | '));
|
|
202
|
+
const twoCreate = cs.e2eCases([CREATE(['fx']), CREATE(['fx']), START, START].join('\n'));
|
|
203
|
+
assert(twoCreate.problems.some(p => /second e2eShard/.test(p)) && twoCreate.problems.some(p => /second "cases start here"/.test(p)));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
check('e2e cases that mention a shared name run in one shard, in order; others are placed one by one', () => {
|
|
207
|
+
const src = [
|
|
208
|
+
CREATE(['fx', 'helper']), START,
|
|
209
|
+
"if (e2eShard.take('s1')) {", ' helper(1);', '}',
|
|
210
|
+
// Not mentions: a longer name, a property, a `$` prefix. A mention inside a string does count (s3b below):
|
|
211
|
+
// grouping a case too many is only slower, grouping one too few can break it.
|
|
212
|
+
"if (e2eShard.take('free1')) {", ' const fxy = 1; obj.fx = 2; const $fx = 3;', '}',
|
|
213
|
+
"if (e2eShard.take('s3b')) {", ' const label = "fx-name";', '}',
|
|
214
|
+
"if (e2eShard.take('s2')) {", ' const t = `', '}', '`; use(fx);', '}',
|
|
215
|
+
"if (e2eShard.take('free2')) {", '}',
|
|
216
|
+
"if (e2eShard.take('s3')) {", ' fs.readFileSync(path.join(fx, "a"));', '}',
|
|
217
|
+
"if (e2eShard.take('free3')) {", '}',
|
|
218
|
+
].join('\n');
|
|
219
|
+
const parsed = cs.e2eCases(src);
|
|
220
|
+
assert.deepStrictEqual(parsed.problems, []);
|
|
221
|
+
assert.deepStrictEqual([...parsed.usesShared], ['s1', 's3b', 's2', 's3']);
|
|
222
|
+
// Equal weights would spread the shared cases over the shards if they were placed one by one.
|
|
223
|
+
const w = { e2e: { s1: 10, free1: 10, s3b: 10, s2: 10, free2: 10, s3: 10, free3: 10 }, npm: {} };
|
|
224
|
+
for (let k = 1; k <= 6; k++) {
|
|
225
|
+
const { cases, load } = cs.planE2e(src, k, w);
|
|
226
|
+
const shards = new Set(cases.filter(c => c.shared).map(c => c.shard));
|
|
227
|
+
assert.strictEqual(shards.size, 1, `${k} shards: shared cases split over ${[...shards]}`);
|
|
228
|
+
assert.strictEqual(load.reduce((a, b) => a + b, 0), 70);
|
|
229
|
+
}
|
|
230
|
+
// On the real file: the B(1.9.8) rule cases leave rules in the shared project for the next ones, and the
|
|
231
|
+
// session-close case reads them (Linux, shard 1/2 of the first version: three of them failed apart).
|
|
232
|
+
const real = cs.e2eCases(e2eSource);
|
|
233
|
+
assert(real.shared.includes('tmp') && real.shared.includes('run'), `shared names ${real.shared}`);
|
|
234
|
+
for (const k of ['B(1.9.8)', 'B(1.9.8) #2', 'B(1.9.8) #3', 'pass']) assert(real.usesShared.has(k), `${k} not grouped`);
|
|
235
|
+
for (let k = 2; k <= 6; k++) {
|
|
236
|
+
const { cases } = cs.planE2e(e2eSource, k, weights);
|
|
237
|
+
assert.strictEqual(new Set(cases.filter(c => c.shared).map(c => c.shard)).size, 1, `${k} shards split the shared cases`);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
check('e2e cases: every case of the real file runs in exactly one shard, for 1..6 shards', () => {
|
|
242
|
+
const keys = cs.e2eCases(e2eSource).keys;
|
|
243
|
+
for (let k = 1; k <= 6; k++) {
|
|
244
|
+
const { cases } = cs.planE2e(e2eSource, k, weights);
|
|
245
|
+
const union = [];
|
|
246
|
+
for (let i = 1; i <= k; i++) union.push(...cases.filter(c => c.shard === i).map(c => c.key));
|
|
247
|
+
assert.deepStrictEqual([...union].sort(), [...keys].sort(), `${k} shards: cases lost or repeated`);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
check('e2eShard runtime: shards take disjoint cases; finish reports unreached, repeated and unknown keys', () => {
|
|
252
|
+
const file = path.join(arena, 'cases.js');
|
|
253
|
+
const names = ['k1', 'k2', 'k3', 'k4', 'k5'];
|
|
254
|
+
fs.writeFileSync(file, CREATE(['fx']) + '\n' + START + '\n' + names.map(k => `if (e2eShard.take('${k}')) {\n total++;\n}\n`).join(''));
|
|
255
|
+
const w = { e2e: { k1: 9, k2: 1, k3: 5, k4: 5, k5: 2 }, npm: {} };
|
|
256
|
+
const taken = [];
|
|
257
|
+
for (let i = 1; i <= 3; i++) {
|
|
258
|
+
const timesFile = path.join(arena, `times-${i}.json`);
|
|
259
|
+
const env = { LEERNESS_E2E_SHARD: `${i}/3`, LEERNESS_E2E_TIMES: timesFile };
|
|
260
|
+
const s = cs.e2eShard(file, ['fx'], env, { weights: w });
|
|
261
|
+
assert(!('LEERNESS_E2E_SHARD' in env) && !('LEERNESS_E2E_TIMES' in env), 'shard env leaked to children');
|
|
262
|
+
const mine = names.filter(k => s.take(k));
|
|
263
|
+
taken.push(...mine);
|
|
264
|
+
const f = s.finish();
|
|
265
|
+
assert(f.ok, JSON.stringify(f));
|
|
266
|
+
assert.strictEqual(f.ran, mine.length);
|
|
267
|
+
assert.deepStrictEqual(Object.keys(JSON.parse(fs.readFileSync(timesFile, 'utf8'))).sort(), [...mine].sort());
|
|
268
|
+
}
|
|
269
|
+
assert.deepStrictEqual([...taken].sort(), names);
|
|
270
|
+
const all = cs.e2eShard(file, ['fx'], {}, { weights: w });
|
|
271
|
+
assert(names.every(k => all.take(k)), 'an unsharded run skipped a case');
|
|
272
|
+
assert(all.finish().ok);
|
|
273
|
+
const missing = cs.e2eShard(file, ['fx'], {}, { weights: w });
|
|
274
|
+
names.slice(1).forEach(k => missing.take(k));
|
|
275
|
+
assert.deepStrictEqual(missing.finish().missing, ['k1']);
|
|
276
|
+
const twice = cs.e2eShard(file, ['fx'], {}, { weights: w });
|
|
277
|
+
names.forEach(k => twice.take(k)); twice.take('k2');
|
|
278
|
+
assert.deepStrictEqual(twice.finish().repeated, ['k2']);
|
|
279
|
+
const unknown = cs.e2eShard(file, ['fx'], { LEERNESS_E2E_SHARD: '2/3' }, { weights: w });
|
|
280
|
+
names.forEach(k => unknown.take(k));
|
|
281
|
+
assert.strictEqual(unknown.take('not-a-guard'), true, 'an unparsed key must run in every shard');
|
|
282
|
+
const u = unknown.finish();
|
|
283
|
+
assert(!u.ok && u.unknown[0] === 'not-a-guard');
|
|
284
|
+
const drift = cs.e2eShard(file, ['fx', 'other'], {}, { weights: w });
|
|
285
|
+
names.forEach(k => drift.take(k));
|
|
286
|
+
const d = drift.finish();
|
|
287
|
+
assert(!d.ok && d.problems.some(p => /differ from the file/.test(p)), JSON.stringify(d.problems));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
check('readme sync counts a case whose counter sits inside its shard guard', () => {
|
|
291
|
+
// The README e2e badge estimate counts column-0 `total++;` and `run(` lines; guarded cases moved the counter.
|
|
292
|
+
const dir = path.join(arena, 'badge');
|
|
293
|
+
fs.mkdirSync(path.join(dir, 'scripts'), { recursive: true });
|
|
294
|
+
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"b","version":"1.2.3"}\n');
|
|
295
|
+
fs.writeFileSync(path.join(dir, 'README.md'), '# b\n\n\n');
|
|
296
|
+
fs.writeFileSync(path.join(dir, 'scripts', 'e2e.js'), [
|
|
297
|
+
'total++;', "run('x', []);",
|
|
298
|
+
"if (e2eShard.take('a')) {", ' total++;', '}',
|
|
299
|
+
"if (e2eShard.take('b')) {", ' total++;', '}',
|
|
300
|
+
"if (e2eShard.take('c')) {", ' const x = 1;', '}',
|
|
301
|
+
].join('\n') + '\n');
|
|
302
|
+
const r = require('child_process').spawnSync(process.execPath, [path.join(ROOT, 'bin', 'leerness.js'), 'readme', 'sync', dir],
|
|
303
|
+
{ encoding: 'utf8', timeout: 60000, env: { ...process.env, LEERNESS_OFFLINE: '1' } });
|
|
304
|
+
assert.strictEqual(r.status, 0, (r.stdout || '') + (r.stderr || ''));
|
|
305
|
+
assert.match(fs.readFileSync(path.join(dir, 'README.md'), 'utf8'), /badge\/e2e-4%2F4-success/);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
check('workflow: every sharded job lists shards 1..k of each configuration; the gate jobs cover one set', () => {
|
|
309
|
+
for (const name of ['test', 'commands', 'fast']) {
|
|
310
|
+
assert(jobs[name] && jobs[name].entries.length, `job ${name} has no matrix entries`);
|
|
311
|
+
assert.deepStrictEqual(cs.shardLists(jobs[name].entries).problems, [], `job ${name}`);
|
|
312
|
+
}
|
|
313
|
+
const configs = name => cs.shardLists(jobs[name].entries).configs.sort();
|
|
314
|
+
assert.deepStrictEqual(configs('test'), configs('core'), 'e2e and core jobs cover different configurations');
|
|
315
|
+
assert.deepStrictEqual(configs('commands'), configs('core'), 'commands and core jobs cover different configurations');
|
|
316
|
+
assert(configs('core').includes('node=24 os=windows-latest'), 'Windows Node 24 missing from the gate');
|
|
317
|
+
assert(jobs.test.runs.includes('node ./scripts/e2e.js'), 'e2e job does not run e2e.js');
|
|
318
|
+
assert(jobs.test.lines.some(l => /^\s+LEERNESS_E2E_SHARD: \$\{\{ matrix\.shard \}\}\/\$\{\{ matrix\.shards \}\}$/.test(l)), 'e2e job does not pass its shard');
|
|
319
|
+
assert.deepStrictEqual([...sharded].sort(), ['test:commands', 'test:fast']);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
check('workflow checks catch a dropped, repeated or mismatched shard entry', () => {
|
|
323
|
+
const drop = workflowText.replace(' - { os: windows-latest, node: 24, shard: 3, shards: 4 }\n', '');
|
|
324
|
+
const dup = workflowText.replace(' - { os: ubuntu-latest, node: 20, shard: 2, shards: 2 }\n',
|
|
325
|
+
' - { os: ubuntu-latest, node: 20, shard: 1, shards: 2 }\n');
|
|
326
|
+
const mixed = workflowText.replace(' - { os: windows-latest, node: 18, shard: 2, shards: 2 }\n',
|
|
327
|
+
' - { os: windows-latest, node: 18, shard: 2, shards: 3 }\n');
|
|
328
|
+
for (const [label, text, job] of [['drop', drop, 'test'], ['dup', dup, 'test'], ['mixed', mixed, 'commands']]) {
|
|
329
|
+
assert.notStrictEqual(text, workflowText, `${label}: fixture edit did not apply`);
|
|
330
|
+
assert.strictEqual(cs.shardLists(cs.workflowJobs(text)[job].entries).problems.length, 1, label);
|
|
331
|
+
}
|
|
332
|
+
const lessCommands = workflowText.replace(' - { os: ubuntu-latest, node: 22, shard: 1, shards: 1 }\n', '');
|
|
333
|
+
assert.notStrictEqual(lessCommands, workflowText);
|
|
334
|
+
const j = cs.workflowJobs(lessCommands);
|
|
335
|
+
assert.notDeepStrictEqual(cs.shardLists(j.commands.entries).configs.sort(), cs.shardLists(j.core.entries).configs.sort());
|
|
336
|
+
});
|
|
337
|
+
} finally {
|
|
338
|
+
fs.rmSync(arena, { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const failed = results.filter(r => !r.ok);
|
|
342
|
+
console.log(`ci-shard probe: ${results.length - failed.length}/${results.length} checks passed`);
|
|
343
|
+
if (failed.length) process.exitCode = 1;
|