groundswell 0.0.1 → 0.0.2
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/.claude/commands/subtask-planning/prp-base-create.md +120 -0
- package/.claude/commands/subtask-planning/prp-base-execute.md +65 -0
- package/.claude/commands/task-breakdown.md +94 -0
- package/.claude/system_prompts/task-breakdown.md +1 -0
- package/CHANGELOG.md +188 -0
- package/PRD.md +543 -0
- package/README.md +99 -5
- package/examples/README.md +15 -1
- package/examples/examples/11-reparenting-workflows.ts +269 -0
- package/examples/index.ts +4 -0
- package/package-lock.json +2398 -0
- package/package.json +3 -1
- package/plan/001_d3bb02af4886/TEST_RESULTS.md +259 -0
- package/plan/001_d3bb02af4886/bug_fix_tasks.json +484 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M1T1S1/PRP.md +488 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M1T1S2/PRP.md +581 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M1T1S3/PRP.md +687 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T1S1/PRP.md +492 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T1S3/PRP.md +932 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T1S3/research/concurrent_error_testing_patterns.md +1109 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T1S3/research/vitest_concurrent_testing.md +802 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T1S3/research/workflow_engine_test_references.md +603 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T2S1/PRP.md +564 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T2S3/PRP.md +518 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T2S4/PRP.md +1252 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T3S1/PRP.md +364 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T3S1/research/CODEBASE_INVENTORY.md +114 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T3S1/research/DECORATOR_DOCUMENTATION_PATTERNS.md +205 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T3S1/research/PRD_LOCATION_ANALYSIS.md +199 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M2T3S1/research/ULTRATHINK_PRP_PLAN.md +134 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T1S1/PRP.md +495 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T1S1/research/console_error_inventory.md +435 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T1S2/PRP.md +506 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T1S3/PRP.md +612 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T2S2/PRP.md +558 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T2S2/research/external_research.md +788 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T3S2/PRP.md +460 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T3S3/PRP.md +454 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T4S1/PRP.md +520 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T4S1/RECOMMENDATION.md +417 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T4S1/research/external_workflow_engines_research.md +760 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T4S1/research/security_implications_analysis.md +245 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M3T4S2/PRP.md +792 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T1S1/PRP.md +535 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T1S1/TEST_EXECUTION_REPORT.md +190 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T1S2/PRP.md +654 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T1S2/TEST_FIX_REPORT.md +227 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T1S2/research/KEY_FINDINGS.md +345 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T1S2/research/QUICK_REFERENCE.md +193 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T1S2/research/test_maintenance_research.md +1323 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T3S1/BREAKING_CHANGES_AUDIT.md +1011 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T3S1/PRP.md +927 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/P1M4T3S2/PRP.md +505 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/architecture/logger_child_signature_analysis.md +401 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M1T1S3/child_implementation_research.md +142 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M1T1S3/test_patterns_research.md +112 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M1T1S3/vitest_patterns_research.md +159 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M1T1S4/PRP.md +549 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M1T1S4/VERIFICATION_REPORT.md +368 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M1T1S4/edge_case_analysis.md +172 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M1T1S4/usage_inventory.md +175 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T1S2/PRP.md +696 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T1S4/PRP.md +860 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/PRP.md +1066 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/01-testing-aggregated-errors.md +1103 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/01_typescript_error_aggregation_patterns.md +789 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/02-error-merge-strategy-testing-guide.md +1098 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/02_aggregate_error_patterns.md +1037 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/03-promise-allsettled-testing-patterns.md +916 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/03_error_merging_strategies.md +1045 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/04_github_stackoverflow_examples.md +890 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/05_comprehensive_summary.md +822 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/INDEX.md +668 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/QUICK_REFERENCE.md +706 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/README.md +265 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S2/research/RESEARCH_REPORT.md +655 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T2S4/research/vitest_testing_patterns.md +1103 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M2T3S2/PRP.md +426 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T1S2/PRP.md +506 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T1S2/research/QUICK_REFERENCE.md +114 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T1S2/research/RESEARCH_SUMMARY.md +316 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T1S2/research/vitest_observer_error_logging_best_practices.md +754 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T1S3/PRP.md +612 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T2S1/PRP.md +719 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T2S1/README.md +215 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T2S1/analysis.md +765 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T2S3/PRP.md +718 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T3S1/DECISION.md +149 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T3S1/PRP.md +470 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T3S1/research/ULTRATHINK_PLAN.md +332 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T3S1/research/codebase_workflow_name_analysis.md +167 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T3S1/research/external_best_practices.md +265 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T3S1/research/validation_patterns.md +273 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T4S1/workflow_engine_ancestry_api_research.md +760 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M3T4S3-PRP.md +434 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M4T2S1/PRP.md +717 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M4T2S2/PRP.md +472 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M4T2S2/VALIDATION_REPORT.md +125 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/P1M4T2S2/research/ULTRATHINK_PRP_PLAN.md +301 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/error-logging-best-practices.md +1170 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/research_typescript_partial_and_overloads.md +940 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/vitest-quick-reference.md +151 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/docs/vitest-research.md +650 -0
- package/plan/001_d3bb02af4886/bugfix/001_e8e04329daf3/prd_snapshot.md +259 -0
- package/plan/001_d3bb02af4886/bugfix/P1M1T1S1/PRP.md +457 -0
- package/plan/001_d3bb02af4886/bugfix/RESEARCH_SUMMARY.md +346 -0
- package/plan/001_d3bb02af4886/bugfix/architecture/codebase_structure.md +311 -0
- package/plan/001_d3bb02af4886/bugfix/architecture/concurrent_execution_best_practices.md +1565 -0
- package/plan/001_d3bb02af4886/bugfix/architecture/error_handling_patterns.md +288 -0
- package/plan/001_d3bb02af4886/bugfix/architecture/promise_all_analysis.md +741 -0
- package/plan/001_d3bb02af4886/docs/PRP/P1M1T1S4-functional-workflow-error-state-capture-test.md +652 -0
- package/plan/001_d3bb02af4886/docs/PRP/PRP.md +527 -0
- package/plan/001_d3bb02af4886/docs/PRP/bugfix/P1M1T2S1-PRP.md +415 -0
- package/plan/001_d3bb02af4886/docs/PRP/bugfix/P1M1T2S2-PRP.md +378 -0
- package/plan/001_d3bb02af4886/docs/PRP/bugfix/P1M1T2S4-PRP.md +713 -0
- package/plan/001_d3bb02af4886/docs/PRP/bugfix/P1M2T1S4-PRP.md +370 -0
- package/plan/001_d3bb02af4886/docs/PRP_P1M3T1S3.md +499 -0
- package/plan/001_d3bb02af4886/docs/TEST_RESULTS.md +230 -0
- package/plan/001_d3bb02af4886/docs/bugfix/ANALYSIS_PRD_VS_IMPLEMENTATION.md +1134 -0
- package/plan/001_d3bb02af4886/docs/bugfix/GAP_ANALYSIS_SUMMARY.md +179 -0
- package/plan/001_d3bb02af4886/docs/bugfix/P1M4T2S1/PRP.md +629 -0
- package/plan/001_d3bb02af4886/docs/bugfix/P1M4T2S1/validation-report.md +214 -0
- package/plan/001_d3bb02af4886/docs/bugfix/PRP_P1M4T2S3.md +629 -0
- package/plan/001_d3bb02af4886/docs/bugfix/bugfix_PRP.md +529 -0
- package/plan/001_d3bb02af4886/docs/bugfix/bugfix_QUICK_REFERENCE.md +142 -0
- package/plan/001_d3bb02af4886/docs/bugfix/bugfix_README.md +304 -0
- package/plan/001_d3bb02af4886/docs/bugfix/bugfix_TEST_RESULTS.md +558 -0
- package/plan/001_d3bb02af4886/docs/bugfix/bugfix_VALIDATION_SUMMARY.md +256 -0
- package/plan/001_d3bb02af4886/docs/bugfix/system_context.md +346 -0
- package/plan/001_d3bb02af4886/docs/bugfix-architecture/bug_analysis.md +415 -0
- package/plan/001_d3bb02af4886/docs/bugfix-architecture/implementation_patterns.md +489 -0
- package/plan/001_d3bb02af4886/docs/bugfix-architecture/system_context.md +218 -0
- package/plan/001_d3bb02af4886/docs/bugfix_INITIATION_SUMMARY.md +380 -0
- package/plan/001_d3bb02af4886/docs/research/CYCLE_DETECTION_PATTERNS.md +1923 -0
- package/plan/001_d3bb02af4886/docs/research/CYCLE_DETECTION_QUICK_REF.md +319 -0
- package/plan/001_d3bb02af4886/docs/research/P1M1T2S1/codebase-context.md +115 -0
- package/plan/001_d3bb02af4886/docs/research/P1M1T2S1/cycle-detection-algorithms.md +134 -0
- package/plan/001_d3bb02af4886/docs/research/P1M1T2S1/test-patterns.md +153 -0
- package/plan/001_d3bb02af4886/docs/research/P1M1T2S1/workflow-class.md +132 -0
- package/plan/001_d3bb02af4886/docs/research/P1M2T1S4/DECORATOR_DOCUMENTATION_BEST_PRACTICES.md +716 -0
- package/plan/001_d3bb02af4886/docs/research/P1M2T1S4/DECORATOR_DOCUMENTATION_QUICK_REF.md +186 -0
- package/plan/001_d3bb02af4886/docs/research/P1M2T1S4/GROUNDSWELL_DECORATOR_EXAMPLES.md +604 -0
- package/plan/001_d3bb02af4886/docs/research/P1M2T1S4/INDEX.md +213 -0
- package/plan/001_d3bb02af4886/docs/research/P1M2T1S4/codebase_structure.md +30 -0
- package/plan/001_d3bb02af4886/docs/research/P1M2T1S4/existing_test_pattern.md +56 -0
- package/plan/001_d3bb02af4886/docs/research/P1M2T1S4/getRootObservers_implementation.md +53 -0
- package/plan/001_d3bb02af4886/docs/research/P1M2T1S4/test_conventions.md +49 -0
- package/plan/001_d3bb02af4886/docs/research/P1M3T1S4/PRP.md +958 -0
- package/plan/001_d3bb02af4886/docs/research/P1M3T1S4/QUICK_REFERENCE.md +339 -0
- package/plan/001_d3bb02af4886/docs/research/P1M3T1S4/README.md +305 -0
- package/plan/001_d3bb02af4886/docs/research/P1M3T1S4/SUMMARY.md +433 -0
- package/plan/001_d3bb02af4886/docs/research/P1M3T1S4/bidirectional-tree-consistency-testing.md +1574 -0
- package/plan/001_d3bb02af4886/docs/research/P1M3T1S4/test-pattern-examples.md +1014 -0
- package/plan/001_d3bb02af4886/docs/research/PROMISE_ALLSETTLED_QUICK_REF.md +376 -0
- package/plan/001_d3bb02af4886/docs/research/PROMISE_ALLSETTLED_RESEARCH.md +1507 -0
- package/plan/001_d3bb02af4886/docs/research/bugfix_typescript_patterns.md +949 -0
- package/plan/001_d3bb02af4886/docs/research/error-testing-research.md +619 -0
- package/plan/001_d3bb02af4886/docs/research/error_handling_patterns.md +723 -0
- package/plan/{research → 001_d3bb02af4886/docs/research/general}/introspection-security-guide.md +56 -0
- package/plan/001_d3bb02af4886/docs/research/incremental-tree-map-updates/PRP_TEMPLATE.md +460 -0
- package/plan/001_d3bb02af4886/docs/research/incremental-tree-map-updates/QUICK_REFERENCE.md +324 -0
- package/plan/001_d3bb02af4886/docs/research/incremental-tree-map-updates/README.md +175 -0
- package/plan/001_d3bb02af4886/docs/research/incremental-tree-map-updates/RESEARCH_REPORT.md +499 -0
- package/plan/001_d3bb02af4886/docs/research/incremental-tree-map-updates/SUMMARY.md +163 -0
- package/plan/bugfix/BUG_FIX_SUMMARY.md +961 -0
- package/src/__tests__/adversarial/attachChild-performance.test.ts +216 -0
- package/src/__tests__/adversarial/circular-reference.test.ts +101 -0
- package/src/__tests__/adversarial/complex-circular-reference.test.ts +139 -0
- package/src/__tests__/adversarial/concurrent-task-failures.test.ts +571 -0
- package/src/__tests__/adversarial/deep-analysis.test.ts +729 -0
- package/src/__tests__/adversarial/deep-hierarchy-stress.test.ts +213 -0
- package/src/__tests__/adversarial/e2e-prd-validation.test.ts +448 -0
- package/src/__tests__/adversarial/edge-case.test.ts +703 -0
- package/src/__tests__/adversarial/error-merge-strategy.test.ts +760 -0
- package/src/__tests__/adversarial/incremental-performance.test.ts +140 -0
- package/src/__tests__/adversarial/node-map-update-benchmarks.test.ts +457 -0
- package/src/__tests__/adversarial/observer-propagation.test.ts +487 -0
- package/src/__tests__/adversarial/parent-validation.test.ts +143 -0
- package/src/__tests__/adversarial/prd-12-2-compliance.test.ts +611 -0
- package/src/__tests__/adversarial/prd-compliance.test.ts +731 -0
- package/src/__tests__/compatibility/backward-compatibility.test.ts +1572 -0
- package/src/__tests__/helpers/index.ts +18 -0
- package/src/__tests__/helpers/tree-verification.ts +257 -0
- package/src/__tests__/integration/bidirectional-consistency.test.ts +847 -0
- package/src/__tests__/integration/observer-logging.test.ts +643 -0
- package/src/__tests__/integration/tree-mirroring.test.ts +37 -0
- package/src/__tests__/integration/workflow-reparenting.test.ts +303 -0
- package/src/__tests__/unit/context.test.ts +79 -0
- package/src/__tests__/unit/logger.test.ts +293 -0
- package/src/__tests__/unit/observable.test.ts +321 -0
- package/src/__tests__/unit/tree-debugger-incremental.test.ts +170 -0
- package/src/__tests__/unit/utils/workflow-error-utils.test.ts +209 -0
- package/src/__tests__/unit/workflow-detachChild.test.ts +100 -0
- package/src/__tests__/unit/workflow-emitEvent-childDetached.test.ts +153 -0
- package/src/__tests__/unit/workflow-isDescendantOf.test.ts +180 -0
- package/src/__tests__/unit/workflow.test.ts +277 -1
- package/src/core/agent.ts +21 -1
- package/src/core/logger.ts +27 -2
- package/src/core/workflow-context.ts +6 -4
- package/src/core/workflow.ts +252 -14
- package/src/debugger/tree-debugger.ts +52 -7
- package/src/decorators/task.ts +65 -2
- package/src/index.ts +4 -2
- package/src/types/decorators.ts +8 -1
- package/src/types/events.ts +1 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/observable.ts +32 -3
- package/src/utils/workflow-error-utils.ts +56 -0
- package/tsconfig.json +1 -1
- package/llms_full.txt +0 -5890
- package/tasks.json +0 -0
- /package/plan/{backlog.json → 001_d3bb02af4886/backlog.json} +0 -0
- /package/plan/{P1P2/PRP.md → 001_d3bb02af4886/docs/PRP/P1P2-PRP.md} +0 -0
- /package/plan/{P3P4/PRP.md → 001_d3bb02af4886/docs/PRP/P3P4-PRP.md} +0 -0
- /package/plan/{P4P5/PRP.md → 001_d3bb02af4886/docs/PRP/P4P5-PRP.md} +0 -0
- /package/plan/{architecture → 001_d3bb02af4886/docs/architecture}/external_deps.md +0 -0
- /package/plan/{architecture → 001_d3bb02af4886/docs/architecture}/system_context.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/LRU_CACHE_BEST_PRACTICES.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/LRU_CACHE_CODE_PATTERNS.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/LRU_CACHE_INTEGRATION_GUIDE.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/LRU_CACHE_RESEARCH_INDEX.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/REFLECTION_INDEX.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/REFLECTION_RESEARCH_REPORT.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/RESEARCH_SUMMARY.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/anthropic-sdk.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/async-local-storage.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/reflection-code-patterns.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/reflection-decision-matrix.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/reflection-implementation-guide.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/reflection-integration-guide.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/reflection-patterns.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/reflection-quick-reference.md +0 -0
- /package/plan/{P1P2/research → 001_d3bb02af4886/docs/research/P1P2}/zod-schema.md +0 -0
- /package/plan/{P3P4/research → 001_d3bb02af4886/docs/research/P3P4}/caching-lru.md +0 -0
- /package/plan/{P3P4/research → 001_d3bb02af4886/docs/research/P3P4}/introspection-tools.md +0 -0
- /package/plan/{P3P4/research → 001_d3bb02af4886/docs/research/P3P4}/reflection-patterns.md +0 -0
- /package/plan/{P4P5/research → 001_d3bb02af4886/docs/research/P4P5}/RESEARCH_SUMMARY.md +0 -0
- /package/plan/{research → 001_d3bb02af4886/docs/research/general}/INTROSPECTION_RESEARCH_SUMMARY.md +0 -0
- /package/plan/{research → 001_d3bb02af4886/docs/research/general}/README-INTROSPECTION.md +0 -0
- /package/plan/{research → 001_d3bb02af4886/docs/research/general}/agent-introspection-patterns.md +0 -0
- /package/plan/{research → 001_d3bb02af4886/docs/research/general}/introspection-tool-examples.md +0 -0
- /package/{PRPs/PRDs/001-hierarchical-workflow-engine.md → plan/001_d3bb02af4886/prd_snapshot.md} +0 -0
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
# Testing Aggregated Errors in TypeScript/JavaScript Workflow Engines
|
|
2
|
+
|
|
3
|
+
**Research Date:** 2026-01-12
|
|
4
|
+
**Task:** P1.M2.T2.S2 - Research testing patterns for error aggregation
|
|
5
|
+
**Focus:** Testing aggregated errors, Promise.allSettled scenarios, error event emissions
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
1. [Overview](#overview)
|
|
9
|
+
2. [Testing Aggregated Errors](#testing-aggregated-errors)
|
|
10
|
+
3. [Promise.allSettled Error Patterns](#promiseallsettled-error-patterns)
|
|
11
|
+
4. [Error Event Emission Testing](#error-event-emission-testing)
|
|
12
|
+
5. [Mock Patterns for Error Scenarios](#mock-patterns-for-error-scenarios)
|
|
13
|
+
6. [Assertion Patterns for Complex Error Objects](#assertion-patterns-for-complex-error-objects)
|
|
14
|
+
7. [Testing Library Recommendations](#testing-library-recommendations)
|
|
15
|
+
8. [Best Practices](#best-practices)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
Error aggregation in workflow engines requires comprehensive testing strategies to ensure:
|
|
22
|
+
- All concurrent errors are collected and preserved
|
|
23
|
+
- Error context is maintained across workflow hierarchies
|
|
24
|
+
- Error events are properly emitted for observability
|
|
25
|
+
- Complex error objects are correctly structured
|
|
26
|
+
- No workflows are orphaned or left in hanging states
|
|
27
|
+
|
|
28
|
+
### Key Testing Concepts
|
|
29
|
+
|
|
30
|
+
1. **Error Collection:** Verify that all errors from concurrent operations are gathered
|
|
31
|
+
2. **Error Preservation:** Ensure error details (stack traces, logs, state) are intact
|
|
32
|
+
3. **Event Emission:** Confirm error events are emitted with correct structure
|
|
33
|
+
4. **Orphan Prevention:** Validate all workflows complete, even when errors occur
|
|
34
|
+
5. **Error Merging:** Test custom and default error aggregation strategies
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Testing Aggregated Errors
|
|
39
|
+
|
|
40
|
+
### Basic Error Aggregation Test Pattern
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { describe, it, expect } from 'vitest';
|
|
44
|
+
|
|
45
|
+
describe('Error Aggregation', () => {
|
|
46
|
+
it('should aggregate errors from multiple concurrent workflows', async () => {
|
|
47
|
+
// ARRANGE: Create workflows that will fail
|
|
48
|
+
const errors: Error[] = [];
|
|
49
|
+
const expectedErrors = 3;
|
|
50
|
+
|
|
51
|
+
class ParentWorkflow extends Workflow {
|
|
52
|
+
@Task({ concurrent: true, errorMergeStrategy: { enabled: true } })
|
|
53
|
+
async spawnFailingChildren() {
|
|
54
|
+
return [
|
|
55
|
+
createFailingWorkflow(this, 'Child-1', 'Error 1'),
|
|
56
|
+
createFailingWorkflow(this, 'Child-2', 'Error 2'),
|
|
57
|
+
createFailingWorkflow(this, 'Child-3', 'Error 3'),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async run() {
|
|
62
|
+
try {
|
|
63
|
+
await this.spawnFailingChildren();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
errors.push(err as Error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parent = new ParentWorkflow('Parent');
|
|
71
|
+
|
|
72
|
+
// ACT: Run the workflow
|
|
73
|
+
await parent.run();
|
|
74
|
+
|
|
75
|
+
// ASSERT: Verify error aggregation
|
|
76
|
+
expect(errors.length).toBe(1);
|
|
77
|
+
const aggregatedError = errors[0];
|
|
78
|
+
|
|
79
|
+
// Verify error contains aggregated information
|
|
80
|
+
expect(aggregatedError.message).toContain(`${expectedErrors} errors`);
|
|
81
|
+
expect(aggregatedError.message).toContain('Error 1');
|
|
82
|
+
expect(aggregatedError.message).toContain('Error 2');
|
|
83
|
+
expect(aggregatedError.message).toContain('Error 3');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Error Aggregation with Custom Merger
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
describe('Custom Error Aggregation', () => {
|
|
92
|
+
it('should use custom error merge function when provided', async () => {
|
|
93
|
+
// ARRANGE: Define custom merge strategy
|
|
94
|
+
const customMerger = (errors: WorkflowError[]): WorkflowError => ({
|
|
95
|
+
message: `Custom merge: ${errors.length} failures`,
|
|
96
|
+
original: errors,
|
|
97
|
+
workflowId: 'merged',
|
|
98
|
+
state: {} as any,
|
|
99
|
+
logs: errors.flatMap(e => e.logs),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
class ParentWorkflow extends Workflow {
|
|
103
|
+
@Task({
|
|
104
|
+
concurrent: true,
|
|
105
|
+
errorMergeStrategy: {
|
|
106
|
+
enabled: true,
|
|
107
|
+
combine: customMerger
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
async spawnChildren() {
|
|
111
|
+
return [
|
|
112
|
+
createFailingWorkflow(this, 'A', 'Fail A'),
|
|
113
|
+
createFailingWorkflow(this, 'B', 'Fail B'),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async run() {
|
|
118
|
+
try {
|
|
119
|
+
await this.spawnChildren();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parent = new ParentWorkflow('Parent');
|
|
127
|
+
|
|
128
|
+
// ACT
|
|
129
|
+
const result = await parent.run();
|
|
130
|
+
|
|
131
|
+
// ASSERT: Custom merger was used
|
|
132
|
+
expect(result).toBeDefined();
|
|
133
|
+
expect((result as any).message).toBe('Custom merge: 2 failures');
|
|
134
|
+
expect((result as any).workflowId).toBe('merged');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Testing Error Depth Limits
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
describe('Error Aggregation Depth Limits', () => {
|
|
143
|
+
it('should respect maxMergeDepth configuration', async () => {
|
|
144
|
+
// ARRANGE: Create nested workflow failures
|
|
145
|
+
class DeeplyNestedWorkflow extends Workflow {
|
|
146
|
+
@Task({
|
|
147
|
+
concurrent: true,
|
|
148
|
+
errorMergeStrategy: {
|
|
149
|
+
enabled: true,
|
|
150
|
+
maxMergeDepth: 2 // Only merge 2 levels deep
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
async spawnNested() {
|
|
154
|
+
return [
|
|
155
|
+
createFailingWorkflow(this, 'Level1-A', 'Error A'),
|
|
156
|
+
createFailingWorkflow(this, 'Level1-B', 'Error B'),
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async run() {
|
|
161
|
+
try {
|
|
162
|
+
await this.spawnNested();
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const root = new DeeplyNestedWorkflow('Root');
|
|
170
|
+
|
|
171
|
+
// ACT
|
|
172
|
+
const result = await root.run();
|
|
173
|
+
|
|
174
|
+
// ASSERT: Depth limit enforced
|
|
175
|
+
expect(result).toBeDefined();
|
|
176
|
+
// Verify nested errors are flattened but not infinitely nested
|
|
177
|
+
const hasProperDepth = validateErrorDepth(result, 2);
|
|
178
|
+
expect(hasProperDepth).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
function validateErrorDepth(error: any, maxDepth: number): boolean {
|
|
182
|
+
// Implementation checks error nesting depth
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Promise.allSettled Error Patterns
|
|
191
|
+
|
|
192
|
+
### Basic Promise.allSettled Error Collection
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
describe('Promise.allSettled Error Collection', () => {
|
|
196
|
+
it('should collect all errors from allSettled promises', async () => {
|
|
197
|
+
// ARRANGE: Create promises with mixed results
|
|
198
|
+
const promises = [
|
|
199
|
+
Promise.reject(new Error('Error 1')),
|
|
200
|
+
Promise.reject(new Error('Error 2')),
|
|
201
|
+
Promise.resolve('success'),
|
|
202
|
+
Promise.reject(new Error('Error 3')),
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
// ACT: Use Promise.allSettled
|
|
206
|
+
const results = await Promise.allSettled(promises);
|
|
207
|
+
|
|
208
|
+
// ASSERT: Filter and verify errors
|
|
209
|
+
const rejected = results.filter(
|
|
210
|
+
(r): r is PromiseRejectedResult => r.status === 'rejected'
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(rejected.length).toBe(3);
|
|
214
|
+
expect(rejected[0].reason.message).toBe('Error 1');
|
|
215
|
+
expect(rejected[1].reason.message).toBe('Error 2');
|
|
216
|
+
expect(rejected[2].reason.message).toBe('Error 3');
|
|
217
|
+
|
|
218
|
+
// ASSERT: Verify fulfilled promises
|
|
219
|
+
const fulfilled = results.filter(
|
|
220
|
+
(r): r is PromiseFulfilledResult<unknown> => r.status === 'fulfilled'
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(fulfilled.length).toBe(1);
|
|
224
|
+
expect(fulfilled[0].value).toBe('success');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Concurrent Workflow Execution with allSettled
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
describe('Concurrent Workflow allSettled', () => {
|
|
233
|
+
it('should complete all workflows even when some fail', async () => {
|
|
234
|
+
// ARRANGE: Track all completions
|
|
235
|
+
const completedWorkflows = new Set<string>();
|
|
236
|
+
const failedWorkflows = new Set<string>();
|
|
237
|
+
|
|
238
|
+
class ParentWorkflow extends Workflow {
|
|
239
|
+
@Task({ concurrent: true })
|
|
240
|
+
async spawnChildren() {
|
|
241
|
+
const children = [
|
|
242
|
+
createChildWorkflow(this, 'Success-1', false),
|
|
243
|
+
createChildWorkflow(this, 'Fail-1', true),
|
|
244
|
+
createChildWorkflow(this, 'Success-2', false),
|
|
245
|
+
createChildWorkflow(this, 'Fail-2', true),
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
// Track all completions
|
|
249
|
+
children.forEach(child => {
|
|
250
|
+
child.run().then(
|
|
251
|
+
() => completedWorkflows.add(child.id),
|
|
252
|
+
() => {
|
|
253
|
+
failedWorkflows.add(child.id);
|
|
254
|
+
completedWorkflows.add(child.id);
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return children;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async run() {
|
|
263
|
+
try {
|
|
264
|
+
await this.spawnChildren();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
// Expected
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const parent = new ParentWorkflow('Parent');
|
|
272
|
+
|
|
273
|
+
// ACT
|
|
274
|
+
await parent.run();
|
|
275
|
+
|
|
276
|
+
// ASSERT: All workflows completed
|
|
277
|
+
expect(completedWorkflows.size).toBe(4);
|
|
278
|
+
expect(failedWorkflows.size).toBe(2);
|
|
279
|
+
|
|
280
|
+
// ASSERT: All children attached
|
|
281
|
+
expect(parent.children.length).toBe(4);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Testing Error Order Preservation
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
describe('Error Order Preservation', () => {
|
|
290
|
+
it('should preserve error order from concurrent execution', async () => {
|
|
291
|
+
// ARRANGE: Create workflows with deterministic failure order
|
|
292
|
+
class OrderedFailureWorkflow extends Workflow {
|
|
293
|
+
@Task({ concurrent: true, errorMergeStrategy: { enabled: true } })
|
|
294
|
+
async spawnChildren() {
|
|
295
|
+
return [
|
|
296
|
+
createChildWorkflow(this, 'First', true),
|
|
297
|
+
createChildWorkflow(this, 'Second', true),
|
|
298
|
+
createChildWorkflow(this, 'Third', true),
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async run() {
|
|
303
|
+
try {
|
|
304
|
+
await this.spawnChildren();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return err;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const parent = new OrderedFailureWorkflow('Parent');
|
|
312
|
+
|
|
313
|
+
// ACT
|
|
314
|
+
const result = await parent.run();
|
|
315
|
+
|
|
316
|
+
// ASSERT: Errors collected in predictable order
|
|
317
|
+
const errorMessages = extractErrorMessages(result);
|
|
318
|
+
expect(errorMessages).toContain('First failed');
|
|
319
|
+
expect(errorMessages).toContain('Second failed');
|
|
320
|
+
expect(errorMessages).toContain('Third failed');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
function extractErrorMessages(error: any): string[] {
|
|
324
|
+
// Extract all error messages from aggregated error
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Error Event Emission Testing
|
|
333
|
+
|
|
334
|
+
### Setting Up Event Observers
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
describe('Error Event Emission', () => {
|
|
338
|
+
/**
|
|
339
|
+
* Helper to setup event observer for event collection
|
|
340
|
+
* Pattern from: src/__tests__/adversarial/observer-propagation.test.ts
|
|
341
|
+
*/
|
|
342
|
+
function setupEventObserver(workflow: Workflow): WorkflowEvent[] {
|
|
343
|
+
const events: WorkflowEvent[] = [];
|
|
344
|
+
|
|
345
|
+
workflow.addObserver({
|
|
346
|
+
onLog: () => {},
|
|
347
|
+
onEvent: (e) => events.push(e),
|
|
348
|
+
onStateUpdated: () => {},
|
|
349
|
+
onTreeChanged: () => {},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return events;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
it('should emit error events for all failing workflows', async () => {
|
|
356
|
+
// ARRANGE: Setup event collection
|
|
357
|
+
class ParentWorkflow extends Workflow {
|
|
358
|
+
@Task({ concurrent: true })
|
|
359
|
+
async spawnChildren() {
|
|
360
|
+
return [
|
|
361
|
+
createChildWorkflow(this, 'Good', false),
|
|
362
|
+
createChildWorkflow(this, 'Bad1', true),
|
|
363
|
+
createChildWorkflow(this, 'Bad2', true),
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async run() {
|
|
368
|
+
try {
|
|
369
|
+
await this.spawnChildren();
|
|
370
|
+
} catch (err) {
|
|
371
|
+
// Expected
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const parent = new ParentWorkflow('Parent');
|
|
377
|
+
const events = setupEventObserver(parent);
|
|
378
|
+
|
|
379
|
+
// ACT
|
|
380
|
+
await parent.run();
|
|
381
|
+
|
|
382
|
+
// ASSERT: Error events emitted for both failures
|
|
383
|
+
const errorEvents = events.filter(e => e.type === 'error');
|
|
384
|
+
expect(errorEvents.length).toBeGreaterThanOrEqual(2);
|
|
385
|
+
|
|
386
|
+
// ASSERT: Each error event has correct structure
|
|
387
|
+
errorEvents.forEach(event => {
|
|
388
|
+
expect(event.type).toBe('error');
|
|
389
|
+
if (event.type === 'error') {
|
|
390
|
+
expect(event.error).toBeDefined();
|
|
391
|
+
expect(event.error.workflowId).toBeDefined();
|
|
392
|
+
expect(event.error.message).toBeDefined();
|
|
393
|
+
expect(Array.isArray(event.error.logs)).toBe(true);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Verifying Event Propagation
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
describe('Error Event Propagation', () => {
|
|
404
|
+
it('should propagate error events to parent observers', async () => {
|
|
405
|
+
// ARRANGE: Create workflow hierarchy
|
|
406
|
+
class ChildWorkflow extends Workflow {
|
|
407
|
+
async run() {
|
|
408
|
+
throw new Error('Child error');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
class ParentWorkflow extends Workflow {
|
|
413
|
+
@Task({ concurrent: true })
|
|
414
|
+
async spawnChild() {
|
|
415
|
+
return [new ChildWorkflow('Child', this)];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async run() {
|
|
419
|
+
try {
|
|
420
|
+
await this.spawnChild();
|
|
421
|
+
} catch (err) {
|
|
422
|
+
// Expected
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const parent = new ParentWorkflow('Parent');
|
|
428
|
+
const events: WorkflowEvent[] = [];
|
|
429
|
+
|
|
430
|
+
// CRITICAL: Add observer to root workflow
|
|
431
|
+
parent.addObserver({
|
|
432
|
+
onLog: () => {},
|
|
433
|
+
onEvent: (e) => events.push(e),
|
|
434
|
+
onStateUpdated: () => {},
|
|
435
|
+
onTreeChanged: () => {},
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ACT
|
|
439
|
+
await parent.run();
|
|
440
|
+
|
|
441
|
+
// ASSERT: Child error events visible to parent observer
|
|
442
|
+
const childErrorEvents = events.filter(
|
|
443
|
+
e => e.type === 'error' && e.error.workflowId === 'Child'
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect(childErrorEvents.length).toBeGreaterThan(0);
|
|
447
|
+
expect(childErrorEvents[0]).toMatchObject({
|
|
448
|
+
type: 'error',
|
|
449
|
+
error: expect.objectContaining({
|
|
450
|
+
workflowId: 'Child',
|
|
451
|
+
message: expect.stringContaining('Child error')
|
|
452
|
+
})
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Testing Event Order
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
describe('Error Event Order', () => {
|
|
462
|
+
it('should emit events in correct chronological order', async () => {
|
|
463
|
+
// ARRANGE
|
|
464
|
+
const eventTimestamps: number[] = [];
|
|
465
|
+
|
|
466
|
+
class TimestampTrackingWorkflow extends Workflow {
|
|
467
|
+
@Task({ concurrent: true })
|
|
468
|
+
async spawnChildren() {
|
|
469
|
+
return [
|
|
470
|
+
createChildWorkflow(this, 'Child-1', true),
|
|
471
|
+
createChildWorkflow(this, 'Child-2', true),
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async run() {
|
|
476
|
+
try {
|
|
477
|
+
await this.spawnChildren();
|
|
478
|
+
} catch (err) {
|
|
479
|
+
// Expected
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const parent = new TimestampTrackingWorkflow('Parent');
|
|
485
|
+
|
|
486
|
+
parent.addObserver({
|
|
487
|
+
onLog: () => {},
|
|
488
|
+
onEvent: (e) => {
|
|
489
|
+
eventTimestamps.push(Date.now());
|
|
490
|
+
},
|
|
491
|
+
onStateUpdated: () => {},
|
|
492
|
+
onTreeChanged: () => {},
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// ACT
|
|
496
|
+
await parent.run();
|
|
497
|
+
|
|
498
|
+
// ASSERT: Events are in chronological order
|
|
499
|
+
for (let i = 1; i < eventTimestamps.length; i++) {
|
|
500
|
+
expect(eventTimestamps[i]).toBeGreaterThanOrEqual(eventTimestamps[i - 1]);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Mock Patterns for Error Scenarios
|
|
509
|
+
|
|
510
|
+
### Error Factory Pattern
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
describe('Mock Error Patterns', () => {
|
|
514
|
+
/**
|
|
515
|
+
* Factory function to create consistent mock errors
|
|
516
|
+
*/
|
|
517
|
+
function createMockError(overrides?: Partial<WorkflowError>): WorkflowError {
|
|
518
|
+
return {
|
|
519
|
+
message: 'Mock workflow error',
|
|
520
|
+
original: new Error('Original error'),
|
|
521
|
+
workflowId: 'mock-workflow-id',
|
|
522
|
+
stack: 'Error: Original error\n at mock.js:10:15',
|
|
523
|
+
state: {} as any,
|
|
524
|
+
logs: [
|
|
525
|
+
{ id: '1', workflowId: 'mock', timestamp: Date.now(), level: 'error', message: 'Error occurred' }
|
|
526
|
+
],
|
|
527
|
+
...overrides
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
it('should create consistent mock errors for testing', async () => {
|
|
532
|
+
// ARRANGE: Create mock errors with variations
|
|
533
|
+
const mockErrors = [
|
|
534
|
+
createMockError({ message: 'Error 1', workflowId: 'workflow-1' }),
|
|
535
|
+
createMockError({ message: 'Error 2', workflowId: 'workflow-2' }),
|
|
536
|
+
createMockError({ message: 'Error 3', workflowId: 'workflow-3' }),
|
|
537
|
+
];
|
|
538
|
+
|
|
539
|
+
// ACT: Test error merge with mocks
|
|
540
|
+
const merged = mergeErrors(mockErrors);
|
|
541
|
+
|
|
542
|
+
// ASSERT: Verify merge behavior
|
|
543
|
+
expect(merged.message).toContain('3 errors');
|
|
544
|
+
expect(merged.message).toContain('Error 1');
|
|
545
|
+
expect(merged.message).toContain('Error 2');
|
|
546
|
+
expect(merged.message).toContain('Error 3');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
function mergeErrors(errors: WorkflowError[]): WorkflowError {
|
|
550
|
+
return {
|
|
551
|
+
message: `${errors.length} errors: ${errors.map(e => e.message).join(', ')}`,
|
|
552
|
+
original: errors,
|
|
553
|
+
workflowId: 'merged',
|
|
554
|
+
state: {} as any,
|
|
555
|
+
logs: errors.flatMap(e => e.logs),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Scenario-Based Error Mocking
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
describe('Scenario-Based Error Mocking', () => {
|
|
565
|
+
/**
|
|
566
|
+
* Enum of common error scenarios
|
|
567
|
+
*/
|
|
568
|
+
enum ErrorScenario {
|
|
569
|
+
NETWORK_FAILURE = 'NETWORK_FAILURE',
|
|
570
|
+
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
|
571
|
+
TIMEOUT = 'TIMEOUT',
|
|
572
|
+
PERMISSION_DENIED = 'PERMISSION_DENIED',
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Map of scenarios to mock errors
|
|
577
|
+
*/
|
|
578
|
+
const errorScenarios: Record<ErrorScenario, WorkflowError> = {
|
|
579
|
+
[ErrorScenario.NETWORK_FAILURE]: createMockError({
|
|
580
|
+
message: 'Network connection failed',
|
|
581
|
+
workflowId: 'network-task',
|
|
582
|
+
}),
|
|
583
|
+
[ErrorScenario.VALIDATION_ERROR]: createMockError({
|
|
584
|
+
message: 'Validation failed: invalid input',
|
|
585
|
+
workflowId: 'validation-task',
|
|
586
|
+
}),
|
|
587
|
+
[ErrorScenario.TIMEOUT]: createMockError({
|
|
588
|
+
message: 'Operation timed out after 30000ms',
|
|
589
|
+
workflowId: 'timeout-task',
|
|
590
|
+
}),
|
|
591
|
+
[ErrorScenario.PERMISSION_DENIED]: createMockError({
|
|
592
|
+
message: 'Permission denied: insufficient privileges',
|
|
593
|
+
workflowId: 'auth-task',
|
|
594
|
+
}),
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
it('should handle different error scenarios correctly', async () => {
|
|
598
|
+
// Test each scenario
|
|
599
|
+
for (const [scenario, error] of Object.entries(errorScenarios)) {
|
|
600
|
+
// ARRANGE: Setup workflow with scenario
|
|
601
|
+
class ScenarioWorkflow extends Workflow {
|
|
602
|
+
async run() {
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const workflow = new ScenarioWorkflow(scenario);
|
|
608
|
+
|
|
609
|
+
// ACT & ASSERT: Verify error handling
|
|
610
|
+
await expect(workflow.run()).rejects.toMatchObject({
|
|
611
|
+
message: error.message,
|
|
612
|
+
workflowId: error.workflowId,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Spying on Error Handling
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
describe('Error Handling Spies', () => {
|
|
623
|
+
it('should track error handling with spies', async () => {
|
|
624
|
+
// ARRANGE: Setup spies
|
|
625
|
+
const errorHandler = vi.fn();
|
|
626
|
+
const errorMerger = vi.fn((errors: WorkflowError[]) => ({
|
|
627
|
+
message: `Merged: ${errors.length} errors`,
|
|
628
|
+
original: errors,
|
|
629
|
+
workflowId: 'merged',
|
|
630
|
+
state: {} as any,
|
|
631
|
+
logs: [],
|
|
632
|
+
}));
|
|
633
|
+
|
|
634
|
+
class SpiedWorkflow extends Workflow {
|
|
635
|
+
@Task({
|
|
636
|
+
concurrent: true,
|
|
637
|
+
errorMergeStrategy: {
|
|
638
|
+
enabled: true,
|
|
639
|
+
combine: errorMerger,
|
|
640
|
+
}
|
|
641
|
+
})
|
|
642
|
+
async spawnChildren() {
|
|
643
|
+
return [
|
|
644
|
+
createFailingWorkflow(this, 'A', 'Error A'),
|
|
645
|
+
createFailingWorkflow(this, 'B', 'Error B'),
|
|
646
|
+
];
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async run() {
|
|
650
|
+
try {
|
|
651
|
+
await this.spawnChildren();
|
|
652
|
+
} catch (err) {
|
|
653
|
+
errorHandler(err);
|
|
654
|
+
throw err;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const workflow = new SpiedWorkflow('Spied');
|
|
660
|
+
|
|
661
|
+
// ACT
|
|
662
|
+
await expect(workflow.run()).rejects.toThrow();
|
|
663
|
+
|
|
664
|
+
// ASSERT: Verify spies called correctly
|
|
665
|
+
expect(errorHandler).toHaveBeenCalledTimes(1);
|
|
666
|
+
expect(errorMerger).toHaveBeenCalledTimes(1);
|
|
667
|
+
expect(errorMerger).toHaveBeenCalledWith(
|
|
668
|
+
expect.arrayContaining([
|
|
669
|
+
expect.objectContaining({ message: expect.any(String) })
|
|
670
|
+
])
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## Assertion Patterns for Complex Error Objects
|
|
679
|
+
|
|
680
|
+
### Type Guard Assertions
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
describe('Complex Error Object Assertions', () => {
|
|
684
|
+
/**
|
|
685
|
+
* Type guard for WorkflowError
|
|
686
|
+
*/
|
|
687
|
+
function isWorkflowError(error: unknown): error is WorkflowError {
|
|
688
|
+
return (
|
|
689
|
+
typeof error === 'object' &&
|
|
690
|
+
error !== null &&
|
|
691
|
+
'message' in error &&
|
|
692
|
+
'workflowId' in error &&
|
|
693
|
+
'logs' in error
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
it('should use type guards for error assertions', async () => {
|
|
698
|
+
// ARRANGE
|
|
699
|
+
class ComplexWorkflow extends Workflow {
|
|
700
|
+
async run() {
|
|
701
|
+
throw new Error('Complex error');
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const workflow = new ComplexWorkflow('Complex');
|
|
706
|
+
|
|
707
|
+
// ACT
|
|
708
|
+
try {
|
|
709
|
+
await workflow.run();
|
|
710
|
+
fail('Expected error to be thrown');
|
|
711
|
+
} catch (error) {
|
|
712
|
+
// ASSERT: Use type guard
|
|
713
|
+
if (isWorkflowError(error)) {
|
|
714
|
+
expect(error.message).toBeDefined();
|
|
715
|
+
expect(error.workflowId).toBeDefined();
|
|
716
|
+
expect(Array.isArray(error.logs)).toBe(true);
|
|
717
|
+
} else {
|
|
718
|
+
fail('Error is not a WorkflowError');
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### Partial Object Matching
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
describe('Partial Object Matching', () => {
|
|
729
|
+
it('should match error object properties', async () => {
|
|
730
|
+
// ARRANGE
|
|
731
|
+
class PartialWorkflow extends Workflow {
|
|
732
|
+
@Task({ concurrent: true, errorMergeStrategy: { enabled: true } })
|
|
733
|
+
async spawnChildren() {
|
|
734
|
+
return [
|
|
735
|
+
createFailingWorkflow(this, 'Child-1', 'Error 1'),
|
|
736
|
+
createFailingWorkflow(this, 'Child-2', 'Error 2'),
|
|
737
|
+
];
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async run() {
|
|
741
|
+
try {
|
|
742
|
+
await this.spawnChildren();
|
|
743
|
+
} catch (err) {
|
|
744
|
+
return err;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const workflow = new PartialWorkflow('Partial');
|
|
750
|
+
|
|
751
|
+
// ACT
|
|
752
|
+
const result = await workflow.run();
|
|
753
|
+
|
|
754
|
+
// ASSERT: Use matchObject for partial matching
|
|
755
|
+
expect(result).toMatchObject({
|
|
756
|
+
message: expect.stringContaining('2 errors'),
|
|
757
|
+
workflowId: expect.any(String),
|
|
758
|
+
logs: expect.any(Array),
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// ASSERT: Nested object matching
|
|
762
|
+
if (isWorkflowError(result)) {
|
|
763
|
+
expect(result.logs).toEqual(
|
|
764
|
+
expect.arrayContaining([
|
|
765
|
+
expect.objectContaining({
|
|
766
|
+
workflowId: expect.any(String),
|
|
767
|
+
level: expect.any(String),
|
|
768
|
+
})
|
|
769
|
+
])
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### Custom Async Matchers
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
describe('Custom Async Matchers', () => {
|
|
780
|
+
it('should use custom matchers for async error validation', async () => {
|
|
781
|
+
// ARRANGE: Define custom matcher
|
|
782
|
+
const toBeAggregatedError = async (received: any) => {
|
|
783
|
+
const pass = received &&
|
|
784
|
+
typeof received.message === 'string' &&
|
|
785
|
+
received.message.includes('errors') &&
|
|
786
|
+
Array.isArray(received.logs);
|
|
787
|
+
|
|
788
|
+
return {
|
|
789
|
+
pass,
|
|
790
|
+
message: () => pass
|
|
791
|
+
? `Expected ${received} not to be an aggregated error`
|
|
792
|
+
: `Expected ${received} to be an aggregated error with message containing 'errors' and logs array`,
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// Add custom matcher to expect
|
|
797
|
+
expect.extend({ toBeAggregatedError });
|
|
798
|
+
|
|
799
|
+
class AggregatedWorkflow extends Workflow {
|
|
800
|
+
@Task({ concurrent: true, errorMergeStrategy: { enabled: true } })
|
|
801
|
+
async spawnChildren() {
|
|
802
|
+
return [
|
|
803
|
+
createFailingWorkflow(this, 'A', 'Error A'),
|
|
804
|
+
createFailingWorkflow(this, 'B', 'Error B'),
|
|
805
|
+
];
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async run() {
|
|
809
|
+
try {
|
|
810
|
+
await this.spawnChildren();
|
|
811
|
+
} catch (err) {
|
|
812
|
+
return err;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const workflow = new AggregatedWorkflow('Aggregated');
|
|
818
|
+
|
|
819
|
+
// ACT
|
|
820
|
+
const result = await workflow.run();
|
|
821
|
+
|
|
822
|
+
// ASSERT: Use custom matcher
|
|
823
|
+
await expect(result).toBeAggregatedError();
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
## Testing Library Recommendations
|
|
831
|
+
|
|
832
|
+
### Vitest (Recommended for This Project)
|
|
833
|
+
|
|
834
|
+
**Pros:**
|
|
835
|
+
- Native ESM support
|
|
836
|
+
- Fast execution with worker threads
|
|
837
|
+
- Jest-compatible API
|
|
838
|
+
- Built-in TypeScript support
|
|
839
|
+
- Watch mode with intelligent re-running
|
|
840
|
+
|
|
841
|
+
**Installation:**
|
|
842
|
+
```bash
|
|
843
|
+
npm install -D vitest
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
**Configuration:** See `vitest.config.ts` in project root
|
|
847
|
+
|
|
848
|
+
**Usage:**
|
|
849
|
+
```bash
|
|
850
|
+
vitest run # Run tests once
|
|
851
|
+
vitest # Watch mode
|
|
852
|
+
vitest --coverage # With coverage
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### Jest (Alternative)
|
|
856
|
+
|
|
857
|
+
**Pros:**
|
|
858
|
+
- Largest ecosystem of plugins
|
|
859
|
+
- Extensive documentation
|
|
860
|
+
- Wide community adoption
|
|
861
|
+
- Rich mocking capabilities
|
|
862
|
+
|
|
863
|
+
**Installation:**
|
|
864
|
+
```bash
|
|
865
|
+
npm install -D jest @types/jest ts-jest
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
**Configuration:**
|
|
869
|
+
```javascript
|
|
870
|
+
// jest.config.js
|
|
871
|
+
module.exports = {
|
|
872
|
+
preset: 'ts-jest/presets/default-esm',
|
|
873
|
+
testEnvironment: 'node',
|
|
874
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
875
|
+
moduleNameMapper: {
|
|
876
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
877
|
+
},
|
|
878
|
+
transform: {},
|
|
879
|
+
};
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### Library Comparison
|
|
883
|
+
|
|
884
|
+
| Feature | Vitest | Jest |
|
|
885
|
+
|---------|--------|------|
|
|
886
|
+
| ESM Support | Native | Requires config |
|
|
887
|
+
| Speed | Faster | Slower |
|
|
888
|
+
| Watch Mode | Excellent | Good |
|
|
889
|
+
| TypeScript | Built-in | Requires ts-jest |
|
|
890
|
+
| API | Jest-compatible | Jest API |
|
|
891
|
+
| Ecosystem | Growing | Mature |
|
|
892
|
+
|
|
893
|
+
**Recommendation:** Use Vitest for this project (already configured)
|
|
894
|
+
|
|
895
|
+
---
|
|
896
|
+
|
|
897
|
+
## Best Practices
|
|
898
|
+
|
|
899
|
+
### 1. Test Organization
|
|
900
|
+
|
|
901
|
+
```typescript
|
|
902
|
+
describe('Feature Name', () => {
|
|
903
|
+
describe('Specific Scenario', () => {
|
|
904
|
+
it('should do something specific', async () => {
|
|
905
|
+
// ARRANGE: Setup test data and conditions
|
|
906
|
+
// ACT: Execute the code being tested
|
|
907
|
+
// ASSERT: Verify expected outcomes
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### 2. Descriptive Test Names
|
|
914
|
+
|
|
915
|
+
```typescript
|
|
916
|
+
// Good: Descriptive and clear
|
|
917
|
+
it('should aggregate all errors when multiple concurrent workflows fail', async () => {});
|
|
918
|
+
|
|
919
|
+
// Bad: Vague
|
|
920
|
+
it('should work', async () => {});
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### 3. Isolated Tests
|
|
924
|
+
|
|
925
|
+
Each test should be independent and not rely on other tests:
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
describe('Isolated Tests', () => {
|
|
929
|
+
it('test 1', async () => {
|
|
930
|
+
const workflow = new Workflow('Test1'); // Fresh instance
|
|
931
|
+
// Test logic
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('test 2', async () => {
|
|
935
|
+
const workflow = new Workflow('Test2'); // Fresh instance
|
|
936
|
+
// Test logic - doesn't depend on test 1
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
### 4. Explicit Assertions
|
|
942
|
+
|
|
943
|
+
```typescript
|
|
944
|
+
// Good: Explicit expectations
|
|
945
|
+
expect(result).toBeDefined();
|
|
946
|
+
expect(result.errors).toHaveLength(3);
|
|
947
|
+
expect(result.errors[0].message).toBe('Error 1');
|
|
948
|
+
|
|
949
|
+
// Bad: Implicit
|
|
950
|
+
expect(result).toBeTruthy(); // Doesn't tell us what's true
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
### 5. Error Testing Patterns
|
|
954
|
+
|
|
955
|
+
```typescript
|
|
956
|
+
// Pattern 1: Try-catch with assertions
|
|
957
|
+
try {
|
|
958
|
+
await workflow.run();
|
|
959
|
+
fail('Expected error to be thrown');
|
|
960
|
+
} catch (error) {
|
|
961
|
+
expect(error).toMatchObject({ message: 'Expected error' });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Pattern 2: async/await with rejects
|
|
965
|
+
await expect(workflow.run()).rejects.toThrow('Expected error');
|
|
966
|
+
|
|
967
|
+
// Pattern 3: Return error from try-catch
|
|
968
|
+
const error = await workflow.run().catch(err => err);
|
|
969
|
+
expect(error).toBeDefined();
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### 6. Cleanup in Tests
|
|
973
|
+
|
|
974
|
+
```typescript
|
|
975
|
+
describe('Cleanup', () => {
|
|
976
|
+
it('should clean up resources', async () => {
|
|
977
|
+
const workflow = new Workflow('Test');
|
|
978
|
+
const observers = setupObservers(workflow);
|
|
979
|
+
|
|
980
|
+
try {
|
|
981
|
+
await workflow.run();
|
|
982
|
+
} finally {
|
|
983
|
+
// Cleanup: Remove observers
|
|
984
|
+
observers.forEach(obs => workflow.removeObserver(obs));
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
### 7. Testing Edge Cases
|
|
991
|
+
|
|
992
|
+
```typescript
|
|
993
|
+
describe('Edge Cases', () => {
|
|
994
|
+
it('should handle empty error array', async () => {
|
|
995
|
+
const result = mergeErrors([]);
|
|
996
|
+
expect(result).toMatchObject({ message: '0 errors' });
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('should handle single error', async () => {
|
|
1000
|
+
const result = mergeErrors([singleError]);
|
|
1001
|
+
expect(result.message).toContain('1 error');
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('should handle all workflows failing', async () => {
|
|
1005
|
+
// Test when 100% of workflows fail
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it('should handle 50% failure rate', async () => {
|
|
1009
|
+
// Test mixed success/failure
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
### 8. Performance Testing
|
|
1015
|
+
|
|
1016
|
+
```typescript
|
|
1017
|
+
describe('Performance', () => {
|
|
1018
|
+
it('should complete error aggregation within timeout', async () => {
|
|
1019
|
+
const startTime = performance.now();
|
|
1020
|
+
|
|
1021
|
+
await workflowWithManyErrors.run();
|
|
1022
|
+
|
|
1023
|
+
const duration = performance.now() - startTime;
|
|
1024
|
+
expect(duration).toBeLessThan(1000); // 1 second max
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
### 9. Deterministic Tests
|
|
1030
|
+
|
|
1031
|
+
```typescript
|
|
1032
|
+
// Good: Deterministic
|
|
1033
|
+
it('should handle errors in predictable order', async () => {
|
|
1034
|
+
const errors = await workflow.run().catch(err => err);
|
|
1035
|
+
expect(errors[0].workflowId).toBe('workflow-1');
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// Bad: Relies on timing/setTimeout
|
|
1039
|
+
it('should eventually handle errors', async () => {
|
|
1040
|
+
// Don't use arbitrary timeouts
|
|
1041
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1042
|
+
});
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
### 10. Comprehensive Coverage
|
|
1046
|
+
|
|
1047
|
+
```typescript
|
|
1048
|
+
describe('Comprehensive Error Testing', () => {
|
|
1049
|
+
describe('Single error scenarios', () => {
|
|
1050
|
+
it('should handle single workflow failure');
|
|
1051
|
+
it('should handle single workflow timeout');
|
|
1052
|
+
it('should handle single workflow rejection');
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
describe('Multiple error scenarios', () => {
|
|
1056
|
+
it('should handle 2 concurrent failures');
|
|
1057
|
+
it('should handle 5 concurrent failures');
|
|
1058
|
+
it('should handle 10+ concurrent failures');
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
describe('Mixed scenarios', () => {
|
|
1062
|
+
it('should handle 1 failure + 1 success');
|
|
1063
|
+
it('should handle 2 failures + 3 successes');
|
|
1064
|
+
it('should handle 50% failure rate');
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
describe('Edge cases', () => {
|
|
1068
|
+
it('should handle 0 errors');
|
|
1069
|
+
it('should handle all errors');
|
|
1070
|
+
it('should handle deeply nested errors');
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
---
|
|
1076
|
+
|
|
1077
|
+
## Additional Resources
|
|
1078
|
+
|
|
1079
|
+
### Project-Specific Patterns
|
|
1080
|
+
- **Concurrent Task Failures Test:** `/home/dustin/projects/groundswell/src/__tests__/adversarial/concurrent-task-failures.test.ts`
|
|
1081
|
+
- **Observer Propagation Test:** `/home/dustin/projects/groundswell/src/__tests__/adversarial/observer-propagation.test.ts`
|
|
1082
|
+
- **Task Decorator Implementation:** `/home/dustin/projects/groundswell/src/decorators/task.ts`
|
|
1083
|
+
|
|
1084
|
+
### External Documentation
|
|
1085
|
+
- **Vitest Documentation:** https://vitest.dev/guide/
|
|
1086
|
+
- **Jest Documentation:** https://jestjs.io/docs/getting-started
|
|
1087
|
+
- **Promise.allSettled MDN:** https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
|
|
1088
|
+
|
|
1089
|
+
---
|
|
1090
|
+
|
|
1091
|
+
## Summary
|
|
1092
|
+
|
|
1093
|
+
This research document provides comprehensive patterns for testing error aggregation in TypeScript/JavaScript workflow engines, focusing on:
|
|
1094
|
+
|
|
1095
|
+
1. **Aggregated Error Testing:** Multiple patterns for testing error collection and merging
|
|
1096
|
+
2. **Promise.allSettled Patterns:** Ensuring all workflows complete despite failures
|
|
1097
|
+
3. **Event Emission Testing:** Verifying error events are properly emitted and propagated
|
|
1098
|
+
4. **Mock Patterns:** Factory functions, scenario-based mocking, and spying
|
|
1099
|
+
5. **Assertion Patterns:** Type guards, partial matching, and custom matchers
|
|
1100
|
+
6. **Library Recommendations:** Vitest (recommended) vs Jest comparison
|
|
1101
|
+
7. **Best Practices:** Organization, naming, isolation, cleanup, and edge cases
|
|
1102
|
+
|
|
1103
|
+
These patterns can be directly applied to implementing tests for the ErrorMergeStrategy functionality (P1.M2.T2) in the groundswell project.
|