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,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration tests for the streaming state machine that lives in
|
|
3
|
+
* server.ts around `activeDraftStreams: Map<streamKey, DraftStreamHandle>`.
|
|
4
|
+
*
|
|
5
|
+
* These tests model the pattern used by the real `stream_reply` MCP
|
|
6
|
+
* handler and `handlePtyPartial` — both of which look up the map, create
|
|
7
|
+
* a controller on miss, and delete the entry on done. We exercise the
|
|
8
|
+
* *state transitions* directly against real `createStreamController`
|
|
9
|
+
* instances backed by the mock bot harness, so that regressions in the
|
|
10
|
+
* reliability-critical flows (claim-then-new-turn, cross-chat isolation,
|
|
11
|
+
* thread keying, post-done re-entry) fail a test instead of silently
|
|
12
|
+
* ghosting in production.
|
|
13
|
+
*
|
|
14
|
+
* Scope: single-controller primitives are covered in
|
|
15
|
+
* `stream-controller.test.ts`. This file exists for multi-stream
|
|
16
|
+
* + restart + race-shape scenarios.
|
|
17
|
+
*/
|
|
18
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
19
|
+
import { createStreamController } from '../stream-controller.js'
|
|
20
|
+
import type { DraftStreamHandle } from '../draft-stream.js'
|
|
21
|
+
import { createMockBot, installBotResetHook, microtaskFlush } from './bot-api.harness.js'
|
|
22
|
+
|
|
23
|
+
function streamKey(chatId: string, threadId?: number): string {
|
|
24
|
+
return `${chatId}:${threadId ?? '_'}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tiny facsimile of server.ts's stream_reply case. Takes a Map of
|
|
29
|
+
* active streams and wires the create-on-miss + delete-on-done flow.
|
|
30
|
+
* Pure — no side effects outside the map and bot calls.
|
|
31
|
+
*/
|
|
32
|
+
function modelStreamReply(params: {
|
|
33
|
+
bot: ReturnType<typeof createMockBot>
|
|
34
|
+
map: Map<string, DraftStreamHandle>
|
|
35
|
+
chatId: string
|
|
36
|
+
threadId?: number
|
|
37
|
+
text: string
|
|
38
|
+
done?: boolean
|
|
39
|
+
throttleMs?: number
|
|
40
|
+
parseMode?: 'HTML' | 'MarkdownV2'
|
|
41
|
+
}): { stream: DraftStreamHandle; settled: Promise<void> } {
|
|
42
|
+
const key = streamKey(params.chatId, params.threadId)
|
|
43
|
+
let stream = params.map.get(key)
|
|
44
|
+
if (!stream) {
|
|
45
|
+
stream = createStreamController({
|
|
46
|
+
bot: params.bot,
|
|
47
|
+
chatId: params.chatId,
|
|
48
|
+
threadId: params.threadId,
|
|
49
|
+
parseMode: params.parseMode,
|
|
50
|
+
throttleMs: params.throttleMs ?? 600,
|
|
51
|
+
})
|
|
52
|
+
params.map.set(key, stream)
|
|
53
|
+
}
|
|
54
|
+
// Fire-and-forget update — caller advances fake timers + flushes
|
|
55
|
+
// microtasks to drive progress, matching draft-stream.test.ts style.
|
|
56
|
+
void stream.update(params.text)
|
|
57
|
+
const settled = params.done
|
|
58
|
+
? stream.finalize().then(() => { params.map.delete(key) })
|
|
59
|
+
: Promise.resolve()
|
|
60
|
+
return { stream, settled }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('streaming orchestration — activeDraftStreams map', () => {
|
|
64
|
+
const bot = createMockBot()
|
|
65
|
+
installBotResetHook(bot)
|
|
66
|
+
|
|
67
|
+
beforeEach(() => vi.useFakeTimers())
|
|
68
|
+
afterEach(() => vi.useRealTimers())
|
|
69
|
+
|
|
70
|
+
it('first call creates a stream in the map; second call reuses it', async () => {
|
|
71
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
72
|
+
|
|
73
|
+
const { stream: a } = modelStreamReply({ bot, map, chatId: '1', text: 'hi' })
|
|
74
|
+
await microtaskFlush()
|
|
75
|
+
vi.advanceTimersByTime(700)
|
|
76
|
+
await microtaskFlush()
|
|
77
|
+
const { stream: b } = modelStreamReply({ bot, map, chatId: '1', text: 'hi again' })
|
|
78
|
+
await microtaskFlush()
|
|
79
|
+
|
|
80
|
+
expect(a).toBe(b)
|
|
81
|
+
expect(map.size).toBe(1)
|
|
82
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
83
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('done=true deletes the map entry; next call creates a fresh stream', async () => {
|
|
87
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
88
|
+
|
|
89
|
+
const { stream: first, settled } = modelStreamReply({ bot, map, chatId: '1', text: 'a', done: true })
|
|
90
|
+
await microtaskFlush()
|
|
91
|
+
await settled
|
|
92
|
+
expect(map.has('1:_')).toBe(false)
|
|
93
|
+
expect(first.isFinal()).toBe(true)
|
|
94
|
+
|
|
95
|
+
const { stream: second } = modelStreamReply({ bot, map, chatId: '1', text: 'next turn' })
|
|
96
|
+
await microtaskFlush()
|
|
97
|
+
|
|
98
|
+
expect(first).not.toBe(second)
|
|
99
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
100
|
+
expect(second.getMessageId()).toBe(501)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('different chats keep independent streams (no crosstalk)', async () => {
|
|
104
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
105
|
+
|
|
106
|
+
modelStreamReply({ bot, map, chatId: '1', text: 'chat A' })
|
|
107
|
+
modelStreamReply({ bot, map, chatId: '2', text: 'chat B' })
|
|
108
|
+
await microtaskFlush()
|
|
109
|
+
|
|
110
|
+
expect(map.size).toBe(2)
|
|
111
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
112
|
+
const chatIds = bot.api.sendMessage.mock.calls.map(c => c[0]).sort()
|
|
113
|
+
expect(chatIds).toEqual(['1', '2'])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('same chat, different threads → independent streams (forum topic safety)', async () => {
|
|
117
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
118
|
+
|
|
119
|
+
modelStreamReply({ bot, map, chatId: '1', threadId: 10, text: 'topic A' })
|
|
120
|
+
modelStreamReply({ bot, map, chatId: '1', threadId: 20, text: 'topic B' })
|
|
121
|
+
await microtaskFlush()
|
|
122
|
+
|
|
123
|
+
expect(map.size).toBe(2)
|
|
124
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
125
|
+
const threads = bot.api.sendMessage.mock.calls.map(c => c[2]?.message_thread_id).sort()
|
|
126
|
+
expect(threads).toEqual([10, 20])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('pty preview → stream_reply handoff: second call edits the PTY message in place', async () => {
|
|
130
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
131
|
+
|
|
132
|
+
const ptyStream = createStreamController({ bot, chatId: '1', parseMode: 'HTML', throttleMs: 600 })
|
|
133
|
+
map.set('1:_', ptyStream)
|
|
134
|
+
void ptyStream.update('<i>drafting…</i>')
|
|
135
|
+
await microtaskFlush()
|
|
136
|
+
|
|
137
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
138
|
+
const previewId = ptyStream.getMessageId()
|
|
139
|
+
expect(previewId).toBe(500)
|
|
140
|
+
|
|
141
|
+
vi.advanceTimersByTime(600)
|
|
142
|
+
await microtaskFlush()
|
|
143
|
+
const { settled } = modelStreamReply({
|
|
144
|
+
bot, map, chatId: '1', parseMode: 'HTML', text: 'final answer', done: true,
|
|
145
|
+
})
|
|
146
|
+
await microtaskFlush()
|
|
147
|
+
await settled
|
|
148
|
+
|
|
149
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
150
|
+
expect(bot.api.editMessageText).toHaveBeenCalled()
|
|
151
|
+
expect(bot.api.editMessageText.mock.calls[0][1]).toBe(previewId)
|
|
152
|
+
expect(map.has('1:_')).toBe(false)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('rapid stream_reply burst coalesces to one send + one edit with latest text', async () => {
|
|
156
|
+
// The first update fires send('one') immediately. While that's
|
|
157
|
+
// in-flight, updates 'two'/'three'/'four' overwrite pendingText
|
|
158
|
+
// synchronously. draft-stream's flushLoop drains pendingText to
|
|
159
|
+
// 'four' in a single follow-up edit after send resolves. Net:
|
|
160
|
+
// 4 update() calls → 1 send + 1 edit with the latest text.
|
|
161
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
162
|
+
|
|
163
|
+
for (const t of ['one', 'two', 'three', 'four']) {
|
|
164
|
+
modelStreamReply({ bot, map, chatId: '1', text: t, throttleMs: 1000 })
|
|
165
|
+
}
|
|
166
|
+
await microtaskFlush()
|
|
167
|
+
|
|
168
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
169
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('one')
|
|
170
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
171
|
+
expect(bot.api.editMessageText.mock.calls[0][2]).toBe('four')
|
|
172
|
+
|
|
173
|
+
// No further work scheduled; advancing timers should be a no-op.
|
|
174
|
+
vi.advanceTimersByTime(5000)
|
|
175
|
+
await microtaskFlush()
|
|
176
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('done=true after pending text still flushes the final snapshot', async () => {
|
|
180
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
181
|
+
|
|
182
|
+
modelStreamReply({ bot, map, chatId: '1', text: 'drafting…', throttleMs: 1000 })
|
|
183
|
+
await microtaskFlush()
|
|
184
|
+
const { settled } = modelStreamReply({
|
|
185
|
+
bot, map, chatId: '1', text: 'final answer', done: true, throttleMs: 1000,
|
|
186
|
+
})
|
|
187
|
+
await microtaskFlush()
|
|
188
|
+
await settled
|
|
189
|
+
|
|
190
|
+
expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
|
|
191
|
+
expect(bot.api.editMessageText.mock.calls[0][2]).toBe('final answer')
|
|
192
|
+
expect(map.size).toBe(0)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('failed first send leaves a retry-able stream: next update re-attempts send', async () => {
|
|
196
|
+
// First send fails; draft-stream swallows the error, leaves
|
|
197
|
+
// messageId=null. A subsequent update() triggers a fresh send on the
|
|
198
|
+
// same stream object — this is what makes a momentary network blip
|
|
199
|
+
// recoverable without the caller needing to evict-and-recreate.
|
|
200
|
+
bot.api.sendMessage
|
|
201
|
+
.mockImplementationOnce(async () => { throw new Error('network down') })
|
|
202
|
+
.mockImplementationOnce(async () => ({ message_id: 777 }))
|
|
203
|
+
|
|
204
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
205
|
+
const { stream } = modelStreamReply({ bot, map, chatId: '1', text: 'first try' })
|
|
206
|
+
await microtaskFlush()
|
|
207
|
+
|
|
208
|
+
expect(stream.getMessageId()).toBeNull()
|
|
209
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
210
|
+
|
|
211
|
+
// Caller issues next snapshot — should retry send, not try to edit null.
|
|
212
|
+
vi.advanceTimersByTime(1000)
|
|
213
|
+
modelStreamReply({ bot, map, chatId: '1', text: 'second try' })
|
|
214
|
+
await microtaskFlush()
|
|
215
|
+
|
|
216
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
217
|
+
expect(bot.api.editMessageText).not.toHaveBeenCalled()
|
|
218
|
+
expect(stream.getMessageId()).toBe(777)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// suppressPtyPreview claim/release — the "duplicate Telegram message" race
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
//
|
|
226
|
+
// The reply tool handler claims the stream by:
|
|
227
|
+
// 1. adding the streamKey to suppressPtyPreview BEFORE touching the stream
|
|
228
|
+
// 2. finalizing and deleting the active stream
|
|
229
|
+
// 3. sending the canonical reply
|
|
230
|
+
// 4. NOT releasing the suppress lock in the `finally` — turn_end does that
|
|
231
|
+
//
|
|
232
|
+
// This prevents the PTY tail from seeing an empty activeDraftStreams in step
|
|
233
|
+
// 2 and creating a fresh preview that would surface as a duplicate.
|
|
234
|
+
|
|
235
|
+
function modelPtyPartial(params: {
|
|
236
|
+
bot: ReturnType<typeof createMockBot>
|
|
237
|
+
map: Map<string, DraftStreamHandle>
|
|
238
|
+
suppress: Set<string>
|
|
239
|
+
chatId: string
|
|
240
|
+
threadId?: number
|
|
241
|
+
text: string
|
|
242
|
+
throttleMs?: number
|
|
243
|
+
}): DraftStreamHandle | null {
|
|
244
|
+
const key = streamKey(params.chatId, params.threadId)
|
|
245
|
+
if (params.suppress.has(key)) return null
|
|
246
|
+
let stream = params.map.get(key)
|
|
247
|
+
if (!stream) {
|
|
248
|
+
stream = createStreamController({
|
|
249
|
+
bot: params.bot,
|
|
250
|
+
chatId: params.chatId,
|
|
251
|
+
threadId: params.threadId,
|
|
252
|
+
parseMode: 'HTML',
|
|
253
|
+
throttleMs: params.throttleMs ?? 600,
|
|
254
|
+
})
|
|
255
|
+
params.map.set(key, stream)
|
|
256
|
+
}
|
|
257
|
+
void stream.update(params.text)
|
|
258
|
+
return stream
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function modelReplyClaim(params: {
|
|
262
|
+
bot: ReturnType<typeof createMockBot>
|
|
263
|
+
map: Map<string, DraftStreamHandle>
|
|
264
|
+
suppress: Set<string>
|
|
265
|
+
chatId: string
|
|
266
|
+
threadId?: number
|
|
267
|
+
}): Promise<{ previewId: number | null }> {
|
|
268
|
+
const key = streamKey(params.chatId, params.threadId)
|
|
269
|
+
params.suppress.add(key) // step 1: claim BEFORE touching the stream
|
|
270
|
+
const open = params.map.get(key)
|
|
271
|
+
let previewId: number | null = null
|
|
272
|
+
if (open && !open.isFinal()) {
|
|
273
|
+
await open.finalize()
|
|
274
|
+
previewId = open.getMessageId()
|
|
275
|
+
params.map.delete(key)
|
|
276
|
+
}
|
|
277
|
+
return { previewId }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function modelTurnEnd(params: {
|
|
281
|
+
suppress: Set<string>
|
|
282
|
+
chatId: string
|
|
283
|
+
threadId?: number
|
|
284
|
+
}): void {
|
|
285
|
+
params.suppress.delete(streamKey(params.chatId, params.threadId))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
describe('suppressPtyPreview claim/release race', () => {
|
|
289
|
+
const bot = createMockBot()
|
|
290
|
+
installBotResetHook(bot)
|
|
291
|
+
|
|
292
|
+
beforeEach(() => vi.useFakeTimers())
|
|
293
|
+
afterEach(() => vi.useRealTimers())
|
|
294
|
+
|
|
295
|
+
it('PTY partials during a reply claim are dropped (no duplicate send)', async () => {
|
|
296
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
297
|
+
const suppress = new Set<string>()
|
|
298
|
+
|
|
299
|
+
// Step 1: PTY creates a preview
|
|
300
|
+
modelPtyPartial({ bot, map, suppress, chatId: '1', text: 'drafting' })
|
|
301
|
+
await microtaskFlush()
|
|
302
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
303
|
+
|
|
304
|
+
// Step 2: reply claims — finalizes preview, deletes map entry
|
|
305
|
+
const { previewId } = await modelReplyClaim({ bot, map, suppress, chatId: '1' })
|
|
306
|
+
expect(previewId).toBe(500)
|
|
307
|
+
expect(map.has('1:_')).toBe(false)
|
|
308
|
+
expect(suppress.has('1:_')).toBe(true)
|
|
309
|
+
|
|
310
|
+
// Step 3: while suppressed, PTY fires another partial — should be dropped
|
|
311
|
+
const result = modelPtyPartial({ bot, map, suppress, chatId: '1', text: 'more content' })
|
|
312
|
+
await microtaskFlush()
|
|
313
|
+
|
|
314
|
+
expect(result).toBeNull()
|
|
315
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1) // no duplicate send
|
|
316
|
+
expect(map.has('1:_')).toBe(false) // no ghost stream created
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('claim is added BEFORE stream deletion (race-tight ordering)', async () => {
|
|
320
|
+
// If suppress.add happens AFTER map.delete, a PTY partial firing in
|
|
321
|
+
// between would create a new stream. We model the claim in the exact
|
|
322
|
+
// order the real code uses — any reordering here should fail this test.
|
|
323
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
324
|
+
const suppress = new Set<string>()
|
|
325
|
+
|
|
326
|
+
modelPtyPartial({ bot, map, suppress, chatId: '1', text: 'preview' })
|
|
327
|
+
await microtaskFlush()
|
|
328
|
+
|
|
329
|
+
// Interleave: start claim, but simulate a partial arriving mid-claim.
|
|
330
|
+
// In the real code, suppress.add is synchronous and happens before any
|
|
331
|
+
// await. So by the time PTY could fire, suppress is already set.
|
|
332
|
+
suppress.add('1:_') // the claim's first action
|
|
333
|
+
const result = modelPtyPartial({ bot, map, suppress, chatId: '1', text: 'racy partial' })
|
|
334
|
+
await microtaskFlush()
|
|
335
|
+
|
|
336
|
+
expect(result).toBeNull()
|
|
337
|
+
// Finish the claim
|
|
338
|
+
const open = map.get('1:_')!
|
|
339
|
+
await open.finalize()
|
|
340
|
+
map.delete('1:_')
|
|
341
|
+
|
|
342
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1) // only the original
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('turn_end releases suppress; next turn PTY partial creates a fresh stream', async () => {
|
|
346
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
347
|
+
const suppress = new Set<string>()
|
|
348
|
+
|
|
349
|
+
modelPtyPartial({ bot, map, suppress, chatId: '1', text: 'first preview' })
|
|
350
|
+
await microtaskFlush()
|
|
351
|
+
await modelReplyClaim({ bot, map, suppress, chatId: '1' })
|
|
352
|
+
|
|
353
|
+
// Reply lands (simulated by a direct send). Turn ends.
|
|
354
|
+
modelTurnEnd({ suppress, chatId: '1' })
|
|
355
|
+
expect(suppress.has('1:_')).toBe(false)
|
|
356
|
+
|
|
357
|
+
// Next turn — a new PTY partial can now create a stream again.
|
|
358
|
+
const result = modelPtyPartial({ bot, map, suppress, chatId: '1', text: 'next turn preview' })
|
|
359
|
+
await microtaskFlush()
|
|
360
|
+
|
|
361
|
+
expect(result).not.toBeNull()
|
|
362
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2) // original preview + new one
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('suppress is scoped by chat+thread — claim in chat A does not block chat B', async () => {
|
|
366
|
+
const map = new Map<string, DraftStreamHandle>()
|
|
367
|
+
const suppress = new Set<string>()
|
|
368
|
+
|
|
369
|
+
modelPtyPartial({ bot, map, suppress, chatId: 'A', text: 'A draft' })
|
|
370
|
+
await microtaskFlush()
|
|
371
|
+
await modelReplyClaim({ bot, map, suppress, chatId: 'A' })
|
|
372
|
+
|
|
373
|
+
// Chat B is untouched by A's claim — a PTY partial for B should work
|
|
374
|
+
const result = modelPtyPartial({ bot, map, suppress, chatId: 'B', text: 'B draft' })
|
|
375
|
+
await microtaskFlush()
|
|
376
|
+
|
|
377
|
+
expect(result).not.toBeNull()
|
|
378
|
+
expect(suppress.has('A:_')).toBe(true)
|
|
379
|
+
expect(suppress.has('B:_')).toBe(false)
|
|
380
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// PTY activity-lane wiring — onActivity → stream_reply on lane "activity"
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
//
|
|
388
|
+
// These tests model the wiring added in server.ts: when pty-tail's
|
|
389
|
+
// `onActivity(text)` fires, the plugin pushes `text` through
|
|
390
|
+
// handleStreamReply with `lane: "activity"`, producing a distinct Telegram
|
|
391
|
+
// message (different message id) from the answer-lane stream.
|
|
392
|
+
//
|
|
393
|
+
// We don't import server.ts directly (it has top-level side effects: it
|
|
394
|
+
// connects the bot, starts tails, etc.). Instead we reproduce the wire
|
|
395
|
+
// shape by calling handleStreamReply with the same lane arg the real code
|
|
396
|
+
// uses — that exercises stream-reply-handler's streamKey + Map behavior
|
|
397
|
+
// which is where the two-lane separation actually lives.
|
|
398
|
+
|
|
399
|
+
import { handleStreamReply, type StreamReplyDeps, type StreamReplyState } from '../stream-reply-handler.js'
|
|
400
|
+
|
|
401
|
+
function makeActivityDeps(
|
|
402
|
+
bot: ReturnType<typeof createMockBot>,
|
|
403
|
+
overrides?: Partial<StreamReplyDeps>,
|
|
404
|
+
): StreamReplyDeps {
|
|
405
|
+
return {
|
|
406
|
+
bot,
|
|
407
|
+
markdownToHtml: (t) => t,
|
|
408
|
+
escapeMarkdownV2: (t) => t,
|
|
409
|
+
repairEscapedWhitespace: (t) => t,
|
|
410
|
+
takeHandoffPrefix: () => '',
|
|
411
|
+
assertAllowedChat: () => {},
|
|
412
|
+
resolveThreadId: (_, explicit) => (explicit != null ? Number(explicit) : undefined),
|
|
413
|
+
disableLinkPreview: true,
|
|
414
|
+
defaultFormat: 'text',
|
|
415
|
+
logStreamingEvent: () => {},
|
|
416
|
+
endStatusReaction: () => {},
|
|
417
|
+
historyEnabled: false,
|
|
418
|
+
recordOutbound: () => {},
|
|
419
|
+
writeError: () => {},
|
|
420
|
+
throttleMs: 600,
|
|
421
|
+
...overrides,
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
describe('PTY activity-lane wiring (onActivity → stream_reply lane="activity")', () => {
|
|
426
|
+
const bot = createMockBot()
|
|
427
|
+
installBotResetHook(bot)
|
|
428
|
+
|
|
429
|
+
beforeEach(() => vi.useFakeTimers())
|
|
430
|
+
afterEach(() => vi.useRealTimers())
|
|
431
|
+
|
|
432
|
+
it('onActivity callback fires handleStreamReply on lane "activity" with expected text', async () => {
|
|
433
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
434
|
+
const deps = makeActivityDeps(bot)
|
|
435
|
+
|
|
436
|
+
// Simulate what server.ts's handlePtyActivity does when pty-tail emits
|
|
437
|
+
// the first activity line.
|
|
438
|
+
const onActivity = (text: string) => {
|
|
439
|
+
void handleStreamReply(
|
|
440
|
+
{ chat_id: '1', text, lane: 'activity', format: 'text' },
|
|
441
|
+
state,
|
|
442
|
+
deps,
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
onActivity('Running Bash: git status')
|
|
447
|
+
await microtaskFlush()
|
|
448
|
+
|
|
449
|
+
// Lane-specific stream key must be present; the default (answer) lane
|
|
450
|
+
// must NOT — activity is its own channel.
|
|
451
|
+
expect(state.activeDraftStreams.has('1:_:activity')).toBe(true)
|
|
452
|
+
expect(state.activeDraftStreams.has('1:_')).toBe(false)
|
|
453
|
+
|
|
454
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
455
|
+
expect(bot.api.sendMessage.mock.calls[0][0]).toBe('1')
|
|
456
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('Running Bash: git status')
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('two concurrent lanes (activity + answer) each get their own message id', async () => {
|
|
460
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
461
|
+
const deps = makeActivityDeps(bot)
|
|
462
|
+
|
|
463
|
+
// Activity lane fires first — model started running tools
|
|
464
|
+
const pActivity = handleStreamReply(
|
|
465
|
+
{ chat_id: '1', text: 'Running Bash: ls', lane: 'activity', format: 'text' },
|
|
466
|
+
state,
|
|
467
|
+
deps,
|
|
468
|
+
)
|
|
469
|
+
await microtaskFlush()
|
|
470
|
+
const rActivity = await pActivity
|
|
471
|
+
|
|
472
|
+
// Answer lane — model starts emitting the reply
|
|
473
|
+
const pAnswer = handleStreamReply(
|
|
474
|
+
{ chat_id: '1', text: 'Here is the answer', format: 'text' }, // default lane
|
|
475
|
+
state,
|
|
476
|
+
deps,
|
|
477
|
+
)
|
|
478
|
+
await microtaskFlush()
|
|
479
|
+
const rAnswer = await pAnswer
|
|
480
|
+
|
|
481
|
+
// Both lanes have distinct stream keys in the map
|
|
482
|
+
expect(state.activeDraftStreams.has('1:_:activity')).toBe(true)
|
|
483
|
+
expect(state.activeDraftStreams.has('1:_')).toBe(true)
|
|
484
|
+
expect(state.activeDraftStreams.size).toBe(2)
|
|
485
|
+
|
|
486
|
+
// Two separate Telegram sends — two message ids
|
|
487
|
+
expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
|
|
488
|
+
expect(rActivity.messageId).not.toBeNull()
|
|
489
|
+
expect(rAnswer.messageId).not.toBeNull()
|
|
490
|
+
expect(rActivity.messageId).not.toBe(rAnswer.messageId)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
// ---- closeActivityLane / turn-boundary coverage --------------------
|
|
494
|
+
//
|
|
495
|
+
// These cases model the `closeActivityLane(chatId, threadId)` helper in
|
|
496
|
+
// server.ts that fires on `turn_end`. It must (a) finalize + remove the
|
|
497
|
+
// activity-lane stream from the map, (b) be a no-op if no activity
|
|
498
|
+
// stream exists, and (c) allow a fresh activity stream on the NEXT
|
|
499
|
+
// turn (new message id — not editing the now-closed previous one).
|
|
500
|
+
|
|
501
|
+
function modelCloseActivityLane(
|
|
502
|
+
state: StreamReplyState,
|
|
503
|
+
chatId: string,
|
|
504
|
+
threadId?: number,
|
|
505
|
+
): Promise<void> {
|
|
506
|
+
const key = `${chatId}:${threadId ?? '_'}:activity`
|
|
507
|
+
const stream = state.activeDraftStreams.get(key)
|
|
508
|
+
if (stream == null) return Promise.resolve()
|
|
509
|
+
state.activeDraftStreams.delete(key)
|
|
510
|
+
return stream.finalize().catch(() => { /* swallow */ })
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
it('closeActivityLane removes the stream entry and is idempotent', async () => {
|
|
514
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
515
|
+
const deps = makeActivityDeps(bot)
|
|
516
|
+
|
|
517
|
+
await handleStreamReply(
|
|
518
|
+
{ chat_id: '1', text: 'Running Bash: ls', lane: 'activity', format: 'text' },
|
|
519
|
+
state, deps,
|
|
520
|
+
)
|
|
521
|
+
await microtaskFlush()
|
|
522
|
+
expect(state.activeDraftStreams.has('1:_:activity')).toBe(true)
|
|
523
|
+
|
|
524
|
+
await modelCloseActivityLane(state, '1', undefined)
|
|
525
|
+
expect(state.activeDraftStreams.has('1:_:activity')).toBe(false)
|
|
526
|
+
|
|
527
|
+
// Calling again with no stream present must not throw.
|
|
528
|
+
await expect(modelCloseActivityLane(state, '1', undefined)).resolves.toBeUndefined()
|
|
529
|
+
await expect(modelCloseActivityLane(state, 'never-existed', undefined)).resolves.toBeUndefined()
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it('next turn after closeActivityLane creates a fresh message (no edit of closed stream)', async () => {
|
|
533
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
534
|
+
const deps = makeActivityDeps(bot)
|
|
535
|
+
|
|
536
|
+
// Turn 1
|
|
537
|
+
await handleStreamReply(
|
|
538
|
+
{ chat_id: '1', text: 'Running Bash: ls', lane: 'activity', format: 'text' },
|
|
539
|
+
state, deps,
|
|
540
|
+
)
|
|
541
|
+
await microtaskFlush()
|
|
542
|
+
const firstId = state.activeDraftStreams.get('1:_:activity')!.getMessageId()
|
|
543
|
+
const sendsBefore = bot.api.sendMessage.mock.calls.length
|
|
544
|
+
|
|
545
|
+
// Turn boundary
|
|
546
|
+
await modelCloseActivityLane(state, '1', undefined)
|
|
547
|
+
expect(state.activeDraftStreams.has('1:_:activity')).toBe(false)
|
|
548
|
+
|
|
549
|
+
// Turn 2 — new activity must send a fresh message, not edit the old one
|
|
550
|
+
await handleStreamReply(
|
|
551
|
+
{ chat_id: '1', text: 'Reading file: foo.ts', lane: 'activity', format: 'text' },
|
|
552
|
+
state, deps,
|
|
553
|
+
)
|
|
554
|
+
await microtaskFlush()
|
|
555
|
+
|
|
556
|
+
expect(state.activeDraftStreams.has('1:_:activity')).toBe(true)
|
|
557
|
+
const secondId = state.activeDraftStreams.get('1:_:activity')!.getMessageId()
|
|
558
|
+
expect(secondId).not.toBe(firstId)
|
|
559
|
+
expect(bot.api.sendMessage.mock.calls.length).toBe(sendsBefore + 1)
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it('closeActivityLane leaves the answer lane untouched (independent lifecycle)', async () => {
|
|
563
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
564
|
+
const deps = makeActivityDeps(bot)
|
|
565
|
+
|
|
566
|
+
await handleStreamReply(
|
|
567
|
+
{ chat_id: '1', text: 'Running Bash: ls', lane: 'activity', format: 'text' },
|
|
568
|
+
state, deps,
|
|
569
|
+
)
|
|
570
|
+
await microtaskFlush()
|
|
571
|
+
await handleStreamReply(
|
|
572
|
+
{ chat_id: '1', text: 'Drafting answer…', format: 'text' },
|
|
573
|
+
state, deps,
|
|
574
|
+
)
|
|
575
|
+
await microtaskFlush()
|
|
576
|
+
|
|
577
|
+
expect(state.activeDraftStreams.has('1:_:activity')).toBe(true)
|
|
578
|
+
expect(state.activeDraftStreams.has('1:_')).toBe(true)
|
|
579
|
+
|
|
580
|
+
await modelCloseActivityLane(state, '1', undefined)
|
|
581
|
+
|
|
582
|
+
expect(state.activeDraftStreams.has('1:_:activity')).toBe(false)
|
|
583
|
+
// Answer lane survives — turn_end finalizes answer via its own
|
|
584
|
+
// done=true path, not via closeActivityLane.
|
|
585
|
+
expect(state.activeDraftStreams.has('1:_')).toBe(true)
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
// handlePtyActivity's "drop when no chat id" guard lives in server.ts.
|
|
589
|
+
// We model the guard shape directly: when currentSessionChatId is null
|
|
590
|
+
// the call short-circuits BEFORE handleStreamReply, so nothing lands
|
|
591
|
+
// in the map and no bot traffic fires.
|
|
592
|
+
it('handlePtyActivity guard: drops events when currentSessionChatId is null', async () => {
|
|
593
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
594
|
+
const deps = makeActivityDeps(bot)
|
|
595
|
+
|
|
596
|
+
let currentSessionChatId: string | null = null
|
|
597
|
+
const handlePtyActivity = (text: string) => {
|
|
598
|
+
if (currentSessionChatId == null) return
|
|
599
|
+
void handleStreamReply(
|
|
600
|
+
{ chat_id: currentSessionChatId, text, lane: 'activity', format: 'text' },
|
|
601
|
+
state, deps,
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const sendsBefore = bot.api.sendMessage.mock.calls.length
|
|
606
|
+
handlePtyActivity('Running Bash: git status')
|
|
607
|
+
await microtaskFlush()
|
|
608
|
+
|
|
609
|
+
expect(state.activeDraftStreams.size).toBe(0)
|
|
610
|
+
expect(bot.api.sendMessage.mock.calls.length).toBe(sendsBefore)
|
|
611
|
+
|
|
612
|
+
// Once chat id is known, the next activity line DOES land.
|
|
613
|
+
currentSessionChatId = '1'
|
|
614
|
+
handlePtyActivity('Running Bash: git status')
|
|
615
|
+
await microtaskFlush()
|
|
616
|
+
|
|
617
|
+
expect(state.activeDraftStreams.has('1:_:activity')).toBe(true)
|
|
618
|
+
expect(bot.api.sendMessage.mock.calls.length).toBe(sendsBefore + 1)
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it('activity lane across chat ids: closing lane for chat A does not touch chat B', async () => {
|
|
622
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
623
|
+
const deps = makeActivityDeps(bot)
|
|
624
|
+
|
|
625
|
+
await handleStreamReply(
|
|
626
|
+
{ chat_id: 'A', text: 'A running', lane: 'activity', format: 'text' },
|
|
627
|
+
state, deps,
|
|
628
|
+
)
|
|
629
|
+
await microtaskFlush()
|
|
630
|
+
await handleStreamReply(
|
|
631
|
+
{ chat_id: 'B', text: 'B running', lane: 'activity', format: 'text' },
|
|
632
|
+
state, deps,
|
|
633
|
+
)
|
|
634
|
+
await microtaskFlush()
|
|
635
|
+
|
|
636
|
+
expect(state.activeDraftStreams.has('A:_:activity')).toBe(true)
|
|
637
|
+
expect(state.activeDraftStreams.has('B:_:activity')).toBe(true)
|
|
638
|
+
|
|
639
|
+
await modelCloseActivityLane(state, 'A', undefined)
|
|
640
|
+
|
|
641
|
+
expect(state.activeDraftStreams.has('A:_:activity')).toBe(false)
|
|
642
|
+
expect(state.activeDraftStreams.has('B:_:activity')).toBe(true)
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it('activity lane updates edit the activity message, not the answer message', async () => {
|
|
646
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
647
|
+
const deps = makeActivityDeps(bot)
|
|
648
|
+
|
|
649
|
+
// Seed both lanes
|
|
650
|
+
await handleStreamReply(
|
|
651
|
+
{ chat_id: '1', text: 'Running Bash: ls', lane: 'activity', format: 'text' },
|
|
652
|
+
state, deps,
|
|
653
|
+
)
|
|
654
|
+
await microtaskFlush()
|
|
655
|
+
await handleStreamReply(
|
|
656
|
+
{ chat_id: '1', text: 'Drafting answer…', format: 'text' },
|
|
657
|
+
state, deps,
|
|
658
|
+
)
|
|
659
|
+
await microtaskFlush()
|
|
660
|
+
|
|
661
|
+
const activityId = state.activeDraftStreams.get('1:_:activity')!.getMessageId()
|
|
662
|
+
const answerId = state.activeDraftStreams.get('1:_')!.getMessageId()
|
|
663
|
+
expect(activityId).not.toBe(answerId)
|
|
664
|
+
|
|
665
|
+
// Advance past throttle window so subsequent updates can flush
|
|
666
|
+
vi.advanceTimersByTime(700)
|
|
667
|
+
await microtaskFlush()
|
|
668
|
+
|
|
669
|
+
// Next activity update should edit the activity message
|
|
670
|
+
await handleStreamReply(
|
|
671
|
+
{ chat_id: '1', text: 'Reading file: foo.ts', lane: 'activity', format: 'text' },
|
|
672
|
+
state, deps,
|
|
673
|
+
)
|
|
674
|
+
await microtaskFlush()
|
|
675
|
+
|
|
676
|
+
const editCalls = bot.api.editMessageText.mock.calls
|
|
677
|
+
expect(editCalls.length).toBeGreaterThanOrEqual(1)
|
|
678
|
+
// The most recent edit must have targeted the activity message id
|
|
679
|
+
const lastEdit = editCalls[editCalls.length - 1]
|
|
680
|
+
expect(lastEdit[1]).toBe(activityId)
|
|
681
|
+
expect(lastEdit[2]).toBe('Reading file: foo.ts')
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
// ---- closeProgressLane / turn-boundary coverage --------------------
|
|
685
|
+
//
|
|
686
|
+
// Bug 2 regression: on turn_end the server also closes the `progress`
|
|
687
|
+
// lane (the one the progress-card driver owns). Without this, the
|
|
688
|
+
// same Telegram messageId is re-edited across turns and the user
|
|
689
|
+
// sees a visible shrink-flicker when a new turn opens with 0 items
|
|
690
|
+
// on top of the previous turn's 10-item "Done" state. These tests
|
|
691
|
+
// model `closeProgressLane(chatId, threadId)` exactly like the
|
|
692
|
+
// existing closeActivityLane cases above.
|
|
693
|
+
|
|
694
|
+
function modelCloseProgressLane(
|
|
695
|
+
state: StreamReplyState,
|
|
696
|
+
chatId: string,
|
|
697
|
+
threadId?: number,
|
|
698
|
+
): Promise<void> {
|
|
699
|
+
const key = `${chatId}:${threadId ?? '_'}:progress`
|
|
700
|
+
const stream = state.activeDraftStreams.get(key)
|
|
701
|
+
if (stream == null) return Promise.resolve()
|
|
702
|
+
state.activeDraftStreams.delete(key)
|
|
703
|
+
return stream.finalize().catch(() => { /* swallow */ })
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
it('closeProgressLane removes the stream entry and is idempotent', async () => {
|
|
707
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
708
|
+
const deps = makeActivityDeps(bot)
|
|
709
|
+
|
|
710
|
+
await handleStreamReply(
|
|
711
|
+
{ chat_id: '1', text: '⚙️ Working…', lane: 'progress', format: 'html' },
|
|
712
|
+
state, deps,
|
|
713
|
+
)
|
|
714
|
+
await microtaskFlush()
|
|
715
|
+
expect(state.activeDraftStreams.has('1:_:progress')).toBe(true)
|
|
716
|
+
|
|
717
|
+
await modelCloseProgressLane(state, '1', undefined)
|
|
718
|
+
expect(state.activeDraftStreams.has('1:_:progress')).toBe(false)
|
|
719
|
+
|
|
720
|
+
// Idempotent.
|
|
721
|
+
await expect(modelCloseProgressLane(state, '1', undefined)).resolves.toBeUndefined()
|
|
722
|
+
await expect(modelCloseProgressLane(state, 'never', undefined)).resolves.toBeUndefined()
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
it('next turn after closeProgressLane posts a FRESH progress message (new messageId)', async () => {
|
|
726
|
+
const state: StreamReplyState = { activeDraftStreams: new Map<string, DraftStreamHandle>() }
|
|
727
|
+
const deps = makeActivityDeps(bot)
|
|
728
|
+
|
|
729
|
+
// Turn 1: progress card lives at some messageId.
|
|
730
|
+
await handleStreamReply(
|
|
731
|
+
{ chat_id: '1', text: '⚙️ Working… (turn 1)', lane: 'progress', format: 'html' },
|
|
732
|
+
state, deps,
|
|
733
|
+
)
|
|
734
|
+
await microtaskFlush()
|
|
735
|
+
const firstProgressId = state.activeDraftStreams.get('1:_:progress')!.getMessageId()
|
|
736
|
+
const sendsBefore = bot.api.sendMessage.mock.calls.length
|
|
737
|
+
|
|
738
|
+
// turn_end closes the progress lane.
|
|
739
|
+
await modelCloseProgressLane(state, '1', undefined)
|
|
740
|
+
expect(state.activeDraftStreams.has('1:_:progress')).toBe(false)
|
|
741
|
+
|
|
742
|
+
// Turn 2: the fresh progress-card open must send a NEW message, not
|
|
743
|
+
// edit the previous one — this is what prevents the shrink-flicker
|
|
744
|
+
// described in the bug report.
|
|
745
|
+
await handleStreamReply(
|
|
746
|
+
{ chat_id: '1', text: '⚙️ Working… (turn 2)', lane: 'progress', format: 'html' },
|
|
747
|
+
state, deps,
|
|
748
|
+
)
|
|
749
|
+
await microtaskFlush()
|
|
750
|
+
|
|
751
|
+
expect(state.activeDraftStreams.has('1:_:progress')).toBe(true)
|
|
752
|
+
const secondProgressId = state.activeDraftStreams.get('1:_:progress')!.getMessageId()
|
|
753
|
+
expect(secondProgressId).not.toBe(firstProgressId)
|
|
754
|
+
expect(bot.api.sendMessage.mock.calls.length).toBe(sendsBefore + 1)
|
|
755
|
+
})
|
|
756
|
+
})
|