switchroom 0.5.0
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/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/autoaccept.exp +81 -0
- package/bin/boot-self-test.sh +149 -0
- package/bin/bridge-watchdog.sh +967 -0
- package/bin/handoff-briefing.sh +206 -0
- package/bin/run-hook.sh +228 -0
- package/bin/switchroom.ts +4 -0
- package/bin/timezone-hook.sh +67 -0
- package/bin/user-profile-refresh-hook.sh +38 -0
- package/bin/workspace-dynamic-hook.sh +142 -0
- package/bin/workspace-stable-hook.sh +57 -0
- package/dist/cli/autoaccept-poll.js +118 -0
- package/dist/cli/switchroom.js +48557 -0
- package/package.json +95 -0
- package/profiles/_base/settings.json.hbs +15 -0
- package/profiles/_base/start.sh.hbs +383 -0
- package/profiles/_shared/telegram-style.md.hbs +140 -0
- package/profiles/coding/CLAUDE.md.hbs +57 -0
- package/profiles/coding/skills/architecture/SKILL.md +70 -0
- package/profiles/coding/skills/code-review/SKILL.md +58 -0
- package/profiles/coding/workspace/SOUL.md.hbs +25 -0
- package/profiles/default/CLAUDE.md +238 -0
- package/profiles/default/CLAUDE.md.hbs +113 -0
- package/profiles/default/workspace/CLAUDE.md.hbs +126 -0
- package/profiles/default/workspace/HEARTBEAT.md.hbs +40 -0
- package/profiles/default/workspace/IDENTITY.md.hbs +32 -0
- package/profiles/default/workspace/MEMORY.md.hbs +29 -0
- package/profiles/default/workspace/SOUL.md.hbs +61 -0
- package/profiles/default/workspace/TOOLS.md.hbs +29 -0
- package/profiles/default/workspace/USER.md.hbs +52 -0
- package/profiles/default/workspace/memory/.gitkeep +0 -0
- package/profiles/executive-assistant/CLAUDE.md.hbs +51 -0
- package/profiles/executive-assistant/skills/daily-briefing/SKILL.md +55 -0
- package/profiles/executive-assistant/skills/meeting-prep/SKILL.md +58 -0
- package/profiles/executive-assistant/workspace/SOUL.md.hbs +25 -0
- package/profiles/health-coach/CLAUDE.md.hbs +45 -0
- package/profiles/health-coach/skills/check-in/SKILL.md +41 -0
- package/profiles/health-coach/skills/weekly-review/SKILL.md +53 -0
- package/profiles/health-coach/workspace/SOUL.md.hbs +25 -0
- package/skills/buildkite-agent-infrastructure/SKILL.md +302 -0
- package/skills/buildkite-agent-infrastructure/agents/openai.yaml +6 -0
- package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-agent-infrastructure/references/audit-logging.md +87 -0
- package/skills/buildkite-agent-infrastructure/references/graphql-mutations.md +690 -0
- package/skills/buildkite-agent-infrastructure/references/instance-shapes.md +38 -0
- package/skills/buildkite-agent-infrastructure/references/pipeline-templates.md +73 -0
- package/skills/buildkite-agent-infrastructure/references/self-hosted-agents.md +137 -0
- package/skills/buildkite-agent-infrastructure/references/sso-saml.md +92 -0
- package/skills/buildkite-agent-runtime/SKILL.md +476 -0
- package/skills/buildkite-agent-runtime/agents/openai.yaml +6 -0
- package/skills/buildkite-agent-runtime/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-agent-runtime/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-agent-runtime/references/flag-reference.md +417 -0
- package/skills/buildkite-agent-runtime/references/patterns-and-recipes.md +555 -0
- package/skills/buildkite-api/SKILL.md +285 -0
- package/skills/buildkite-api/agents/openai.yaml +6 -0
- package/skills/buildkite-api/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-api/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-api/references/graphql-reference.md +195 -0
- package/skills/buildkite-api/references/patterns.md +44 -0
- package/skills/buildkite-api/references/webhooks.md +161 -0
- package/skills/buildkite-cli/SKILL.md +379 -0
- package/skills/buildkite-cli/agents/openai.yaml +6 -0
- package/skills/buildkite-cli/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-cli/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-cli/references/command-reference.md +181 -0
- package/skills/buildkite-migration/SKILL.md +182 -0
- package/skills/buildkite-pipelines/SKILL.md +464 -0
- package/skills/buildkite-pipelines/agents/openai.yaml +6 -0
- package/skills/buildkite-pipelines/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-pipelines/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-pipelines/examples/basic-pipeline.yml +24 -0
- package/skills/buildkite-pipelines/examples/optimized-pipeline.yml +100 -0
- package/skills/buildkite-pipelines/references/advanced-patterns.md +286 -0
- package/skills/buildkite-pipelines/references/retry-and-error-codes.md +131 -0
- package/skills/buildkite-pipelines/references/step-types-reference.md +225 -0
- package/skills/buildkite-secure-delivery/SKILL.md +168 -0
- package/skills/buildkite-secure-delivery/agents/openai.yaml +6 -0
- package/skills/buildkite-secure-delivery/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-secure-delivery/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-secure-delivery/references/oidc-cloud-providers.md +83 -0
- package/skills/buildkite-secure-delivery/references/package-publishing.md +100 -0
- package/skills/buildkite-test-engine/SKILL.md +239 -0
- package/skills/buildkite-test-engine/agents/openai.yaml +6 -0
- package/skills/buildkite-test-engine/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-test-engine/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-test-engine/examples/bktec-splitting.yml +16 -0
- package/skills/buildkite-test-engine/examples/collector-pipeline.yml +11 -0
- package/skills/buildkite-test-engine/references/collectors.md +198 -0
- package/skills/buildkite-test-engine/references/splitting-examples.md +93 -0
- package/skills/docx/LICENSE.txt +30 -0
- package/skills/docx/SKILL.md +590 -0
- package/skills/docx/VENDORED.md +32 -0
- package/skills/docx/scripts/__init__.py +1 -0
- package/skills/docx/scripts/accept_changes.py +135 -0
- package/skills/docx/scripts/comment.py +318 -0
- package/skills/docx/scripts/office/helpers/__init__.py +0 -0
- package/skills/docx/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/docx/scripts/office/pack.py +159 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/docx/scripts/office/soffice.py +183 -0
- package/skills/docx/scripts/office/unpack.py +132 -0
- package/skills/docx/scripts/office/validate.py +111 -0
- package/skills/docx/scripts/office/validators/__init__.py +15 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/base.py +847 -0
- package/skills/docx/scripts/office/validators/docx.py +446 -0
- package/skills/docx/scripts/office/validators/pptx.py +275 -0
- package/skills/docx/scripts/office/validators/redlining.py +247 -0
- package/skills/docx/scripts/templates/comments.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/skills/docx/scripts/templates/people.xml +3 -0
- package/skills/file-bug/SKILL.md +129 -0
- package/skills/humanizer/LICENSE +21 -0
- package/skills/humanizer/SKILL.md +559 -0
- package/skills/humanizer/VENDORED.md +38 -0
- package/skills/humanizer-calibrate/SKILL.md +144 -0
- package/skills/mcp-builder/LICENSE.txt +202 -0
- package/skills/mcp-builder/SKILL.md +236 -0
- package/skills/mcp-builder/VENDORED.md +32 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/pdf/LICENSE.txt +30 -0
- package/skills/pdf/SKILL.md +314 -0
- package/skills/pdf/VENDORED.md +32 -0
- package/skills/pdf/forms.md +294 -0
- package/skills/pdf/reference.md +612 -0
- package/skills/pdf/scripts/check_bounding_boxes.py +65 -0
- package/skills/pdf/scripts/check_fillable_fields.py +11 -0
- package/skills/pdf/scripts/convert_pdf_to_images.py +33 -0
- package/skills/pdf/scripts/create_validation_image.py +37 -0
- package/skills/pdf/scripts/extract_form_field_info.py +122 -0
- package/skills/pdf/scripts/extract_form_structure.py +115 -0
- package/skills/pdf/scripts/fill_fillable_fields.py +98 -0
- package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/skills/pptx/LICENSE.txt +30 -0
- package/skills/pptx/SKILL.md +232 -0
- package/skills/pptx/VENDORED.md +32 -0
- package/skills/pptx/editing.md +205 -0
- package/skills/pptx/pptxgenjs.md +420 -0
- package/skills/pptx/scripts/__init__.py +0 -0
- package/skills/pptx/scripts/add_slide.py +195 -0
- package/skills/pptx/scripts/clean.py +286 -0
- package/skills/pptx/scripts/office/helpers/__init__.py +0 -0
- package/skills/pptx/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/pptx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/pptx/scripts/office/pack.py +159 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/pptx/scripts/office/soffice.py +183 -0
- package/skills/pptx/scripts/office/unpack.py +132 -0
- package/skills/pptx/scripts/office/validate.py +111 -0
- package/skills/pptx/scripts/office/validators/__init__.py +15 -0
- package/skills/pptx/scripts/office/validators/base.py +847 -0
- package/skills/pptx/scripts/office/validators/docx.py +446 -0
- package/skills/pptx/scripts/office/validators/pptx.py +275 -0
- package/skills/pptx/scripts/office/validators/redlining.py +247 -0
- package/skills/pptx/scripts/thumbnail.py +289 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/SKILL.md +485 -0
- package/skills/skill-creator/VENDORED.md +32 -0
- package/skills/skill-creator/agents/analyzer.md +274 -0
- package/skills/skill-creator/agents/comparator.md +202 -0
- package/skills/skill-creator/agents/grader.md +223 -0
- package/skills/skill-creator/assets/eval_review.html +146 -0
- package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/skill-creator/references/schemas.md +430 -0
- package/skills/skill-creator/scripts/__init__.py +0 -0
- package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/skill-creator/scripts/generate_report.py +326 -0
- package/skills/skill-creator/scripts/improve_description.py +247 -0
- package/skills/skill-creator/scripts/package_skill.py +136 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/run_eval.py +310 -0
- package/skills/skill-creator/scripts/run_loop.py +328 -0
- package/skills/skill-creator/scripts/utils.py +47 -0
- package/skills/switchroom-architecture/SKILL.md +60 -0
- package/skills/switchroom-architecture/cascade.md +112 -0
- package/skills/switchroom-architecture/sub-agents.md +87 -0
- package/skills/switchroom-architecture/telegram.md +94 -0
- package/skills/switchroom-cli/SKILL.md +274 -0
- package/skills/switchroom-health/SKILL.md +101 -0
- package/skills/switchroom-install/SKILL.md +116 -0
- package/skills/switchroom-manage/SKILL.md +90 -0
- package/skills/switchroom-status/SKILL.md +69 -0
- package/skills/switchroom-status/scripts/status.sh +69 -0
- package/skills/telegram-test-harness/SKILL.md +191 -0
- package/skills/token-helpers/SKILL.md +73 -0
- package/skills/token-helpers/scripts/google-cal-token.sh +62 -0
- package/skills/token-helpers/scripts/ms-graph-token.sh +70 -0
- package/skills/webapp-testing/LICENSE.txt +202 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/VENDORED.md +32 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/skills/xlsx/LICENSE.txt +30 -0
- package/skills/xlsx/SKILL.md +292 -0
- package/skills/xlsx/VENDORED.md +32 -0
- package/skills/xlsx/scripts/office/helpers/__init__.py +0 -0
- package/skills/xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/xlsx/scripts/office/pack.py +159 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/xlsx/scripts/office/soffice.py +183 -0
- package/skills/xlsx/scripts/office/unpack.py +132 -0
- package/skills/xlsx/scripts/office/validate.py +111 -0
- package/skills/xlsx/scripts/office/validators/__init__.py +15 -0
- package/skills/xlsx/scripts/office/validators/base.py +847 -0
- package/skills/xlsx/scripts/office/validators/docx.py +446 -0
- package/skills/xlsx/scripts/office/validators/pptx.py +275 -0
- package/skills/xlsx/scripts/office/validators/redlining.py +247 -0
- package/skills/xlsx/scripts/recalc.py +184 -0
- package/telegram-plugin/.claude-plugin/plugin.json +20 -0
- package/telegram-plugin/.mcp.json +14 -0
- package/telegram-plugin/LICENSE +21 -0
- package/telegram-plugin/README.md +352 -0
- package/telegram-plugin/active-pins-sweep.ts +204 -0
- package/telegram-plugin/active-pins.ts +146 -0
- package/telegram-plugin/active-reactions-sweep.ts +79 -0
- package/telegram-plugin/active-reactions.ts +134 -0
- package/telegram-plugin/admin-commands/dispatch.test.ts +149 -0
- package/telegram-plugin/admin-commands/index.ts +106 -0
- package/telegram-plugin/answer-stream.ts +565 -0
- package/telegram-plugin/ask-user.ts +179 -0
- package/telegram-plugin/attachment-path.ts +80 -0
- package/telegram-plugin/auth-code-redact.ts +83 -0
- package/telegram-plugin/auth-dashboard.ts +1104 -0
- package/telegram-plugin/auth-slot-parser.ts +497 -0
- package/telegram-plugin/auto-fallback-dispatcher.ts +68 -0
- package/telegram-plugin/auto-fallback.ts +348 -0
- package/telegram-plugin/bridge/bridge.ts +687 -0
- package/telegram-plugin/bridge/ipc-client.ts +326 -0
- package/telegram-plugin/bun.lock +218 -0
- package/telegram-plugin/card-format.ts +62 -0
- package/telegram-plugin/channel-envelope-safety.test.ts +56 -0
- package/telegram-plugin/channel-envelope-safety.ts +56 -0
- package/telegram-plugin/chat-lock.ts +65 -0
- package/telegram-plugin/context-exhaustion.ts +38 -0
- package/telegram-plugin/credits-watch.ts +220 -0
- package/telegram-plugin/dist/bridge/bridge.js +24758 -0
- package/telegram-plugin/dist/foreman/foreman.js +30723 -0
- package/telegram-plugin/dist/gateway/gateway.js +46497 -0
- package/telegram-plugin/dist/server.js +24551 -0
- package/telegram-plugin/dm-command-gate.ts +56 -0
- package/telegram-plugin/docs/gateway-server-split.md +133 -0
- package/telegram-plugin/docs/multi-agent-card-design.md +847 -0
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +144 -0
- package/telegram-plugin/docs/stream-json-daemon-mode.md +477 -0
- package/telegram-plugin/docs/waiting-ux-spec.md +233 -0
- package/telegram-plugin/draft-stream.ts +442 -0
- package/telegram-plugin/draft-transport.ts +72 -0
- package/telegram-plugin/first-paint.ts +246 -0
- package/telegram-plugin/fleet-state.ts +246 -0
- package/telegram-plugin/foreman/foreman-create-flow.ts +202 -0
- package/telegram-plugin/foreman/foreman-handlers.ts +493 -0
- package/telegram-plugin/foreman/foreman.ts +1130 -0
- package/telegram-plugin/foreman/setup-flow.ts +345 -0
- package/telegram-plugin/foreman/setup-state.ts +239 -0
- package/telegram-plugin/foreman/state.ts +203 -0
- package/telegram-plugin/format.ts +685 -0
- package/telegram-plugin/gateway/access-validator.test.ts +95 -0
- package/telegram-plugin/gateway/access-validator.ts +37 -0
- package/telegram-plugin/gateway/boot-card.ts +582 -0
- package/telegram-plugin/gateway/boot-probes.ts +863 -0
- package/telegram-plugin/gateway/boot-reason.ts +51 -0
- package/telegram-plugin/gateway/boot-sweep-filter.test.ts +54 -0
- package/telegram-plugin/gateway/boot-sweep-filter.ts +32 -0
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +183 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +109 -0
- package/telegram-plugin/gateway/gateway.ts +10202 -0
- package/telegram-plugin/gateway/inbound-coalesce.ts +147 -0
- package/telegram-plugin/gateway/inject-handler.test.ts +221 -0
- package/telegram-plugin/gateway/inject-handler.ts +190 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +151 -0
- package/telegram-plugin/gateway/ipc-server.ts +494 -0
- package/telegram-plugin/gateway/pid-file.ts +103 -0
- package/telegram-plugin/gateway/poll-health.ts +156 -0
- package/telegram-plugin/gateway/preamble-suppressor.ts +154 -0
- package/telegram-plugin/gateway/quota-cache.ts +125 -0
- package/telegram-plugin/gateway/resolve-calling-subagent.ts +78 -0
- package/telegram-plugin/gateway/restart-watchdog.ts +200 -0
- package/telegram-plugin/gateway/session-marker.ts +83 -0
- package/telegram-plugin/gateway/shutdown-drain.ts +162 -0
- package/telegram-plugin/gateway/startup-mutex.ts +285 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +142 -0
- package/telegram-plugin/gateway/turn-active-marker.ts +176 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +78 -0
- package/telegram-plugin/handoff-continuity.ts +200 -0
- package/telegram-plugin/history.ts +468 -0
- package/telegram-plugin/hooks/hooks.json +58 -0
- package/telegram-plugin/hooks/secret-guard-pretool.mjs +208 -0
- package/telegram-plugin/hooks/secret-scrub-stop.mjs +98 -0
- package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +111 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +296 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +261 -0
- package/telegram-plugin/html-sanitize.ts +244 -0
- package/telegram-plugin/idle-footer.ts +65 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +166 -0
- package/telegram-plugin/interrupt-marker.ts +66 -0
- package/telegram-plugin/issues-card.ts +371 -0
- package/telegram-plugin/issues-watcher.ts +125 -0
- package/telegram-plugin/model-unavailable.ts +325 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/operator-events-history.ts +94 -0
- package/telegram-plugin/operator-events.fixtures.json +161 -0
- package/telegram-plugin/operator-events.ts +421 -0
- package/telegram-plugin/package.json +55 -0
- package/telegram-plugin/permission-rule.ts +133 -0
- package/telegram-plugin/permission-title.ts +117 -0
- package/telegram-plugin/pin-event-log.ts +76 -0
- package/telegram-plugin/plugin-logger.ts +136 -0
- package/telegram-plugin/progress-card-driver.ts +2697 -0
- package/telegram-plugin/progress-card-pin-manager.ts +589 -0
- package/telegram-plugin/progress-card-pin-watchdog.ts +98 -0
- package/telegram-plugin/progress-card.ts +1409 -0
- package/telegram-plugin/pty-partial-handler.ts +247 -0
- package/telegram-plugin/pty-tail.ts +730 -0
- package/telegram-plugin/quota-check.ts +474 -0
- package/telegram-plugin/recent-outbound-dedup.ts +169 -0
- package/telegram-plugin/registry/api-registry.test.ts +201 -0
- package/telegram-plugin/registry/subagents-bugs.test.ts +454 -0
- package/telegram-plugin/registry/subagents-schema.ts +509 -0
- package/telegram-plugin/registry/subagents.test.ts +476 -0
- package/telegram-plugin/registry/turns-schema.test.ts +101 -0
- package/telegram-plugin/registry/turns-schema.ts +417 -0
- package/telegram-plugin/retry-api-call.ts +172 -0
- package/telegram-plugin/scripts/build.mjs +78 -0
- package/telegram-plugin/secret-detect/audit.ts +66 -0
- package/telegram-plugin/secret-detect/chunker.ts +37 -0
- package/telegram-plugin/secret-detect/entropy.ts +20 -0
- package/telegram-plugin/secret-detect/gitleaks-loader.ts +74 -0
- package/telegram-plugin/secret-detect/gitleaks.toml +27 -0
- package/telegram-plugin/secret-detect/index.ts +218 -0
- package/telegram-plugin/secret-detect/kv-scanner.ts +60 -0
- package/telegram-plugin/secret-detect/mask.ts +13 -0
- package/telegram-plugin/secret-detect/patterns.ts +115 -0
- package/telegram-plugin/secret-detect/pipeline.ts +144 -0
- package/telegram-plugin/secret-detect/rewrite.ts +26 -0
- package/telegram-plugin/secret-detect/secretlint-source.ts +95 -0
- package/telegram-plugin/secret-detect/slug.ts +44 -0
- package/telegram-plugin/secret-detect/staging.ts +85 -0
- package/telegram-plugin/secret-detect/suppressor.ts +34 -0
- package/telegram-plugin/secret-detect/url-redact.ts +60 -0
- package/telegram-plugin/secret-detect/vault-write.ts +56 -0
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/server.ts +171 -0
- package/telegram-plugin/session-tail.ts +884 -0
- package/telegram-plugin/shared/bot-runtime.ts +324 -0
- package/telegram-plugin/silent-reply.ts +58 -0
- package/telegram-plugin/slot-banner-driver.ts +147 -0
- package/telegram-plugin/slot-banner.ts +86 -0
- package/telegram-plugin/start.js +26 -0
- package/telegram-plugin/startup-reset.ts +45 -0
- package/telegram-plugin/status-reactions.ts +332 -0
- package/telegram-plugin/steering.ts +155 -0
- package/telegram-plugin/sticker-aliases.ts +249 -0
- package/telegram-plugin/stream-controller.ts +311 -0
- package/telegram-plugin/stream-reply-handler.ts +664 -0
- package/telegram-plugin/streaming-metrics.ts +134 -0
- package/telegram-plugin/streaming-report.ts +204 -0
- package/telegram-plugin/subagent-watcher.ts +880 -0
- package/telegram-plugin/telegram-button-constraints.ts +191 -0
- package/telegram-plugin/telegraph.ts +381 -0
- package/telegram-plugin/tests/HARNESS.md +340 -0
- package/telegram-plugin/tests/_progress-card-harness.ts +105 -0
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +211 -0
- package/telegram-plugin/tests/active-pins-sweep.test.ts +309 -0
- package/telegram-plugin/tests/active-pins.test.ts +187 -0
- package/telegram-plugin/tests/active-reactions-sweep.test.ts +116 -0
- package/telegram-plugin/tests/active-reactions.test.ts +198 -0
- package/telegram-plugin/tests/answer-stream-dedup.test.ts +352 -0
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +236 -0
- package/telegram-plugin/tests/answer-stream.test.ts +878 -0
- package/telegram-plugin/tests/ask-user.test.ts +203 -0
- package/telegram-plugin/tests/attachment-path.test.ts +199 -0
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +118 -0
- package/telegram-plugin/tests/auth-code-auto-capture.test.ts +144 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +248 -0
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +260 -0
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +140 -0
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +559 -0
- package/telegram-plugin/tests/auth-dashboard.test.ts +1045 -0
- package/telegram-plugin/tests/auth-login-url-button.test.ts +122 -0
- package/telegram-plugin/tests/auth-slot-commands.test.ts +640 -0
- package/telegram-plugin/tests/auto-fallback-dispatcher.e2e.test.ts +183 -0
- package/telegram-plugin/tests/auto-fallback.test.ts +381 -0
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +137 -0
- package/telegram-plugin/tests/boot-card-dedupe.test.ts +154 -0
- package/telegram-plugin/tests/boot-card-probe-target.test.ts +194 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +103 -0
- package/telegram-plugin/tests/boot-card-render.test.ts +219 -0
- package/telegram-plugin/tests/boot-probes.test.ts +451 -0
- package/telegram-plugin/tests/bot-api.harness.ts +116 -0
- package/telegram-plugin/tests/bot-runtime.test.ts +190 -0
- package/telegram-plugin/tests/bridge-anonymous-refuse.test.ts +60 -0
- package/telegram-plugin/tests/context-exhaustion.test.ts +114 -0
- package/telegram-plugin/tests/credits-watch.test.ts +221 -0
- package/telegram-plugin/tests/dm-command-gate.test.ts +176 -0
- package/telegram-plugin/tests/draft-stream.test.ts +752 -0
- package/telegram-plugin/tests/draft-transport.test.ts +141 -0
- package/telegram-plugin/tests/e2e.test.ts +436 -0
- package/telegram-plugin/tests/fake-bot-api.test.ts +213 -0
- package/telegram-plugin/tests/fake-bot-api.ts +617 -0
- package/telegram-plugin/tests/false-restart-banner.test.ts +253 -0
- package/telegram-plugin/tests/first-paint.test.ts +257 -0
- package/telegram-plugin/tests/fixtures/pty-tail-tmux-fragment.bin +6 -0
- package/telegram-plugin/tests/fixtures/service-log-current-claude-code.bin +3624 -0
- package/telegram-plugin/tests/fleet-state-watcher.test.ts +101 -0
- package/telegram-plugin/tests/fleet-state.test.ts +185 -0
- package/telegram-plugin/tests/foreman-create-flow.test.ts +359 -0
- package/telegram-plugin/tests/foreman-handlers.test.ts +347 -0
- package/telegram-plugin/tests/foreman-state.test.ts +164 -0
- package/telegram-plugin/tests/foreman-write-ops.test.ts +214 -0
- package/telegram-plugin/tests/gateway-409-retry-banner.test.ts +173 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +72 -0
- package/telegram-plugin/tests/gateway-bridge.test.ts +811 -0
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +414 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +144 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +133 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +103 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +127 -0
- package/telegram-plugin/tests/gateway-startup-mutex.test.ts +284 -0
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +185 -0
- package/telegram-plugin/tests/gateway-startup-reset.test.ts +72 -0
- package/telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts +125 -0
- package/telegram-plugin/tests/handoff-continuity.test.ts +249 -0
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +243 -0
- package/telegram-plugin/tests/harness-parse-mode-validation.test.ts +114 -0
- package/telegram-plugin/tests/history.test.ts +364 -0
- package/telegram-plugin/tests/html-balanced.ts +63 -0
- package/telegram-plugin/tests/html-sanitize.test.ts +146 -0
- package/telegram-plugin/tests/idle-footer-wiring.test.ts +88 -0
- package/telegram-plugin/tests/idle-footer.test.ts +66 -0
- package/telegram-plugin/tests/inbound-coalesce.test.ts +127 -0
- package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +150 -0
- package/telegram-plugin/tests/interrupt-marker.test.ts +126 -0
- package/telegram-plugin/tests/ipc-protocol.test.ts +218 -0
- package/telegram-plugin/tests/ipc-server-anonymous-refuse.test.ts +82 -0
- package/telegram-plugin/tests/ipc-server-client.test.ts +323 -0
- package/telegram-plugin/tests/ipc-server-race.test.ts +183 -0
- package/telegram-plugin/tests/ipc-server-validate-operator.test.ts +91 -0
- package/telegram-plugin/tests/ipc-server-validate-pty-partial.test.ts +64 -0
- package/telegram-plugin/tests/ipc-server-validate-update-placeholder.test.ts +77 -0
- package/telegram-plugin/tests/ipc-validator.test.ts +274 -0
- package/telegram-plugin/tests/issues-card.test.ts +495 -0
- package/telegram-plugin/tests/issues-watcher.test.ts +165 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +303 -0
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +159 -0
- package/telegram-plugin/tests/operator-events-history.test.ts +125 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +192 -0
- package/telegram-plugin/tests/operator-events.test.ts +331 -0
- package/telegram-plugin/tests/outbound-ordering.test.ts +293 -0
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +164 -0
- package/telegram-plugin/tests/permission-rule.test.ts +121 -0
- package/telegram-plugin/tests/permission-title.test.ts +106 -0
- package/telegram-plugin/tests/pin-event-log.test.ts +124 -0
- package/telegram-plugin/tests/plugin-logger.test.ts +97 -0
- package/telegram-plugin/tests/poll-health.test.ts +86 -0
- package/telegram-plugin/tests/preamble-suppressor.test.ts +285 -0
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +73 -0
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +272 -0
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +258 -0
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +81 -0
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +80 -0
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +215 -0
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +123 -0
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +76 -0
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +62 -0
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +84 -0
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +139 -0
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +773 -0
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +66 -0
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +64 -0
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +190 -0
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +146 -0
- package/telegram-plugin/tests/progress-update.test.ts +236 -0
- package/telegram-plugin/tests/protocol-fixtures.test.ts +59 -0
- package/telegram-plugin/tests/protocol-fixtures.ts +198 -0
- package/telegram-plugin/tests/pty-partial-handler.test.ts +326 -0
- package/telegram-plugin/tests/pty-tail-real-fixture.test.ts +114 -0
- package/telegram-plugin/tests/pty-tail-tmux-fragment.test.ts +71 -0
- package/telegram-plugin/tests/pty-tail.test.ts +525 -0
- package/telegram-plugin/tests/quota-cache.test.ts +187 -0
- package/telegram-plugin/tests/quota-check.test.ts +622 -0
- package/telegram-plugin/tests/races.test.ts +842 -0
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +123 -0
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +82 -0
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +114 -0
- package/telegram-plugin/tests/real-gateway-harness.ts +699 -0
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +313 -0
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +299 -0
- package/telegram-plugin/tests/real-gateway-spec.test.ts +487 -0
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +101 -0
- package/telegram-plugin/tests/recent-outbound-dedup.test.ts +192 -0
- package/telegram-plugin/tests/registry-turns.test.ts +432 -0
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +149 -0
- package/telegram-plugin/tests/resolve-calling-subagent.test.ts +269 -0
- package/telegram-plugin/tests/restart-watchdog.test.ts +224 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +287 -0
- package/telegram-plugin/tests/secret-detect-audit.test.ts +58 -0
- package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +83 -0
- package/telegram-plugin/tests/secret-detect-gitleaks.test.ts +32 -0
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +308 -0
- package/telegram-plugin/tests/secret-detect-pipeline.test.ts +123 -0
- package/telegram-plugin/tests/secret-detect-secretlint.test.ts +101 -0
- package/telegram-plugin/tests/secret-detect-staging.test.ts +45 -0
- package/telegram-plugin/tests/secret-detect-suppressor-no-silent-allow.test.ts +67 -0
- package/telegram-plugin/tests/secret-detect.test.ts +223 -0
- package/telegram-plugin/tests/secret-guard-pretool.test.ts +194 -0
- package/telegram-plugin/tests/send-typing-action-validation.test.ts +61 -0
- package/telegram-plugin/tests/session-tail-capped.test.ts +285 -0
- package/telegram-plugin/tests/session-tail.test.ts +524 -0
- package/telegram-plugin/tests/setup-flow.test.ts +510 -0
- package/telegram-plugin/tests/setup-state.test.ts +146 -0
- package/telegram-plugin/tests/silent-reply-guard.test.ts +122 -0
- package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +350 -0
- package/telegram-plugin/tests/slot-banner.test.ts +74 -0
- package/telegram-plugin/tests/snapshot-serializer.ts +79 -0
- package/telegram-plugin/tests/spawn-detached-cgroup-escape.test.ts +51 -0
- package/telegram-plugin/tests/status-accent.test.ts +186 -0
- package/telegram-plugin/tests/status-reactions-allowed-filter.test.ts +132 -0
- package/telegram-plugin/tests/status-reactions.test.ts +314 -0
- package/telegram-plugin/tests/steering.test.ts +282 -0
- package/telegram-plugin/tests/sticker-aliases.test.ts +232 -0
- package/telegram-plugin/tests/stream-controller-html-fallback.test.ts +127 -0
- package/telegram-plugin/tests/stream-controller.test.ts +262 -0
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +208 -0
- package/telegram-plugin/tests/stream-reply-handler.test.ts +1292 -0
- package/telegram-plugin/tests/streaming-e2e.test.ts +389 -0
- package/telegram-plugin/tests/streaming-metrics.test.ts +201 -0
- package/telegram-plugin/tests/streaming-orchestration.test.ts +756 -0
- package/telegram-plugin/tests/subagent-registry-bugs.test.ts +725 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +213 -0
- package/telegram-plugin/tests/subagent-watcher-parent-marker.test.ts +274 -0
- package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +243 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +877 -0
- package/telegram-plugin/tests/subagents-schema-init-order.test.ts +109 -0
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +116 -0
- package/telegram-plugin/tests/telegram-button-constraints.test.ts +194 -0
- package/telegram-plugin/tests/telegram-format.test.ts +1093 -0
- package/telegram-plugin/tests/telegraph.test.ts +246 -0
- package/telegram-plugin/tests/tool-labels.test.ts +383 -0
- package/telegram-plugin/tests/turn-active-marker.test.ts +195 -0
- package/telegram-plugin/tests/turn-end-regressions.test.ts +489 -0
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +218 -0
- package/telegram-plugin/tests/turn-flush-dedup-controller.test.ts +144 -0
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +78 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +189 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +107 -0
- package/telegram-plugin/tests/turns-writer.test.ts +323 -0
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +131 -0
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +120 -0
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +114 -0
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +87 -0
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +211 -0
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +62 -0
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +101 -0
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +68 -0
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +110 -0
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +128 -0
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +58 -0
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +133 -0
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +155 -0
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +117 -0
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +143 -0
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +149 -0
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +101 -0
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +114 -0
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +105 -0
- package/telegram-plugin/tests/typing-wrap.test.ts +141 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +147 -0
- package/telegram-plugin/tests/update-factory-edited-and-reactions.test.ts +108 -0
- package/telegram-plugin/tests/update-factory.ts +305 -0
- package/telegram-plugin/tests/vault-grant-wizard.test.ts +84 -0
- package/telegram-plugin/tests/vault-grants-revoke.test.ts +265 -0
- package/telegram-plugin/tests/vault-subcommands.test.ts +234 -0
- package/telegram-plugin/tests/voice-transcribe.test.ts +196 -0
- package/telegram-plugin/tests/waiting-ux-harness.ts +381 -0
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +233 -0
- package/telegram-plugin/tests/welcome-text.test.ts +407 -0
- package/telegram-plugin/tool-error-filter.ts +89 -0
- package/telegram-plugin/tool-labels.ts +330 -0
- package/telegram-plugin/tool-names.ts +53 -0
- package/telegram-plugin/turn-flush-prose-recovery.ts +40 -0
- package/telegram-plugin/turn-flush-safety.ts +109 -0
- package/telegram-plugin/turn-signal-tracker.ts +79 -0
- package/telegram-plugin/two-zone-card.ts +249 -0
- package/telegram-plugin/typing-wrap.ts +92 -0
- package/telegram-plugin/voice-transcribe.ts +166 -0
- package/telegram-plugin/welcome-text.ts +359 -0
|
@@ -0,0 +1,1292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the `stream_reply` MCP tool handler.
|
|
3
|
+
*
|
|
4
|
+
* Exercises the extracted `handleStreamReply` against the mock bot harness
|
|
5
|
+
* with realistic deps (format rendering, access check, thread resolution,
|
|
6
|
+
* handoff prefix, history record).
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
9
|
+
import {
|
|
10
|
+
handleStreamReply,
|
|
11
|
+
type StreamReplyDeps,
|
|
12
|
+
type StreamReplyState,
|
|
13
|
+
} from '../stream-reply-handler.js'
|
|
14
|
+
import type { DraftStreamHandle } from '../draft-stream.js'
|
|
15
|
+
import { markdownToHtml as realMarkdownToHtml } from '../format.js'
|
|
16
|
+
import { createMockBot, installBotResetHook, microtaskFlush } from './bot-api.harness.js'
|
|
17
|
+
import {
|
|
18
|
+
handlePtyPartialPure,
|
|
19
|
+
type PtyHandlerState,
|
|
20
|
+
} from '../pty-partial-handler.js'
|
|
21
|
+
|
|
22
|
+
function makeState(): StreamReplyState {
|
|
23
|
+
return {
|
|
24
|
+
activeDraftStreams: new Map<string, DraftStreamHandle>(),
|
|
25
|
+
activeDraftParseModes: new Map<string, 'HTML' | 'MarkdownV2' | undefined>(),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeDeps(
|
|
30
|
+
bot: ReturnType<typeof createMockBot>,
|
|
31
|
+
overrides?: Partial<StreamReplyDeps>,
|
|
32
|
+
): StreamReplyDeps {
|
|
33
|
+
return {
|
|
34
|
+
bot,
|
|
35
|
+
markdownToHtml: (t) => `<b>${t}</b>`,
|
|
36
|
+
escapeMarkdownV2: (t) => `\\${t}\\`,
|
|
37
|
+
repairEscapedWhitespace: (t) => t,
|
|
38
|
+
takeHandoffPrefix: () => '',
|
|
39
|
+
assertAllowedChat: () => {},
|
|
40
|
+
resolveThreadId: (_, explicit) => (explicit != null ? Number(explicit) : undefined),
|
|
41
|
+
disableLinkPreview: true,
|
|
42
|
+
defaultFormat: 'html',
|
|
43
|
+
logStreamingEvent: () => {},
|
|
44
|
+
endStatusReaction: () => {},
|
|
45
|
+
historyEnabled: false,
|
|
46
|
+
recordOutbound: () => {},
|
|
47
|
+
writeError: () => {},
|
|
48
|
+
throttleMs: 600,
|
|
49
|
+
...overrides,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('handleStreamReply', () => {
|
|
54
|
+
const bot = createMockBot()
|
|
55
|
+
installBotResetHook(bot)
|
|
56
|
+
|
|
57
|
+
beforeEach(() => vi.useFakeTimers())
|
|
58
|
+
afterEach(() => vi.useRealTimers())
|
|
59
|
+
|
|
60
|
+
it('first call creates stream + sends with rendered HTML text', async () => {
|
|
61
|
+
const state = makeState()
|
|
62
|
+
const deps = makeDeps(bot)
|
|
63
|
+
|
|
64
|
+
const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
|
|
65
|
+
await microtaskFlush()
|
|
66
|
+
const result = await pending
|
|
67
|
+
|
|
68
|
+
expect(result.status).toBe('updated')
|
|
69
|
+
expect(result.messageId).toBe(500)
|
|
70
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
71
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>hi</b>')
|
|
72
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.parse_mode).toBe('HTML')
|
|
73
|
+
expect(state.activeDraftStreams.size).toBe(1)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('respects format=markdownv2 — uses MDv2 escaper and parse_mode', async () => {
|
|
77
|
+
const state = makeState()
|
|
78
|
+
const deps = makeDeps(bot)
|
|
79
|
+
|
|
80
|
+
const pending = handleStreamReply(
|
|
81
|
+
{ chat_id: '1', text: 'hi', format: 'markdownv2' },
|
|
82
|
+
state,
|
|
83
|
+
deps,
|
|
84
|
+
)
|
|
85
|
+
await microtaskFlush()
|
|
86
|
+
await pending
|
|
87
|
+
|
|
88
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('\\hi\\')
|
|
89
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.parse_mode).toBe('MarkdownV2')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('respects format=text — no parse_mode, raw text', async () => {
|
|
93
|
+
const state = makeState()
|
|
94
|
+
const deps = makeDeps(bot)
|
|
95
|
+
|
|
96
|
+
const pending = handleStreamReply(
|
|
97
|
+
{ chat_id: '1', text: 'plain', format: 'text' },
|
|
98
|
+
state,
|
|
99
|
+
deps,
|
|
100
|
+
)
|
|
101
|
+
await microtaskFlush()
|
|
102
|
+
await pending
|
|
103
|
+
|
|
104
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('plain')
|
|
105
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.parse_mode).toBeUndefined()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('prepends handoff prefix on first chunk only', async () => {
|
|
109
|
+
const state = makeState()
|
|
110
|
+
const deps = makeDeps(bot, {
|
|
111
|
+
takeHandoffPrefix: vi.fn<(fmt: string) => string>(() => '↩️ '),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// First call: prefix applied
|
|
115
|
+
const p1 = handleStreamReply({ chat_id: '1', text: 'first' }, state, deps)
|
|
116
|
+
await microtaskFlush()
|
|
117
|
+
await p1
|
|
118
|
+
// Prefix is prepended AFTER format rendering (it's already format-safe
|
|
119
|
+
// because takeHandoffPrefix takes the format tag).
|
|
120
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('↩️ <b>first</b>')
|
|
121
|
+
|
|
122
|
+
// Second call: handoff not consumed again
|
|
123
|
+
vi.advanceTimersByTime(1000)
|
|
124
|
+
const p2 = handleStreamReply({ chat_id: '1', text: 'second' }, state, deps)
|
|
125
|
+
await microtaskFlush()
|
|
126
|
+
await p2
|
|
127
|
+
expect(bot.api.editMessageText.mock.calls[0][2]).toBe('<b>second</b>')
|
|
128
|
+
expect(deps.takeHandoffPrefix).toHaveBeenCalledTimes(1)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('throws when text exceeds 4096 (no silent id:pending)', async () => {
|
|
132
|
+
// Pins the bug found in prod: a >4096-char text would hit draft-
|
|
133
|
+
// stream's length guard, silently stop, and the handler would return
|
|
134
|
+
// status:finalized, messageId:null — the MCP response read
|
|
135
|
+
// "finalized (id: pending)" looking like success. Fixed upstream by
|
|
136
|
+
// an over-limit pre-check that throws BEFORE touching stream state,
|
|
137
|
+
// so both first-send-over-limit AND mid-stream-over-limit fail loudly
|
|
138
|
+
// instead of corrupting the stream. done=true not required.
|
|
139
|
+
const state = makeState()
|
|
140
|
+
const deps = makeDeps(bot)
|
|
141
|
+
const tooLong = 'x'.repeat(5000)
|
|
142
|
+
|
|
143
|
+
await expect(
|
|
144
|
+
handleStreamReply({ chat_id: '1', text: tooLong, done: true }, state, deps),
|
|
145
|
+
).rejects.toThrow(/exceeds Telegram's 4096-char limit/)
|
|
146
|
+
|
|
147
|
+
// Mock bot should NOT have received any sendMessage call.
|
|
148
|
+
expect(bot.api.sendMessage).not.toHaveBeenCalled()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('mid-stream over-limit throws without corrupting stream state', async () => {
|
|
152
|
+
// A stream that starts small but a later update() goes over 4096.
|
|
153
|
+
// Before the upfront length check, the draft-stream would set its
|
|
154
|
+
// internal stopped=true flag and silently drop all further text —
|
|
155
|
+
// including the done=true final answer. The pre-check now throws
|
|
156
|
+
// on the over-limit call, leaving the stream intact so the caller
|
|
157
|
+
// can fall back to `reply`. The previously-sent short text stays
|
|
158
|
+
// visible in Telegram; the throw is the signal to the caller.
|
|
159
|
+
const state = makeState()
|
|
160
|
+
const deps = makeDeps(bot)
|
|
161
|
+
|
|
162
|
+
await handleStreamReply(
|
|
163
|
+
{ chat_id: '1', text: 'short' },
|
|
164
|
+
state,
|
|
165
|
+
deps,
|
|
166
|
+
)
|
|
167
|
+
await microtaskFlush()
|
|
168
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
169
|
+
|
|
170
|
+
// Second call: now over limit.
|
|
171
|
+
await expect(
|
|
172
|
+
handleStreamReply(
|
|
173
|
+
{ chat_id: '1', text: 'y'.repeat(5000), done: true },
|
|
174
|
+
state,
|
|
175
|
+
deps,
|
|
176
|
+
),
|
|
177
|
+
).rejects.toThrow(/exceeds Telegram's 4096-char limit/)
|
|
178
|
+
|
|
179
|
+
// No additional API calls from the rejected update.
|
|
180
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('done=true finalizes and fires terminal 👍 on default lane after finalize resolves', async () => {
|
|
184
|
+
// Bug Z fix: stream_reply(done=true) on the default (unnamed) lane
|
|
185
|
+
// now fires endStatusReaction('done') AFTER stream.finalize()
|
|
186
|
+
// resolves. This ties the 👍 emoji to actual Telegram delivery
|
|
187
|
+
// (the final draft edit landing) rather than to JSONL turn_end
|
|
188
|
+
// (which races the disconnect-flush and the dedup-suppress paths).
|
|
189
|
+
//
|
|
190
|
+
// Previously this test asserted endStatusReaction was NOT called,
|
|
191
|
+
// and the gateway turn_end handler was the sole 👍 emitter. That
|
|
192
|
+
// design left 👍 firing off either (a) a 500ms-lagged read of
|
|
193
|
+
// local history (turn-flush dedup branch), or (b) a disconnect
|
|
194
|
+
// event that may have fired before any verification of delivery.
|
|
195
|
+
const state = makeState()
|
|
196
|
+
const endStatusReaction = vi.fn()
|
|
197
|
+
const deps = makeDeps(bot, { endStatusReaction })
|
|
198
|
+
|
|
199
|
+
const pending = handleStreamReply(
|
|
200
|
+
{ chat_id: '1', text: 'final', done: true },
|
|
201
|
+
state,
|
|
202
|
+
deps,
|
|
203
|
+
)
|
|
204
|
+
await microtaskFlush()
|
|
205
|
+
const result = await pending
|
|
206
|
+
|
|
207
|
+
expect(result.status).toBe('finalized')
|
|
208
|
+
expect(state.activeDraftStreams.size).toBe(0)
|
|
209
|
+
expect(endStatusReaction).toHaveBeenCalledTimes(1)
|
|
210
|
+
expect(endStatusReaction).toHaveBeenCalledWith('1', undefined, 'done')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('done=true on a named lane does NOT fire terminal 👍', async () => {
|
|
214
|
+
// Named lanes (lane:'progress', lane:'thinking', lane:'activity'
|
|
215
|
+
// etc.) are internal driver emits, not user-visible answers. They
|
|
216
|
+
// must not be allowed to claim turn-completion: a progress-lane
|
|
217
|
+
// emit firing setDone would race the actual answer message.
|
|
218
|
+
const state = makeState()
|
|
219
|
+
const endStatusReaction = vi.fn()
|
|
220
|
+
const deps = makeDeps(bot, { endStatusReaction })
|
|
221
|
+
|
|
222
|
+
const pending = handleStreamReply(
|
|
223
|
+
{ chat_id: '1', text: 'progress snapshot', done: true, lane: 'progress' },
|
|
224
|
+
state,
|
|
225
|
+
deps,
|
|
226
|
+
)
|
|
227
|
+
await microtaskFlush()
|
|
228
|
+
await pending
|
|
229
|
+
|
|
230
|
+
expect(endStatusReaction).not.toHaveBeenCalled()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('done=true does NOT fire 👍 if finalize never produced a messageId', async () => {
|
|
234
|
+
// The over-limit branch throws before getMessageId() is non-null.
|
|
235
|
+
// Even if it didn't throw, a null messageId means the initial send
|
|
236
|
+
// never landed, so 👍 must not fire. Pinning that the gating on
|
|
237
|
+
// `getMessageId() != null` holds.
|
|
238
|
+
const state = makeState()
|
|
239
|
+
const endStatusReaction = vi.fn()
|
|
240
|
+
const deps = makeDeps(bot, { endStatusReaction })
|
|
241
|
+
|
|
242
|
+
await expect(
|
|
243
|
+
handleStreamReply(
|
|
244
|
+
{ chat_id: '1', text: 'x'.repeat(5000), done: true },
|
|
245
|
+
state,
|
|
246
|
+
deps,
|
|
247
|
+
),
|
|
248
|
+
).rejects.toThrow(/exceeds Telegram's 4096-char limit/)
|
|
249
|
+
|
|
250
|
+
expect(endStatusReaction).not.toHaveBeenCalled()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('done=true with historyEnabled records the final message row', async () => {
|
|
254
|
+
const state = makeState()
|
|
255
|
+
const recordOutbound = vi.fn()
|
|
256
|
+
const deps = makeDeps(bot, {
|
|
257
|
+
historyEnabled: true,
|
|
258
|
+
recordOutbound,
|
|
259
|
+
resolveThreadId: () => 42,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const pending = handleStreamReply(
|
|
263
|
+
{ chat_id: '1', text: 'final text', done: true, message_thread_id: '42' },
|
|
264
|
+
state,
|
|
265
|
+
deps,
|
|
266
|
+
)
|
|
267
|
+
await microtaskFlush()
|
|
268
|
+
await pending
|
|
269
|
+
|
|
270
|
+
expect(recordOutbound).toHaveBeenCalledWith({
|
|
271
|
+
chat_id: '1',
|
|
272
|
+
thread_id: 42,
|
|
273
|
+
message_ids: [500],
|
|
274
|
+
texts: ['final text'], // raw text, NOT HTML-rendered
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('historyEnabled=false skips recordOutbound', async () => {
|
|
279
|
+
const state = makeState()
|
|
280
|
+
const recordOutbound = vi.fn()
|
|
281
|
+
const deps = makeDeps(bot, { historyEnabled: false, recordOutbound })
|
|
282
|
+
|
|
283
|
+
const pending = handleStreamReply(
|
|
284
|
+
{ chat_id: '1', text: 'f', done: true },
|
|
285
|
+
state,
|
|
286
|
+
deps,
|
|
287
|
+
)
|
|
288
|
+
await microtaskFlush()
|
|
289
|
+
await pending
|
|
290
|
+
|
|
291
|
+
expect(recordOutbound).not.toHaveBeenCalled()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('recordOutbound throws → error logged, handler still resolves finalized', async () => {
|
|
295
|
+
const state = makeState()
|
|
296
|
+
const writeError = vi.fn()
|
|
297
|
+
const recordOutbound = vi.fn(() => {
|
|
298
|
+
throw new Error('db locked')
|
|
299
|
+
})
|
|
300
|
+
const deps = makeDeps(bot, { historyEnabled: true, recordOutbound, writeError })
|
|
301
|
+
|
|
302
|
+
const pending = handleStreamReply(
|
|
303
|
+
{ chat_id: '1', text: 'f', done: true },
|
|
304
|
+
state,
|
|
305
|
+
deps,
|
|
306
|
+
)
|
|
307
|
+
await microtaskFlush()
|
|
308
|
+
const result = await pending
|
|
309
|
+
|
|
310
|
+
expect(result.status).toBe('finalized')
|
|
311
|
+
expect(writeError).toHaveBeenCalledTimes(1)
|
|
312
|
+
expect(writeError.mock.calls[0][0]).toMatch(/db locked/)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('rejects when assertAllowedChat throws', async () => {
|
|
316
|
+
const state = makeState()
|
|
317
|
+
const deps = makeDeps(bot, {
|
|
318
|
+
assertAllowedChat: () => { throw new Error('chat not allowed') },
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
await expect(
|
|
322
|
+
handleStreamReply({ chat_id: 'evil', text: 'x' }, state, deps),
|
|
323
|
+
).rejects.toThrow('chat not allowed')
|
|
324
|
+
|
|
325
|
+
expect(bot.api.sendMessage).not.toHaveBeenCalled()
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('subsequent calls reuse the same stream + edit in place', async () => {
|
|
329
|
+
const state = makeState()
|
|
330
|
+
const deps = makeDeps(bot)
|
|
331
|
+
|
|
332
|
+
const p1 = handleStreamReply({ chat_id: '1', text: 'step 1' }, state, deps)
|
|
333
|
+
await microtaskFlush()
|
|
334
|
+
await p1
|
|
335
|
+
vi.advanceTimersByTime(1000)
|
|
336
|
+
|
|
337
|
+
const p2 = handleStreamReply({ chat_id: '1', text: 'step 2' }, state, deps)
|
|
338
|
+
await microtaskFlush()
|
|
339
|
+
await p2
|
|
340
|
+
|
|
341
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
342
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
343
|
+
expect(bot.api.editMessageText.mock.calls[0][1]).toBe(500) // same id
|
|
344
|
+
expect(bot.api.editMessageText.mock.calls[0][2]).toBe('<b>step 2</b>')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('passes repairEscapedWhitespace through before rendering', async () => {
|
|
348
|
+
const state = makeState()
|
|
349
|
+
const deps = makeDeps(bot, {
|
|
350
|
+
repairEscapedWhitespace: (t) => t.replace(/\\n/g, '\n'),
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const pending = handleStreamReply({ chat_id: '1', text: 'a\\nb' }, state, deps)
|
|
354
|
+
await microtaskFlush()
|
|
355
|
+
await pending
|
|
356
|
+
|
|
357
|
+
// repair happens first; then markdownToHtml wraps the repaired text
|
|
358
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>a\nb</b>')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('different lanes for same chat produce independent Telegram messages', async () => {
|
|
362
|
+
const state = makeState()
|
|
363
|
+
const deps = makeDeps(bot)
|
|
364
|
+
|
|
365
|
+
const p1 = handleStreamReply(
|
|
366
|
+
{ chat_id: '1', text: 'thinking aloud', lane: 'thinking' },
|
|
367
|
+
state,
|
|
368
|
+
deps,
|
|
369
|
+
)
|
|
370
|
+
await microtaskFlush()
|
|
371
|
+
const r1 = await p1
|
|
372
|
+
|
|
373
|
+
const p2 = handleStreamReply(
|
|
374
|
+
{ chat_id: '1', text: 'final answer' }, // no lane = answer
|
|
375
|
+
state,
|
|
376
|
+
deps,
|
|
377
|
+
)
|
|
378
|
+
await microtaskFlush()
|
|
379
|
+
const r2 = await p2
|
|
380
|
+
|
|
381
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
382
|
+
expect(r1.messageId).not.toBe(r2.messageId) // separate messages
|
|
383
|
+
expect(state.activeDraftStreams.size).toBe(2)
|
|
384
|
+
expect(state.activeDraftStreams.has('1:_')).toBe(true)
|
|
385
|
+
expect(state.activeDraftStreams.has('1:_:thinking')).toBe(true)
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('same lane updates the same message (no duplicate send per lane)', async () => {
|
|
389
|
+
const state = makeState()
|
|
390
|
+
const deps = makeDeps(bot)
|
|
391
|
+
|
|
392
|
+
const p1 = handleStreamReply(
|
|
393
|
+
{ chat_id: '1', text: 'step 1', lane: 'thinking' },
|
|
394
|
+
state,
|
|
395
|
+
deps,
|
|
396
|
+
)
|
|
397
|
+
await microtaskFlush()
|
|
398
|
+
await p1
|
|
399
|
+
|
|
400
|
+
vi.advanceTimersByTime(1000)
|
|
401
|
+
const p2 = handleStreamReply(
|
|
402
|
+
{ chat_id: '1', text: 'step 1 — step 2', lane: 'thinking' },
|
|
403
|
+
state,
|
|
404
|
+
deps,
|
|
405
|
+
)
|
|
406
|
+
await microtaskFlush()
|
|
407
|
+
await p2
|
|
408
|
+
|
|
409
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
410
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
411
|
+
expect(bot.api.editMessageText.mock.calls[0][2]).toBe('<b>step 1 — step 2</b>')
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('done=true on one lane does not affect other lanes', async () => {
|
|
415
|
+
const state = makeState()
|
|
416
|
+
const deps = makeDeps(bot)
|
|
417
|
+
|
|
418
|
+
const pThink = handleStreamReply(
|
|
419
|
+
{ chat_id: '1', text: 'thinking', lane: 'thinking', done: true },
|
|
420
|
+
state,
|
|
421
|
+
deps,
|
|
422
|
+
)
|
|
423
|
+
await microtaskFlush()
|
|
424
|
+
await pThink
|
|
425
|
+
|
|
426
|
+
const pAnswer = handleStreamReply(
|
|
427
|
+
{ chat_id: '1', text: 'answering' }, // still in progress
|
|
428
|
+
state,
|
|
429
|
+
deps,
|
|
430
|
+
)
|
|
431
|
+
await microtaskFlush()
|
|
432
|
+
await pAnswer
|
|
433
|
+
|
|
434
|
+
expect(state.activeDraftStreams.has('1:_:thinking')).toBe(false)
|
|
435
|
+
expect(state.activeDraftStreams.has('1:_')).toBe(true)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// ─── Regression: concurrent turns on the same chat+thread+lane ───────
|
|
439
|
+
// Before the fix, two simultaneously active turns emitting on
|
|
440
|
+
// lane:'progress' (the progress-card driver's lane) computed the same
|
|
441
|
+
// streamKey and collapsed into one draft stream. Telegram saw a single
|
|
442
|
+
// message flapping between the two turns' narratives instead of two
|
|
443
|
+
// separate pinned cards. The fix threads a per-turn `turnKey` through
|
|
444
|
+
// `StreamReplyArgs` → `streamKey()` so each active turn gets its own
|
|
445
|
+
// slot in `activeDraftStreams` (and therefore its own Telegram message
|
|
446
|
+
// and its own pin via `progressPinnedMsgIds`).
|
|
447
|
+
it('concurrent turns with different turnKeys produce separate draft streams and messages', async () => {
|
|
448
|
+
const state = makeState()
|
|
449
|
+
const deps = makeDeps(bot)
|
|
450
|
+
|
|
451
|
+
// Turn A: progress lane, turnKey "1:_:1"
|
|
452
|
+
const pA = handleStreamReply(
|
|
453
|
+
{ chat_id: '1', text: 'turn A step 1', lane: 'progress', turnKey: '1:_:1' },
|
|
454
|
+
state,
|
|
455
|
+
deps,
|
|
456
|
+
)
|
|
457
|
+
await microtaskFlush()
|
|
458
|
+
const rA = await pA
|
|
459
|
+
|
|
460
|
+
// Turn B: progress lane, same chat+thread+lane but DIFFERENT turnKey
|
|
461
|
+
const pB = handleStreamReply(
|
|
462
|
+
{ chat_id: '1', text: 'turn B step 1', lane: 'progress', turnKey: '1:_:2' },
|
|
463
|
+
state,
|
|
464
|
+
deps,
|
|
465
|
+
)
|
|
466
|
+
await microtaskFlush()
|
|
467
|
+
const rB = await pB
|
|
468
|
+
|
|
469
|
+
// Two independent Telegram messages (not one edited twice).
|
|
470
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
471
|
+
expect(bot.api.editMessageText).not.toHaveBeenCalled()
|
|
472
|
+
expect(rA.messageId).not.toBe(rB.messageId)
|
|
473
|
+
|
|
474
|
+
// Two independent draft streams in state, each keyed by turnKey.
|
|
475
|
+
expect(state.activeDraftStreams.size).toBe(2)
|
|
476
|
+
expect(state.activeDraftStreams.has('1:_:progress:1:_:1')).toBe(true)
|
|
477
|
+
expect(state.activeDraftStreams.has('1:_:progress:1:_:2')).toBe(true)
|
|
478
|
+
|
|
479
|
+
// Each message carried its own turn's text.
|
|
480
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>turn A step 1</b>')
|
|
481
|
+
expect(bot.api.sendMessage.mock.calls[1][1]).toBe('<b>turn B step 1</b>')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('subsequent updates with same turnKey reuse the stream (edit in place)', async () => {
|
|
485
|
+
const state = makeState()
|
|
486
|
+
const deps = makeDeps(bot)
|
|
487
|
+
|
|
488
|
+
const p1 = handleStreamReply(
|
|
489
|
+
{ chat_id: '1', text: 'A first', lane: 'progress', turnKey: '1:_:1' },
|
|
490
|
+
state,
|
|
491
|
+
deps,
|
|
492
|
+
)
|
|
493
|
+
await microtaskFlush()
|
|
494
|
+
await p1
|
|
495
|
+
|
|
496
|
+
vi.advanceTimersByTime(1000)
|
|
497
|
+
|
|
498
|
+
const p2 = handleStreamReply(
|
|
499
|
+
{ chat_id: '1', text: 'A first + second', lane: 'progress', turnKey: '1:_:1' },
|
|
500
|
+
state,
|
|
501
|
+
deps,
|
|
502
|
+
)
|
|
503
|
+
await microtaskFlush()
|
|
504
|
+
await p2
|
|
505
|
+
|
|
506
|
+
// One send (first call) + one edit (second call) on the same message.
|
|
507
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
508
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
509
|
+
expect(bot.api.editMessageText.mock.calls[0][1]).toBe(500)
|
|
510
|
+
expect(bot.api.editMessageText.mock.calls[0][2]).toBe('<b>A first + second</b>')
|
|
511
|
+
expect(state.activeDraftStreams.size).toBe(1)
|
|
512
|
+
expect(state.activeDraftStreams.has('1:_:progress:1:_:1')).toBe(true)
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('interleaved concurrent turns each update their own message independently', async () => {
|
|
516
|
+
const state = makeState()
|
|
517
|
+
const deps = makeDeps(bot)
|
|
518
|
+
|
|
519
|
+
// Turn A opens
|
|
520
|
+
const pa1 = handleStreamReply(
|
|
521
|
+
{ chat_id: '1', text: 'A step 1', lane: 'progress', turnKey: '1:_:1' },
|
|
522
|
+
state, deps,
|
|
523
|
+
)
|
|
524
|
+
await microtaskFlush()
|
|
525
|
+
await pa1
|
|
526
|
+
|
|
527
|
+
// Turn B opens
|
|
528
|
+
const pb1 = handleStreamReply(
|
|
529
|
+
{ chat_id: '1', text: 'B step 1', lane: 'progress', turnKey: '1:_:2' },
|
|
530
|
+
state, deps,
|
|
531
|
+
)
|
|
532
|
+
await microtaskFlush()
|
|
533
|
+
await pb1
|
|
534
|
+
|
|
535
|
+
vi.advanceTimersByTime(1000)
|
|
536
|
+
|
|
537
|
+
// Turn A updates
|
|
538
|
+
const pa2 = handleStreamReply(
|
|
539
|
+
{ chat_id: '1', text: 'A step 1 + 2', lane: 'progress', turnKey: '1:_:1' },
|
|
540
|
+
state, deps,
|
|
541
|
+
)
|
|
542
|
+
await microtaskFlush()
|
|
543
|
+
await pa2
|
|
544
|
+
|
|
545
|
+
vi.advanceTimersByTime(1000)
|
|
546
|
+
|
|
547
|
+
// Turn B updates
|
|
548
|
+
const pb2 = handleStreamReply(
|
|
549
|
+
{ chat_id: '1', text: 'B step 1 + 2', lane: 'progress', turnKey: '1:_:2' },
|
|
550
|
+
state, deps,
|
|
551
|
+
)
|
|
552
|
+
await microtaskFlush()
|
|
553
|
+
await pb2
|
|
554
|
+
|
|
555
|
+
// Two sends (one per turn), two edits (one per turn's update).
|
|
556
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
557
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(2)
|
|
558
|
+
|
|
559
|
+
// The edits must target distinct message ids — one per turn's
|
|
560
|
+
// original message — not both collapse onto the same id.
|
|
561
|
+
const editTargets = bot.api.editMessageText.mock.calls.map((c) => c[1])
|
|
562
|
+
expect(new Set(editTargets).size).toBe(2)
|
|
563
|
+
|
|
564
|
+
// And each edit carries its own turn's text — no cross-contamination.
|
|
565
|
+
const editTexts = bot.api.editMessageText.mock.calls.map((c) => c[2])
|
|
566
|
+
expect(editTexts).toContain('<b>A step 1 + 2</b>')
|
|
567
|
+
expect(editTexts).toContain('<b>B step 1 + 2</b>')
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('done=true on one turnKey does not close the other concurrent turn', async () => {
|
|
571
|
+
const state = makeState()
|
|
572
|
+
const deps = makeDeps(bot)
|
|
573
|
+
|
|
574
|
+
const pA = handleStreamReply(
|
|
575
|
+
{ chat_id: '1', text: 'A', lane: 'progress', turnKey: '1:_:1' },
|
|
576
|
+
state, deps,
|
|
577
|
+
)
|
|
578
|
+
await microtaskFlush()
|
|
579
|
+
await pA
|
|
580
|
+
|
|
581
|
+
const pB = handleStreamReply(
|
|
582
|
+
{ chat_id: '1', text: 'B', lane: 'progress', turnKey: '1:_:2' },
|
|
583
|
+
state, deps,
|
|
584
|
+
)
|
|
585
|
+
await microtaskFlush()
|
|
586
|
+
await pB
|
|
587
|
+
|
|
588
|
+
expect(state.activeDraftStreams.size).toBe(2)
|
|
589
|
+
|
|
590
|
+
// Advance past the throttle window so the finalize edit can flush
|
|
591
|
+
// instead of sitting on the debounce timer (fake timers).
|
|
592
|
+
vi.advanceTimersByTime(1000)
|
|
593
|
+
|
|
594
|
+
// Finalize turn A
|
|
595
|
+
const pAFinal = handleStreamReply(
|
|
596
|
+
{ chat_id: '1', text: 'A final', lane: 'progress', turnKey: '1:_:1', done: true },
|
|
597
|
+
state, deps,
|
|
598
|
+
)
|
|
599
|
+
await microtaskFlush()
|
|
600
|
+
await pAFinal
|
|
601
|
+
|
|
602
|
+
// Turn A's slot is gone; turn B's is still live.
|
|
603
|
+
expect(state.activeDraftStreams.has('1:_:progress:1:_:1')).toBe(false)
|
|
604
|
+
expect(state.activeDraftStreams.has('1:_:progress:1:_:2')).toBe(true)
|
|
605
|
+
expect(state.activeDraftStreams.size).toBe(1)
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it('turnKey omitted falls back to legacy chat+thread+lane key (no regression for non-progress callers)', async () => {
|
|
609
|
+
// Other lanes (default, thinking, activity) don't pass turnKey. They
|
|
610
|
+
// must still multiplex the legacy way: one stream per chat+thread+lane.
|
|
611
|
+
// This pins the backwards-compatible behavior of streamKey() when
|
|
612
|
+
// turnKey is undefined — a non-progress caller shouldn't suddenly
|
|
613
|
+
// create a new stream on every call.
|
|
614
|
+
const state = makeState()
|
|
615
|
+
const deps = makeDeps(bot)
|
|
616
|
+
|
|
617
|
+
const p1 = handleStreamReply({ chat_id: '1', text: 'a1' }, state, deps)
|
|
618
|
+
await microtaskFlush()
|
|
619
|
+
await p1
|
|
620
|
+
vi.advanceTimersByTime(1000)
|
|
621
|
+
const p2 = handleStreamReply({ chat_id: '1', text: 'a2' }, state, deps)
|
|
622
|
+
await microtaskFlush()
|
|
623
|
+
await p2
|
|
624
|
+
|
|
625
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
626
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
627
|
+
expect(state.activeDraftStreams.size).toBe(1)
|
|
628
|
+
expect(state.activeDraftStreams.has('1:_')).toBe(true)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
it('bug 1: parseMode mismatch with existing stream rotates to fresh stream with new parseMode + rendered text', async () => {
|
|
632
|
+
// Reproduces the reported bug: PTY-tail auto-stream seeds a stream
|
|
633
|
+
// with format:'text' (parseMode undefined). A later explicit
|
|
634
|
+
// stream_reply on the same key with format:'html' + markdown text
|
|
635
|
+
// must NOT inherit the stale parseMode — it must finalize the old
|
|
636
|
+
// stream and create a fresh one with parse_mode=HTML so the markdown
|
|
637
|
+
// converts to HTML tags instead of sending literal asterisks.
|
|
638
|
+
const state = makeState()
|
|
639
|
+
const deps = makeDeps(bot, {
|
|
640
|
+
markdownToHtml: realMarkdownToHtml,
|
|
641
|
+
defaultFormat: 'text',
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
// First call: PTY-tail-style, format:'text'
|
|
645
|
+
const p1 = handleStreamReply(
|
|
646
|
+
{ chat_id: '1', text: 'Running Bash: ls', format: 'text' },
|
|
647
|
+
state, deps,
|
|
648
|
+
)
|
|
649
|
+
await microtaskFlush()
|
|
650
|
+
await p1
|
|
651
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
652
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.parse_mode).toBeUndefined()
|
|
653
|
+
|
|
654
|
+
vi.advanceTimersByTime(1000)
|
|
655
|
+
|
|
656
|
+
// Second call on the same stream key: model explicitly uses html +
|
|
657
|
+
// markdown. Must produce a new send (stream rotated), parse_mode HTML,
|
|
658
|
+
// and literal markdown converted to Telegram HTML tags.
|
|
659
|
+
const p2 = handleStreamReply(
|
|
660
|
+
{ chat_id: '1', text: '**bold** and `code`', format: 'html' },
|
|
661
|
+
state, deps,
|
|
662
|
+
)
|
|
663
|
+
await microtaskFlush()
|
|
664
|
+
await p2
|
|
665
|
+
|
|
666
|
+
// A fresh stream means a second sendMessage, not an edit of the old
|
|
667
|
+
// one (the old stream was finalized + discarded).
|
|
668
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
669
|
+
const secondSend = bot.api.sendMessage.mock.calls[1]
|
|
670
|
+
expect(secondSend[2]?.parse_mode).toBe('HTML')
|
|
671
|
+
// markdownToHtml renders `**bold**` → `<b>bold</b>` and
|
|
672
|
+
// `` `code` `` → `<code>code</code>`.
|
|
673
|
+
expect(secondSend[1]).toContain('<b>bold</b>')
|
|
674
|
+
expect(secondSend[1]).toContain('<code>code</code>')
|
|
675
|
+
expect(secondSend[1]).not.toContain('**')
|
|
676
|
+
expect(secondSend[1]).not.toMatch(/`code`/)
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
// ─── Regression: PTY-tail duplicate message. Before the fix,
|
|
680
|
+
// stream_reply did not add itself to suppressPtyPreview, so a PTY
|
|
681
|
+
// partial firing after a finalized stream (TUI capture of the same
|
|
682
|
+
// assistant text) created a duplicate message with the raw TUI text
|
|
683
|
+
// and visibly escaped HTML tags. See log sequence: msg 559 finalized,
|
|
684
|
+
// then msg 560 draft_send path=pty_preview with the same content.
|
|
685
|
+
// Now stream_reply claims the suppress slot on the first call.
|
|
686
|
+
|
|
687
|
+
it('adds sKey (without lane) to suppressPtyPreview on first call', async () => {
|
|
688
|
+
const state: StreamReplyState = {
|
|
689
|
+
...makeState(),
|
|
690
|
+
suppressPtyPreview: new Set<string>(),
|
|
691
|
+
}
|
|
692
|
+
const deps = makeDeps(bot)
|
|
693
|
+
|
|
694
|
+
const pending = handleStreamReply({ chat_id: '42', text: 'hi' }, state, deps)
|
|
695
|
+
await microtaskFlush()
|
|
696
|
+
await pending
|
|
697
|
+
|
|
698
|
+
expect(state.suppressPtyPreview!.has('42:_')).toBe(true)
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it('suppression key ignores lane — claims default PTY lane', async () => {
|
|
702
|
+
const state: StreamReplyState = {
|
|
703
|
+
...makeState(),
|
|
704
|
+
suppressPtyPreview: new Set<string>(),
|
|
705
|
+
}
|
|
706
|
+
const deps = makeDeps(bot)
|
|
707
|
+
|
|
708
|
+
const pending = handleStreamReply(
|
|
709
|
+
{ chat_id: '42', text: 'hi', lane: 'thinking' },
|
|
710
|
+
state,
|
|
711
|
+
deps,
|
|
712
|
+
)
|
|
713
|
+
await microtaskFlush()
|
|
714
|
+
await pending
|
|
715
|
+
|
|
716
|
+
// The stream itself is keyed with the lane...
|
|
717
|
+
expect(state.activeDraftStreams.has('42:_:thinking')).toBe(true)
|
|
718
|
+
// ...but the PTY-suppression key is lane-less so the PTY handler
|
|
719
|
+
// (which has no concept of lanes) actually sees it as suppressed.
|
|
720
|
+
expect(state.suppressPtyPreview!.has('42:_')).toBe(true)
|
|
721
|
+
expect(state.suppressPtyPreview!.has('42:_:thinking')).toBe(false)
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
it('suppression survives done=true so late PTY partials are still dropped', async () => {
|
|
725
|
+
// This covers the exact production sequence from telegram-plugin.log:
|
|
726
|
+
// stream_reply done=true → draft_edit final → PTY partial arrives
|
|
727
|
+
// 500ms later with the TUI capture → must NOT create a new message.
|
|
728
|
+
const state: StreamReplyState = {
|
|
729
|
+
...makeState(),
|
|
730
|
+
suppressPtyPreview: new Set<string>(),
|
|
731
|
+
}
|
|
732
|
+
const deps = makeDeps(bot)
|
|
733
|
+
|
|
734
|
+
const pending = handleStreamReply(
|
|
735
|
+
{ chat_id: '42', text: 'final', done: true },
|
|
736
|
+
state,
|
|
737
|
+
deps,
|
|
738
|
+
)
|
|
739
|
+
await microtaskFlush()
|
|
740
|
+
await pending
|
|
741
|
+
|
|
742
|
+
// After done=true the stream is gone from activeDraftStreams...
|
|
743
|
+
expect(state.activeDraftStreams.has('42:_')).toBe(false)
|
|
744
|
+
// ...but the suppress slot must remain so a PTY partial landing
|
|
745
|
+
// AFTER finalize is dropped. server.ts clears this on turn_end.
|
|
746
|
+
expect(state.suppressPtyPreview!.has('42:_')).toBe(true)
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
it('end-to-end: PTY partial after stream_reply finalize is suppressed (no dup message)', async () => {
|
|
750
|
+
// Reproduces the production sequence:
|
|
751
|
+
// 1. stream_reply done=true for chat 42
|
|
752
|
+
// 2. PTY-tail fires with the TUI capture of the same assistant text
|
|
753
|
+
// 3. PTY handler sees suppress flag and drops the partial
|
|
754
|
+
// Before the fix, step 2 created a duplicate Telegram message with
|
|
755
|
+
// raw TUI text and visibly-escaped HTML tags (see log msg 559 → 560).
|
|
756
|
+
const activeDraftStreams = new Map<string, DraftStreamHandle>()
|
|
757
|
+
const suppressPtyPreview = new Set<string>()
|
|
758
|
+
const streamState: StreamReplyState = {
|
|
759
|
+
activeDraftStreams,
|
|
760
|
+
activeDraftParseModes: new Map(),
|
|
761
|
+
suppressPtyPreview,
|
|
762
|
+
}
|
|
763
|
+
const streamDeps = makeDeps(bot)
|
|
764
|
+
|
|
765
|
+
// Step 1: stream_reply finalizes.
|
|
766
|
+
const pending = handleStreamReply(
|
|
767
|
+
{ chat_id: '42', text: 'final answer', done: true },
|
|
768
|
+
streamState,
|
|
769
|
+
streamDeps,
|
|
770
|
+
)
|
|
771
|
+
await microtaskFlush()
|
|
772
|
+
await pending
|
|
773
|
+
const sendsAfterStream = bot.api.sendMessage.mock.calls.length
|
|
774
|
+
|
|
775
|
+
// Step 2: PTY partial fires into the SHARED state — same Sets/Maps.
|
|
776
|
+
const ptyState: PtyHandlerState = {
|
|
777
|
+
currentSessionChatId: '42',
|
|
778
|
+
currentSessionThreadId: undefined,
|
|
779
|
+
pendingPtyPartial: null,
|
|
780
|
+
activeDraftStreams,
|
|
781
|
+
suppressPtyPreview,
|
|
782
|
+
lastPtyPreviewByChat: new Map(),
|
|
783
|
+
}
|
|
784
|
+
const action = handlePtyPartialPure(
|
|
785
|
+
'TUI capture: <b>final answer</b>',
|
|
786
|
+
ptyState,
|
|
787
|
+
{ bot, renderText: (t) => t },
|
|
788
|
+
)
|
|
789
|
+
await microtaskFlush()
|
|
790
|
+
|
|
791
|
+
// Step 3: partial was dropped — no extra sendMessage call.
|
|
792
|
+
expect(action).toBe('suppressed')
|
|
793
|
+
expect(bot.api.sendMessage.mock.calls.length).toBe(sendsAfterStream)
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it('works without suppressPtyPreview (backwards compat)', async () => {
|
|
797
|
+
// Callers that don't thread the set through must still function.
|
|
798
|
+
const state = makeState() // no suppressPtyPreview
|
|
799
|
+
const deps = makeDeps(bot)
|
|
800
|
+
|
|
801
|
+
const pending = handleStreamReply({ chat_id: '42', text: 'hi' }, state, deps)
|
|
802
|
+
await microtaskFlush()
|
|
803
|
+
const result = await pending
|
|
804
|
+
expect(result.messageId).toBe(500)
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
it('streamExisted flag in logStreamingEvent reflects map state', async () => {
|
|
808
|
+
const state = makeState()
|
|
809
|
+
const logStreamingEvent = vi.fn()
|
|
810
|
+
const deps = makeDeps(bot, { logStreamingEvent })
|
|
811
|
+
|
|
812
|
+
const p1 = handleStreamReply({ chat_id: '1', text: 'a' }, state, deps)
|
|
813
|
+
await microtaskFlush()
|
|
814
|
+
await p1
|
|
815
|
+
vi.advanceTimersByTime(1000)
|
|
816
|
+
const p2 = handleStreamReply({ chat_id: '1', text: 'b' }, state, deps)
|
|
817
|
+
await microtaskFlush()
|
|
818
|
+
await p2
|
|
819
|
+
|
|
820
|
+
const calledEvents = logStreamingEvent.mock.calls.map(c => c[0])
|
|
821
|
+
const streamReplyCalledEvents = calledEvents.filter(
|
|
822
|
+
(e: { kind: string }) => e.kind === 'stream_reply_called',
|
|
823
|
+
)
|
|
824
|
+
expect(streamReplyCalledEvents[0].streamExisted).toBe(false)
|
|
825
|
+
expect(streamReplyCalledEvents[1].streamExisted).toBe(true)
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
describe('progressCardActive coexistence', () => {
|
|
829
|
+
// The progress card and the answer message live on different lanes
|
|
830
|
+
// (progress vs default) and render different content (tool structure
|
|
831
|
+
// vs model prose), so default-lane stream_reply(done=false) is
|
|
832
|
+
// accepted in checklist mode. The card is no longer treated as the
|
|
833
|
+
// sole mid-turn surface — it shows tool structure on its own lane
|
|
834
|
+
// while the model's progressive replies stream into the answer
|
|
835
|
+
// message. See #481.
|
|
836
|
+
it('accepts default-lane done=false when progress card is active (streams into answer)', async () => {
|
|
837
|
+
const state: StreamReplyState = {
|
|
838
|
+
...makeState(),
|
|
839
|
+
suppressPtyPreview: new Set<string>(),
|
|
840
|
+
}
|
|
841
|
+
const deps = makeDeps(bot, { progressCardActive: true })
|
|
842
|
+
|
|
843
|
+
const pending = handleStreamReply(
|
|
844
|
+
{ chat_id: '1', text: 'working...' },
|
|
845
|
+
state,
|
|
846
|
+
deps,
|
|
847
|
+
)
|
|
848
|
+
await microtaskFlush()
|
|
849
|
+
const result = await pending
|
|
850
|
+
|
|
851
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
852
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>working...</b>')
|
|
853
|
+
expect(result.status).toBe('updated')
|
|
854
|
+
// PTY-preview slot is claimed because the model is now the
|
|
855
|
+
// answer-lane surface owner — late PTY partials should defer to
|
|
856
|
+
// the model's stream rather than racing it with a parallel edit.
|
|
857
|
+
// (Same suppression pattern as before — no longer for cleanup
|
|
858
|
+
// after a rejection, but for ownership during normal streaming.)
|
|
859
|
+
expect(state.suppressPtyPreview?.has('1:_')).toBe(true)
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
it('still posts final done=true call when progress card is active', async () => {
|
|
863
|
+
const state = makeState()
|
|
864
|
+
const deps = makeDeps(bot, { progressCardActive: true })
|
|
865
|
+
|
|
866
|
+
const pending = handleStreamReply(
|
|
867
|
+
{ chat_id: '1', text: 'final answer', done: true },
|
|
868
|
+
state,
|
|
869
|
+
deps,
|
|
870
|
+
)
|
|
871
|
+
await microtaskFlush()
|
|
872
|
+
const result = await pending
|
|
873
|
+
|
|
874
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
875
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>final answer</b>')
|
|
876
|
+
expect(result.status).toBe('finalized')
|
|
877
|
+
expect(result.messageId).toBe(500)
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
it('does NOT reject named-lane calls (internal progress-card driver uses lane=progress)', async () => {
|
|
881
|
+
const state = makeState()
|
|
882
|
+
const deps = makeDeps(bot, { progressCardActive: true })
|
|
883
|
+
|
|
884
|
+
const pending = handleStreamReply(
|
|
885
|
+
{ chat_id: '1', text: 'card snapshot', lane: 'progress' },
|
|
886
|
+
state,
|
|
887
|
+
deps,
|
|
888
|
+
)
|
|
889
|
+
await microtaskFlush()
|
|
890
|
+
await pending
|
|
891
|
+
|
|
892
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
it('legacy behavior preserved when progressCardActive is false', async () => {
|
|
896
|
+
const state = makeState()
|
|
897
|
+
const deps = makeDeps(bot, { progressCardActive: false })
|
|
898
|
+
|
|
899
|
+
const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
|
|
900
|
+
await microtaskFlush()
|
|
901
|
+
await pending
|
|
902
|
+
|
|
903
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
904
|
+
})
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
describe('quote-reply default', () => {
|
|
908
|
+
it('auto-quotes the latest inbound message when reply_to is omitted', async () => {
|
|
909
|
+
const state = makeState()
|
|
910
|
+
const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
|
|
911
|
+
() => 4242,
|
|
912
|
+
)
|
|
913
|
+
const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
|
|
914
|
+
|
|
915
|
+
const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
|
|
916
|
+
await microtaskFlush()
|
|
917
|
+
await pending
|
|
918
|
+
|
|
919
|
+
expect(lookup).toHaveBeenCalledWith('1', null)
|
|
920
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toEqual({
|
|
921
|
+
message_id: 4242,
|
|
922
|
+
})
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
it('explicit reply_to overrides the auto-quote lookup', async () => {
|
|
926
|
+
const state = makeState()
|
|
927
|
+
const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
|
|
928
|
+
() => 4242,
|
|
929
|
+
)
|
|
930
|
+
const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
|
|
931
|
+
|
|
932
|
+
const pending = handleStreamReply(
|
|
933
|
+
{ chat_id: '1', text: 'hi', reply_to: '777' },
|
|
934
|
+
state,
|
|
935
|
+
deps,
|
|
936
|
+
)
|
|
937
|
+
await microtaskFlush()
|
|
938
|
+
await pending
|
|
939
|
+
|
|
940
|
+
// Lookup is skipped entirely when reply_to is explicit.
|
|
941
|
+
expect(lookup).not.toHaveBeenCalled()
|
|
942
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toEqual({
|
|
943
|
+
message_id: 777,
|
|
944
|
+
})
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
it('quote:false opts out — no reply_parameters sent', async () => {
|
|
948
|
+
const state = makeState()
|
|
949
|
+
const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
|
|
950
|
+
() => 4242,
|
|
951
|
+
)
|
|
952
|
+
const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
|
|
953
|
+
|
|
954
|
+
const pending = handleStreamReply(
|
|
955
|
+
{ chat_id: '1', text: 'hi', quote: false },
|
|
956
|
+
state,
|
|
957
|
+
deps,
|
|
958
|
+
)
|
|
959
|
+
await microtaskFlush()
|
|
960
|
+
await pending
|
|
961
|
+
|
|
962
|
+
expect(lookup).not.toHaveBeenCalled()
|
|
963
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toBeUndefined()
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
it('no reply_parameters when history lookup returns null (empty history)', async () => {
|
|
967
|
+
const state = makeState()
|
|
968
|
+
const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
|
|
969
|
+
() => null,
|
|
970
|
+
)
|
|
971
|
+
const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
|
|
972
|
+
|
|
973
|
+
const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
|
|
974
|
+
await microtaskFlush()
|
|
975
|
+
await pending
|
|
976
|
+
|
|
977
|
+
expect(lookup).toHaveBeenCalledTimes(1)
|
|
978
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toBeUndefined()
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
it('no auto-quote when getLatestInboundMessageId dep is omitted (legacy callers)', async () => {
|
|
982
|
+
const state = makeState()
|
|
983
|
+
const deps = makeDeps(bot) // no lookup dep
|
|
984
|
+
|
|
985
|
+
const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
|
|
986
|
+
await microtaskFlush()
|
|
987
|
+
await pending
|
|
988
|
+
|
|
989
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toBeUndefined()
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
it('passes thread id to the lookup', async () => {
|
|
993
|
+
const state = makeState()
|
|
994
|
+
const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
|
|
995
|
+
() => 55,
|
|
996
|
+
)
|
|
997
|
+
const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
|
|
998
|
+
|
|
999
|
+
const pending = handleStreamReply(
|
|
1000
|
+
{ chat_id: '1', text: 'hi', message_thread_id: '7' },
|
|
1001
|
+
state,
|
|
1002
|
+
deps,
|
|
1003
|
+
)
|
|
1004
|
+
await microtaskFlush()
|
|
1005
|
+
await pending
|
|
1006
|
+
|
|
1007
|
+
expect(lookup).toHaveBeenCalledWith('1', 7)
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
it('edit-path does not include reply_parameters (only initial send)', async () => {
|
|
1011
|
+
const state = makeState()
|
|
1012
|
+
const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
|
|
1013
|
+
() => 4242,
|
|
1014
|
+
)
|
|
1015
|
+
const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
|
|
1016
|
+
|
|
1017
|
+
// First call → send with reply_parameters.
|
|
1018
|
+
await handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
|
|
1019
|
+
await microtaskFlush()
|
|
1020
|
+
|
|
1021
|
+
// Second call on the same stream → edit. editMessageText must NOT
|
|
1022
|
+
// receive reply_parameters (Telegram rejects it on edit).
|
|
1023
|
+
vi.advanceTimersByTime(1000)
|
|
1024
|
+
await handleStreamReply({ chat_id: '1', text: 'hi there' }, state, deps)
|
|
1025
|
+
await microtaskFlush()
|
|
1026
|
+
|
|
1027
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toEqual({
|
|
1028
|
+
message_id: 4242,
|
|
1029
|
+
})
|
|
1030
|
+
expect(bot.api.editMessageText).toHaveBeenCalled()
|
|
1031
|
+
const editOpts = bot.api.editMessageText.mock.calls[0][3]
|
|
1032
|
+
expect((editOpts as { reply_parameters?: unknown })?.reply_parameters).toBeUndefined()
|
|
1033
|
+
})
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
describe('reply_markup persistence', () => {
|
|
1037
|
+
it('reply_markup in args is included in sendMessage opts on stream creation', async () => {
|
|
1038
|
+
const state = makeState()
|
|
1039
|
+
const deps = makeDeps(bot)
|
|
1040
|
+
const keyboard = { inline_keyboard: [[{ text: 'Steer', callback_data: 'steer:1' }]] }
|
|
1041
|
+
|
|
1042
|
+
const pending = handleStreamReply(
|
|
1043
|
+
{ chat_id: '1', text: 'hi', reply_markup: keyboard },
|
|
1044
|
+
state,
|
|
1045
|
+
deps,
|
|
1046
|
+
)
|
|
1047
|
+
await microtaskFlush()
|
|
1048
|
+
await pending
|
|
1049
|
+
|
|
1050
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.reply_markup).toBe(keyboard)
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
it('reply_markup persists through editMessageText on subsequent updates', async () => {
|
|
1054
|
+
const state = makeState()
|
|
1055
|
+
const deps = makeDeps(bot)
|
|
1056
|
+
const keyboard = { inline_keyboard: [[{ text: 'Steer', callback_data: 'steer:1' }]] }
|
|
1057
|
+
|
|
1058
|
+
const p1 = handleStreamReply(
|
|
1059
|
+
{ chat_id: '1', text: 'step 1', reply_markup: keyboard },
|
|
1060
|
+
state,
|
|
1061
|
+
deps,
|
|
1062
|
+
)
|
|
1063
|
+
await microtaskFlush()
|
|
1064
|
+
await p1
|
|
1065
|
+
|
|
1066
|
+
vi.advanceTimersByTime(1000)
|
|
1067
|
+
const p2 = handleStreamReply(
|
|
1068
|
+
{ chat_id: '1', text: 'step 2', reply_markup: keyboard },
|
|
1069
|
+
state,
|
|
1070
|
+
deps,
|
|
1071
|
+
)
|
|
1072
|
+
await microtaskFlush()
|
|
1073
|
+
await p2
|
|
1074
|
+
|
|
1075
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
1076
|
+
expect(bot.api.editMessageText.mock.calls[0][3]?.reply_markup).toBe(keyboard)
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
it('reply_markup persists through finalize flush on done=true', async () => {
|
|
1080
|
+
const state = makeState()
|
|
1081
|
+
const deps = makeDeps(bot)
|
|
1082
|
+
const keyboard = { inline_keyboard: [[{ text: 'Steer', callback_data: 'steer:1' }]] }
|
|
1083
|
+
|
|
1084
|
+
const p1 = handleStreamReply(
|
|
1085
|
+
{ chat_id: '1', text: 'draft', reply_markup: keyboard },
|
|
1086
|
+
state,
|
|
1087
|
+
deps,
|
|
1088
|
+
)
|
|
1089
|
+
await microtaskFlush()
|
|
1090
|
+
await p1
|
|
1091
|
+
|
|
1092
|
+
vi.advanceTimersByTime(1000)
|
|
1093
|
+
const p2 = handleStreamReply(
|
|
1094
|
+
{ chat_id: '1', text: 'final', done: true, reply_markup: keyboard },
|
|
1095
|
+
state,
|
|
1096
|
+
deps,
|
|
1097
|
+
)
|
|
1098
|
+
await microtaskFlush()
|
|
1099
|
+
await p2
|
|
1100
|
+
|
|
1101
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
1102
|
+
expect(bot.api.editMessageText.mock.calls[0][3]?.reply_markup).toBe(keyboard)
|
|
1103
|
+
})
|
|
1104
|
+
|
|
1105
|
+
it('omits reply_markup when not provided in args', async () => {
|
|
1106
|
+
const state = makeState()
|
|
1107
|
+
const deps = makeDeps(bot)
|
|
1108
|
+
|
|
1109
|
+
const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
|
|
1110
|
+
await microtaskFlush()
|
|
1111
|
+
await pending
|
|
1112
|
+
|
|
1113
|
+
expect(bot.api.sendMessage.mock.calls[0][2]?.reply_markup).toBeUndefined()
|
|
1114
|
+
})
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
describe('lookupExistingMessageId hook (#626 — multiple status messages regression)', () => {
|
|
1118
|
+
it('reuses an externally-known messageId on stream creation — first emit edits, no sendMessage', async () => {
|
|
1119
|
+
// The pin manager already knows the anchor message id for this
|
|
1120
|
+
// turnKey from a previous emit cycle (e.g. before done=true wiped
|
|
1121
|
+
// activeDraftStreams[sKey]). The hook hands that id back; the
|
|
1122
|
+
// new stream initializes with it, so the FIRST update edits in
|
|
1123
|
+
// place. No fresh sendMessage = no extra "status message" lands.
|
|
1124
|
+
const state = makeState()
|
|
1125
|
+
const deps = makeDeps(bot, {
|
|
1126
|
+
lookupExistingMessageId: ({ turnKey, lane }) => {
|
|
1127
|
+
if (turnKey === 'turn-A' && lane === 'progress') return 4242
|
|
1128
|
+
return null
|
|
1129
|
+
},
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
const pending = handleStreamReply(
|
|
1133
|
+
{ chat_id: '1', text: 'second emit', lane: 'progress', turnKey: 'turn-A' },
|
|
1134
|
+
state,
|
|
1135
|
+
deps,
|
|
1136
|
+
)
|
|
1137
|
+
await microtaskFlush()
|
|
1138
|
+
const result = await pending
|
|
1139
|
+
|
|
1140
|
+
expect(bot.api.sendMessage).not.toHaveBeenCalled()
|
|
1141
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
1142
|
+
const [, id] = bot.api.editMessageText.mock.calls[0]
|
|
1143
|
+
expect(id).toBe(4242)
|
|
1144
|
+
expect(result.messageId).toBe(4242)
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
it('hook returns null → falls through to legacy sendMessage path', async () => {
|
|
1148
|
+
// Back-compat sanity: a hook that returns null on every call
|
|
1149
|
+
// produces identical behavior to omitting the hook entirely.
|
|
1150
|
+
const state = makeState()
|
|
1151
|
+
const deps = makeDeps(bot, {
|
|
1152
|
+
lookupExistingMessageId: () => null,
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
const pending = handleStreamReply(
|
|
1156
|
+
{ chat_id: '1', text: 'fresh send', lane: 'progress', turnKey: 'turn-X' },
|
|
1157
|
+
state,
|
|
1158
|
+
deps,
|
|
1159
|
+
)
|
|
1160
|
+
await microtaskFlush()
|
|
1161
|
+
await pending
|
|
1162
|
+
|
|
1163
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
1164
|
+
expect(bot.api.editMessageText).not.toHaveBeenCalled()
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
it('hook NOT consulted when an active draft stream already exists for the lane+turn', async () => {
|
|
1168
|
+
// Lifecycle invariant: the hook only fires on stream creation.
|
|
1169
|
+
// If activeDraftStreams[sKey] is already populated (turn in
|
|
1170
|
+
// progress, no done=true yet), the existing stream handles
|
|
1171
|
+
// edits — the hook is never consulted, so it can't disturb the
|
|
1172
|
+
// running stream's state.
|
|
1173
|
+
const state = makeState()
|
|
1174
|
+
let lookupCalls = 0
|
|
1175
|
+
const deps = makeDeps(bot, {
|
|
1176
|
+
lookupExistingMessageId: () => {
|
|
1177
|
+
lookupCalls++
|
|
1178
|
+
return 9999
|
|
1179
|
+
},
|
|
1180
|
+
})
|
|
1181
|
+
|
|
1182
|
+
// First emit creates the stream (lookup IS called, returns
|
|
1183
|
+
// 9999 → first edit goes to 9999).
|
|
1184
|
+
await handleStreamReply(
|
|
1185
|
+
{ chat_id: '1', text: 'first', lane: 'progress', turnKey: 'turn-B' },
|
|
1186
|
+
state,
|
|
1187
|
+
deps,
|
|
1188
|
+
)
|
|
1189
|
+
await microtaskFlush()
|
|
1190
|
+
vi.advanceTimersByTime(1000)
|
|
1191
|
+
expect(lookupCalls).toBe(1)
|
|
1192
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
1193
|
+
|
|
1194
|
+
// Second emit on the same lane+turn reuses the existing stream
|
|
1195
|
+
// — the lookup is NOT called again. Edits still target 9999.
|
|
1196
|
+
await handleStreamReply(
|
|
1197
|
+
{ chat_id: '1', text: 'second', lane: 'progress', turnKey: 'turn-B' },
|
|
1198
|
+
state,
|
|
1199
|
+
deps,
|
|
1200
|
+
)
|
|
1201
|
+
await microtaskFlush()
|
|
1202
|
+
expect(lookupCalls).toBe(1)
|
|
1203
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(2)
|
|
1204
|
+
})
|
|
1205
|
+
|
|
1206
|
+
it('hook throws → error logged, handler falls through to fresh sendMessage', async () => {
|
|
1207
|
+
// Defensive contract: a buggy lookup must never break the
|
|
1208
|
+
// outbound path. Caller's writeError gets the diagnostic; the
|
|
1209
|
+
// emit lands as a fresh send.
|
|
1210
|
+
const state = makeState()
|
|
1211
|
+
const writeError = vi.fn()
|
|
1212
|
+
const deps = makeDeps(bot, {
|
|
1213
|
+
writeError,
|
|
1214
|
+
lookupExistingMessageId: () => {
|
|
1215
|
+
throw new Error('lookup blew up')
|
|
1216
|
+
},
|
|
1217
|
+
})
|
|
1218
|
+
|
|
1219
|
+
await handleStreamReply(
|
|
1220
|
+
{ chat_id: '1', text: 'after-fault', lane: 'progress', turnKey: 'turn-C' },
|
|
1221
|
+
state,
|
|
1222
|
+
deps,
|
|
1223
|
+
)
|
|
1224
|
+
await microtaskFlush()
|
|
1225
|
+
|
|
1226
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
1227
|
+
expect(writeError).toHaveBeenCalled()
|
|
1228
|
+
const errLine = (writeError.mock.calls[0]?.[0] as string) ?? ''
|
|
1229
|
+
expect(errLine).toContain('lookupExistingMessageId failed')
|
|
1230
|
+
})
|
|
1231
|
+
|
|
1232
|
+
it('full #626 lifecycle scenario — done=true → activeDraftStreams cleared → next emit edits via hook (one anchor message total)', async () => {
|
|
1233
|
+
// The end-to-end repro of #626. Sequence:
|
|
1234
|
+
// 1. First progress-card emit (isFirstEmit=true) → fresh
|
|
1235
|
+
// sendMessage on the 'progress' lane for turn-A. Pin
|
|
1236
|
+
// manager records messageId=500.
|
|
1237
|
+
// 2. done=true emit (e.g. the parent turn_end fires before
|
|
1238
|
+
// sub-agents finish) → handler finalizes + DELETES
|
|
1239
|
+
// activeDraftStreams[sKey].
|
|
1240
|
+
// 3. A subsequent sub-agent event triggers a fresh progress-
|
|
1241
|
+
// card emit on the SAME turn-A. Without the hook, the
|
|
1242
|
+
// handler would create a new stream → fresh sendMessage →
|
|
1243
|
+
// a SECOND status message lands in the chat.
|
|
1244
|
+
// 4. With the hook returning the pin-manager's messageId 500,
|
|
1245
|
+
// the new stream initializes with 500. The next update
|
|
1246
|
+
// hits editMessageText against 500. Total Telegram surface
|
|
1247
|
+
// = ONE message.
|
|
1248
|
+
const state = makeState()
|
|
1249
|
+
const knownMessageId = { value: null as number | null }
|
|
1250
|
+
const deps = makeDeps(bot, {
|
|
1251
|
+
lookupExistingMessageId: () => knownMessageId.value,
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
// 1. First emit, no known messageId yet → fresh sendMessage
|
|
1255
|
+
await handleStreamReply(
|
|
1256
|
+
{ chat_id: '1', text: 'tool 1...', lane: 'progress', turnKey: 'turn-A' },
|
|
1257
|
+
state,
|
|
1258
|
+
deps,
|
|
1259
|
+
)
|
|
1260
|
+
await microtaskFlush()
|
|
1261
|
+
// The pin manager records id=500 (mock bot's first id).
|
|
1262
|
+
knownMessageId.value = 500
|
|
1263
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
1264
|
+
expect(bot.api.editMessageText).not.toHaveBeenCalled()
|
|
1265
|
+
|
|
1266
|
+
// 2. done=true → finalize + clear sKey
|
|
1267
|
+
vi.advanceTimersByTime(1000)
|
|
1268
|
+
await handleStreamReply(
|
|
1269
|
+
{ chat_id: '1', text: 'tool 1, tool 2 ✓', lane: 'progress', turnKey: 'turn-A', done: true },
|
|
1270
|
+
state,
|
|
1271
|
+
deps,
|
|
1272
|
+
)
|
|
1273
|
+
await microtaskFlush()
|
|
1274
|
+
expect(state.activeDraftStreams.size).toBe(0)
|
|
1275
|
+
|
|
1276
|
+
// 3. Subsequent sub-agent emit on the SAME turn-A — without
|
|
1277
|
+
// the hook this would land as sendMessage #2 (the bug).
|
|
1278
|
+
await handleStreamReply(
|
|
1279
|
+
{ chat_id: '1', text: 'tool 1, tool 2 ✓, sub-agent...', lane: 'progress', turnKey: 'turn-A' },
|
|
1280
|
+
state,
|
|
1281
|
+
deps,
|
|
1282
|
+
)
|
|
1283
|
+
await microtaskFlush()
|
|
1284
|
+
|
|
1285
|
+
// Invariant: total fresh sendMessages on this chat = 1.
|
|
1286
|
+
// Anything > 1 is the #626 bug class.
|
|
1287
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
1288
|
+
// The post-done emit was an edit against id 500.
|
|
1289
|
+
expect(bot.api.editMessageText.mock.calls.some((c) => c[1] === 500)).toBe(true)
|
|
1290
|
+
})
|
|
1291
|
+
})
|
|
1292
|
+
})
|