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,1093 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Telegram formatting utilities: markdownToHtml, splitHtmlChunks,
|
|
3
|
+
* file reference wrapping, and message coalescing.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect } from 'vitest'
|
|
6
|
+
|
|
7
|
+
// Import from the side-effect-free format module so tests don't trigger
|
|
8
|
+
// server.ts's startup (env load, token check, grammy init).
|
|
9
|
+
import { markdownToHtml, splitHtmlChunks, isLikelyTelegramHtml, repairEscapedWhitespace, sanitizeForTelegram } from '../format.js'
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// markdownToHtml
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
describe('markdownToHtml', () => {
|
|
16
|
+
test('converts bold **text** to <b>text</b>', () => {
|
|
17
|
+
expect(markdownToHtml('Hello **world**')).toContain('<b>world</b>')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('converts italic *text* to <i>text</i>', () => {
|
|
21
|
+
expect(markdownToHtml('Hello *world*')).toContain('<i>world</i>')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('does not confuse bold and italic', () => {
|
|
25
|
+
const result = markdownToHtml('**bold** and *italic*')
|
|
26
|
+
expect(result).toContain('<b>bold</b>')
|
|
27
|
+
expect(result).toContain('<i>italic</i>')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// _..._ italic (underscore form) — 8 cases
|
|
31
|
+
test('converts _text_ to <i>text</i> (plain underscore italic)', () => {
|
|
32
|
+
expect(markdownToHtml('Hello _world_')).toContain('<i>world</i>')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('converts emoji-leading _📥 queued as a new task_', () => {
|
|
36
|
+
expect(markdownToHtml('_📥 queued as a new task_')).toContain('<i>📥 queued as a new task</i>')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('converts emoji-trailing _steer on the prior task 🔁_', () => {
|
|
40
|
+
expect(markdownToHtml('_steer on the prior task 🔁_')).toContain('<i>steer on the prior task 🔁</i>')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('converts both-ends emoji _🔥 hot take 🔥_', () => {
|
|
44
|
+
expect(markdownToHtml('_🔥 hot take 🔥_')).toContain('<i>🔥 hot take 🔥</i>')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('does NOT convert snake_case to italic', () => {
|
|
48
|
+
const result = markdownToHtml('my_snake_case_var')
|
|
49
|
+
expect(result).not.toContain('<i>')
|
|
50
|
+
expect(result).toContain('my_snake_case_var')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('does NOT convert __double__ underscore to italic', () => {
|
|
54
|
+
const result = markdownToHtml('__double__')
|
|
55
|
+
expect(result).not.toContain('<i>')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('does NOT convert word-internal underscores', () => {
|
|
59
|
+
const result = markdownToHtml('foo_bar')
|
|
60
|
+
expect(result).not.toContain('<i>')
|
|
61
|
+
expect(result).toContain('foo_bar')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('_..._ and *...* italics coexist correctly', () => {
|
|
65
|
+
const result = markdownToHtml('*asterisk* and _underscore_')
|
|
66
|
+
expect(result).toContain('<i>asterisk</i>')
|
|
67
|
+
expect(result).toContain('<i>underscore</i>')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('converts inline `code` to <code>code</code>', () => {
|
|
71
|
+
expect(markdownToHtml('Use `console.log`')).toContain('<code>console.log</code>')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('converts code blocks with language', () => {
|
|
75
|
+
const input = '```typescript\nconst x = 1\n```'
|
|
76
|
+
const result = markdownToHtml(input)
|
|
77
|
+
expect(result).toContain('<pre><code class="language-typescript">')
|
|
78
|
+
expect(result).toContain('const x = 1')
|
|
79
|
+
expect(result).toContain('</code></pre>')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('converts code blocks without language', () => {
|
|
83
|
+
const input = '```\nplain code\n```'
|
|
84
|
+
const result = markdownToHtml(input)
|
|
85
|
+
expect(result).toContain('<pre><code>')
|
|
86
|
+
expect(result).toContain('plain code')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('converts strikethrough ~~text~~ to <s>text</s>', () => {
|
|
90
|
+
expect(markdownToHtml('~~deleted~~')).toContain('<s>deleted</s>')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('converts [text](url) to <a href="url">text</a>', () => {
|
|
94
|
+
const result = markdownToHtml('Click [here](https://example.com)')
|
|
95
|
+
expect(result).toContain('<a href="https://example.com">here</a>')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('escapes HTML entities in plain text', () => {
|
|
99
|
+
const result = markdownToHtml('x < y & z > w')
|
|
100
|
+
expect(result).toContain('<')
|
|
101
|
+
expect(result).toContain('&')
|
|
102
|
+
expect(result).toContain('>')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('does not escape HTML inside code blocks', () => {
|
|
106
|
+
const input = '```html\n<div>test</div>\n```'
|
|
107
|
+
const result = markdownToHtml(input)
|
|
108
|
+
expect(result).toContain('<div>test</div>')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('does not escape HTML inside inline code', () => {
|
|
112
|
+
const result = markdownToHtml('Use `<div>` element')
|
|
113
|
+
expect(result).toContain('<code><div></code>')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('wraps file references in code tags', () => {
|
|
117
|
+
const result = markdownToHtml('Edit server.ts and package.json')
|
|
118
|
+
expect(result).toContain('<code>server.ts</code>')
|
|
119
|
+
expect(result).toContain('<code>package.json</code>')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('does not double-wrap file references already in code', () => {
|
|
123
|
+
const result = markdownToHtml('Edit `server.ts` now')
|
|
124
|
+
// Should have exactly one <code>server.ts</code>, not nested
|
|
125
|
+
const matches = result.match(/<code>server\.ts<\/code>/g)
|
|
126
|
+
expect(matches).not.toBeNull()
|
|
127
|
+
expect(matches!.length).toBe(1)
|
|
128
|
+
// And crucially: NO nested <code><code>...</code></code>
|
|
129
|
+
expect(result).not.toContain('<code><code>')
|
|
130
|
+
expect(result).not.toContain('</code></code>')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('inline code containing asterisks does not get re-matched by italic regex (#415)', () => {
|
|
134
|
+
// Regression for #415: inline-code spans containing `*` (e.g. C
|
|
135
|
+
// pointer syntax) used to be restored from their placeholder BEFORE
|
|
136
|
+
// the italic pass, so the italic regex would see `<code>size_t *p</code>`
|
|
137
|
+
// and try to wrap `p</code>...` in <i>...</i>, producing invalid HTML
|
|
138
|
+
// that Telegram rejected with 400 Bad Request, sending the caller into
|
|
139
|
+
// a `format: text` fallback for the rest of the chunk.
|
|
140
|
+
const result = markdownToHtml('Use `size_t *p` to declare a pointer.')
|
|
141
|
+
expect(result).toContain('<code>size_t *p</code>')
|
|
142
|
+
// No stray <i> wrapping the asterisk — pre-fix the buggy output was
|
|
143
|
+
// `<code>size_t <i>p</code> to declare a pointer.</i>`.
|
|
144
|
+
expect(result).not.toMatch(/<i>[^<]*<\/code>/)
|
|
145
|
+
expect(result).not.toMatch(/<code>[^<]*<i>/)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('inline code containing double-asterisks does not get re-matched by bold regex (#415)', () => {
|
|
149
|
+
const result = markdownToHtml('Pattern is `**glob**` not regex.')
|
|
150
|
+
expect(result).toContain('<code>**glob**</code>')
|
|
151
|
+
// The bold regex must not have wrapped the literal asterisks inside <code>.
|
|
152
|
+
expect(result).not.toMatch(/<b>[^<]*<\/code>/)
|
|
153
|
+
expect(result).not.toMatch(/<code>[^<]*<b>/)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('code block containing asterisks does not get re-matched by italic regex (#415)', () => {
|
|
157
|
+
const input = '```c\nsize_t *p = NULL;\n```'
|
|
158
|
+
const result = markdownToHtml(input)
|
|
159
|
+
expect(result).toContain('size_t *p = NULL;')
|
|
160
|
+
expect(result).not.toMatch(/<i>[^<]*<\/code>/)
|
|
161
|
+
expect(result).not.toMatch(/<i>[^<]*<\/pre>/)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('does not double-wrap when inline code sits alongside prose with file refs', () => {
|
|
165
|
+
// Regression for the user-observed bug: messages that mixed inline code
|
|
166
|
+
// spans (backticks around filenames) with prose produced
|
|
167
|
+
// `<code><code>settings.json</code></code>` in the stored history. The
|
|
168
|
+
// file-reference regex ran AFTER inline-code placeholder restoration and
|
|
169
|
+
// re-wrapped the filename inside the just-restored <code> tag because
|
|
170
|
+
// its negative lookbehind did not exclude `>`.
|
|
171
|
+
const result = markdownToHtml(
|
|
172
|
+
'I mixed raw `<a href="...">` HTML into messages whose `format` defaults ' +
|
|
173
|
+
'to `html` — but the plugin runs a markdown→HTML converter which escapes ' +
|
|
174
|
+
'literal `<` and `>`, so raw tags render as visible text in the rendered ' +
|
|
175
|
+
'`settings.json` output.'
|
|
176
|
+
)
|
|
177
|
+
expect(result).not.toContain('<code><code>')
|
|
178
|
+
expect(result).not.toContain('</code></code>')
|
|
179
|
+
// settings.json, format, html should each appear inside exactly one
|
|
180
|
+
// <code> tag — either from the backtick wrapping or the file-ref regex,
|
|
181
|
+
// but never both.
|
|
182
|
+
const settingsMatches = result.match(/<code>settings\.json<\/code>/g)
|
|
183
|
+
expect(settingsMatches).not.toBeNull()
|
|
184
|
+
expect(settingsMatches!.length).toBe(1)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('file-reference wrap still runs on bare filenames in prose', () => {
|
|
188
|
+
// Confirm the fix doesn't break the normal case: bare filenames in
|
|
189
|
+
// plain prose still get auto-wrapped in <code> tags.
|
|
190
|
+
const result = markdownToHtml('Edit server.ts and then run tsc --noEmit')
|
|
191
|
+
expect(result).toContain('<code>server.ts</code>')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('file-reference wrap does not match filenames adjacent to > (inside tag markup)', () => {
|
|
195
|
+
// A filename that sits right after a `>` (tag close) should not be
|
|
196
|
+
// re-wrapped — it's already inside some structured context.
|
|
197
|
+
const input = '<b>foo.ts</b>'
|
|
198
|
+
const result = markdownToHtml(input)
|
|
199
|
+
// Passes through as Telegram HTML (smart pass-through) — filename is
|
|
200
|
+
// not wrapped in <code> because it's inside a <b>.
|
|
201
|
+
expect(result).toBe(input)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('handles nested bold and italic', () => {
|
|
205
|
+
const result = markdownToHtml('**bold *and italic* text**')
|
|
206
|
+
expect(result).toContain('<b>')
|
|
207
|
+
expect(result).toContain('</b>')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('handles plain text with no formatting', () => {
|
|
211
|
+
const result = markdownToHtml('Just a plain message')
|
|
212
|
+
expect(result).toBe('Just a plain message')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('handles empty string', () => {
|
|
216
|
+
expect(markdownToHtml('')).toBe('')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('preserves multiple paragraphs', () => {
|
|
220
|
+
const result = markdownToHtml('First paragraph\n\nSecond paragraph')
|
|
221
|
+
expect(result).toContain('First paragraph')
|
|
222
|
+
expect(result).toContain('Second paragraph')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('converts ## headings to bold (Telegram has no <h1>)', () => {
|
|
226
|
+
const result = markdownToHtml('## My Heading\n\nbody text')
|
|
227
|
+
expect(result).toContain('<b>My Heading</b>')
|
|
228
|
+
expect(result).not.toContain('## ')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('converts # headings to bold', () => {
|
|
232
|
+
const result = markdownToHtml('# Top heading\n\nbody')
|
|
233
|
+
expect(result).toContain('<b>Top heading</b>')
|
|
234
|
+
expect(result).not.toMatch(/^# /m)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('converts deep ### #### headings to bold without losing content', () => {
|
|
238
|
+
const result = markdownToHtml('### Section\n#### Subsection\nbody')
|
|
239
|
+
expect(result).toContain('<b>Section</b>')
|
|
240
|
+
expect(result).toContain('<b>Subsection</b>')
|
|
241
|
+
expect(result).not.toContain('###')
|
|
242
|
+
expect(result).not.toContain('####')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('does not convert # inside code blocks', () => {
|
|
246
|
+
const input = '```bash\n# this is a comment\n```'
|
|
247
|
+
const result = markdownToHtml(input)
|
|
248
|
+
expect(result).toContain('# this is a comment')
|
|
249
|
+
expect(result).not.toContain('<b># this is a comment</b>')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// ─── HTML pass-through (the bug that made <b> tags render as text) ─────
|
|
253
|
+
|
|
254
|
+
test('passes through already-rendered Telegram HTML untouched', () => {
|
|
255
|
+
const input = '<b>Bold heading</b>\n<i>italic body</i>'
|
|
256
|
+
expect(markdownToHtml(input)).toBe(input)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('passes through Telegram HTML with <code> blocks', () => {
|
|
260
|
+
const input = '<b>commit</b> <code>abc123</code>'
|
|
261
|
+
expect(markdownToHtml(input)).toBe(input)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('passes through Telegram HTML with mixed tags and text', () => {
|
|
265
|
+
const input = '<b>What you should see</b>\n👀 immediately, then 🤔 after 2s'
|
|
266
|
+
expect(markdownToHtml(input)).toBe(input)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('escapes when input has unsupported HTML tags (e.g. <div>)', () => {
|
|
270
|
+
const input = '<div>not telegram html</div>'
|
|
271
|
+
const out = markdownToHtml(input)
|
|
272
|
+
// Falls into the markdown path → escapes the angle brackets
|
|
273
|
+
expect(out).toContain('<div>')
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('escapes when input is plain markdown without HTML', () => {
|
|
277
|
+
const input = '**bold** text'
|
|
278
|
+
const out = markdownToHtml(input)
|
|
279
|
+
expect(out).toContain('<b>bold</b>')
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('isLikelyTelegramHtml', () => {
|
|
284
|
+
test('returns true for simple <b>', () => {
|
|
285
|
+
expect(isLikelyTelegramHtml('<b>hello</b>')).toBe(true)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('returns true for <code>', () => {
|
|
289
|
+
expect(isLikelyTelegramHtml('use <code>git status</code>')).toBe(true)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('returns true for nested supported tags', () => {
|
|
293
|
+
expect(isLikelyTelegramHtml('<b><i>bold italic</i></b>')).toBe(true)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('returns true for <a href>', () => {
|
|
297
|
+
expect(isLikelyTelegramHtml('see <a href="https://x.com">x</a>')).toBe(true)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('returns false when ANY tag is unsupported', () => {
|
|
301
|
+
expect(isLikelyTelegramHtml('<b>fine</b> but <div>not</div>')).toBe(false)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('returns false for plain text with no tags', () => {
|
|
305
|
+
expect(isLikelyTelegramHtml('just words here')).toBe(false)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('returns false for plain markdown', () => {
|
|
309
|
+
expect(isLikelyTelegramHtml('**bold** and *italic*')).toBe(false)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('returns false for code with angle brackets', () => {
|
|
313
|
+
expect(isLikelyTelegramHtml('the operator <-> means something')).toBe(false)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// ─── The bug: HTML tags inside markdown inline code spans ─────────────
|
|
317
|
+
|
|
318
|
+
test('ignores HTML tags inside backtick inline code', () => {
|
|
319
|
+
// The model writes `<b>tag</b>` (showing literal HTML in inline code).
|
|
320
|
+
// The text is markdown, NOT raw HTML — must return false.
|
|
321
|
+
expect(isLikelyTelegramHtml('Use `<b>tag</b>` to make text bold.')).toBe(false)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('ignores HTML tags inside fenced code blocks', () => {
|
|
325
|
+
const input = 'Example:\n```html\n<div>hi</div>\n```\nThat\'s it.'
|
|
326
|
+
expect(isLikelyTelegramHtml(input)).toBe(false)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('returns false when text mixes markdown bold with HTML examples in code', () => {
|
|
330
|
+
// The exact bug pattern from the user-facing screenshot regression
|
|
331
|
+
const input = '**1. Raw HTML rendering** — replies showed `<b>tag</b>` text instead of bold.'
|
|
332
|
+
expect(isLikelyTelegramHtml(input)).toBe(false)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('returns false when text has markdown links', () => {
|
|
336
|
+
expect(isLikelyTelegramHtml('See [docs](https://example.com)')).toBe(false)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('returns false when text has markdown headings', () => {
|
|
340
|
+
expect(isLikelyTelegramHtml('## Section\n\nbody')).toBe(false)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
test('still returns true for pure HTML even with code spans', () => {
|
|
344
|
+
// Code spans can coexist with real HTML — as long as there are NO
|
|
345
|
+
// markdown bold/link/heading patterns and the tags outside code are
|
|
346
|
+
// all valid Telegram HTML, trust it.
|
|
347
|
+
expect(isLikelyTelegramHtml('<b>commit</b> <code>abc123</code>')).toBe(true)
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
describe('markdownToHtml regression: mixed markdown + raw Telegram HTML', () => {
|
|
352
|
+
// The exact bug pattern from the user-facing screenshot regression: model
|
|
353
|
+
// emits markdown bold AND raw <b>/<a> tags in the same message. The
|
|
354
|
+
// markdown path used to escape every `<` to `<`, so the raw tags
|
|
355
|
+
// rendered as literal text. Now the converter preserves whitelisted
|
|
356
|
+
// Telegram HTML tags through the escape pass.
|
|
357
|
+
|
|
358
|
+
test('preserves embedded <b> when text also has markdown bold', () => {
|
|
359
|
+
const input = '**Pattern worth stealing:** the <b>verification subagent</b> is a validator.'
|
|
360
|
+
const out = markdownToHtml(input)
|
|
361
|
+
expect(out).toContain('<b>Pattern worth stealing:</b>')
|
|
362
|
+
expect(out).toContain('<b>verification subagent</b>')
|
|
363
|
+
expect(out).not.toContain('<b>')
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test('preserves embedded <a href> when text also has markdown bold', () => {
|
|
367
|
+
const input = '**Sources:** see <a href="https://example.com/x">Example</a> for details.'
|
|
368
|
+
const out = markdownToHtml(input)
|
|
369
|
+
expect(out).toContain('<b>Sources:</b>')
|
|
370
|
+
expect(out).toContain('<a href="https://example.com/x">Example</a>')
|
|
371
|
+
expect(out).not.toContain('<a ')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('preserves embedded <i> when text also has markdown bold', () => {
|
|
375
|
+
const input = '**Rule:** group work by <i>what context it needs</i>.'
|
|
376
|
+
const out = markdownToHtml(input)
|
|
377
|
+
expect(out).toContain('<b>Rule:</b>')
|
|
378
|
+
expect(out).toContain('<i>what context it needs</i>')
|
|
379
|
+
expect(out).not.toContain('<i>')
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test('preserves multiple embedded tags in one message', () => {
|
|
383
|
+
const input = '**Header**\n- <b>Context</b> matters\n- <i>Speed</i> too\n- See <a href="https://x.com">x</a>'
|
|
384
|
+
const out = markdownToHtml(input)
|
|
385
|
+
expect(out).toContain('<b>Header</b>')
|
|
386
|
+
expect(out).toContain('<b>Context</b>')
|
|
387
|
+
expect(out).toContain('<i>Speed</i>')
|
|
388
|
+
expect(out).toContain('<a href="https://x.com">x</a>')
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test('still escapes unsupported tags even when whitelisted ones are present', () => {
|
|
392
|
+
const input = '**hi** <b>ok</b> and <div>bad</div>'
|
|
393
|
+
const out = markdownToHtml(input)
|
|
394
|
+
expect(out).toContain('<b>hi</b>')
|
|
395
|
+
expect(out).toContain('<b>ok</b>')
|
|
396
|
+
// <div> is not in the whitelist → escaped
|
|
397
|
+
expect(out).toContain('<div>')
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
test('preserves embedded <code> spans alongside markdown', () => {
|
|
401
|
+
const input = '**Run:** <code>git status</code> first.'
|
|
402
|
+
const out = markdownToHtml(input)
|
|
403
|
+
expect(out).toContain('<b>Run:</b>')
|
|
404
|
+
expect(out).toContain('<code>git status</code>')
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('preserves <a> with query-string href containing markdown-link-like text', () => {
|
|
408
|
+
const input = 'See <a href="https://example.com/path">the docs</a>.'
|
|
409
|
+
const out = markdownToHtml(input)
|
|
410
|
+
expect(out).toContain('<a href="https://example.com/path">the docs</a>')
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
describe('markdownToHtml regression: HTML in code spans', () => {
|
|
415
|
+
test('renders **bold** correctly when text also contains `<b>` in inline code', () => {
|
|
416
|
+
const input = '**1. Raw HTML rendering** — replies showed `<b>tag</b>` text instead of bold.'
|
|
417
|
+
const out = markdownToHtml(input)
|
|
418
|
+
expect(out).toContain('<b>1. Raw HTML rendering</b>')
|
|
419
|
+
expect(out).toContain('<code><b>tag</b></code>')
|
|
420
|
+
expect(out).not.toContain('**1. Raw HTML rendering**')
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
test('renders fenced code blocks even when they contain HTML examples', () => {
|
|
424
|
+
const input = 'Example:\n```html\n<div>hi</div>\n```'
|
|
425
|
+
const out = markdownToHtml(input)
|
|
426
|
+
expect(out).toContain('<pre><code class="language-html">')
|
|
427
|
+
expect(out).toContain('<div>hi</div>')
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// splitHtmlChunks
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
describe('splitHtmlChunks', () => {
|
|
436
|
+
test('returns single chunk for short text', () => {
|
|
437
|
+
const result = splitHtmlChunks('Hello world', 4000)
|
|
438
|
+
expect(result).toEqual(['Hello world'])
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
test('splits long text into multiple chunks', () => {
|
|
442
|
+
const longText = 'a'.repeat(5000)
|
|
443
|
+
const chunks = splitHtmlChunks(longText, 2000)
|
|
444
|
+
expect(chunks.length).toBeGreaterThan(1)
|
|
445
|
+
// All chunks should be <= maxLen (plus possible closing tags)
|
|
446
|
+
for (const c of chunks) {
|
|
447
|
+
expect(c.length).toBeLessThanOrEqual(2100) // small margin for closing tags
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test('preserves open tags across chunk boundaries', () => {
|
|
452
|
+
const html = '<b>' + 'x'.repeat(5000) + '</b>'
|
|
453
|
+
const chunks = splitHtmlChunks(html, 2000)
|
|
454
|
+
expect(chunks.length).toBeGreaterThan(1)
|
|
455
|
+
// First chunk should have closing </b>
|
|
456
|
+
expect(chunks[0]).toContain('</b>')
|
|
457
|
+
// Second chunk should reopen <b>
|
|
458
|
+
expect(chunks[1]).toMatch(/^<b>/)
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
test('prefers splitting at paragraph boundaries', () => {
|
|
462
|
+
const html = 'First paragraph content here' + '\n\n' + 'Second paragraph content here'
|
|
463
|
+
// Set maxLen so it would split somewhere in the middle
|
|
464
|
+
const chunks = splitHtmlChunks(html, 35)
|
|
465
|
+
expect(chunks.length).toBe(2)
|
|
466
|
+
expect(chunks[0]).toContain('First paragraph')
|
|
467
|
+
expect(chunks[1]).toContain('Second paragraph')
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
test('handles nested tags', () => {
|
|
471
|
+
const html = '<b><i>' + 'x'.repeat(5000) + '</i></b>'
|
|
472
|
+
const chunks = splitHtmlChunks(html, 2000)
|
|
473
|
+
expect(chunks.length).toBeGreaterThan(1)
|
|
474
|
+
// First chunk should close both tags
|
|
475
|
+
expect(chunks[0]).toMatch(/<\/i><\/b>$/)
|
|
476
|
+
// Second chunk should reopen both tags
|
|
477
|
+
expect(chunks[1]).toMatch(/^<b><i>/)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test('handles empty string', () => {
|
|
481
|
+
expect(splitHtmlChunks('')).toEqual([''])
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('respects custom maxLen', () => {
|
|
485
|
+
const text = 'a'.repeat(100)
|
|
486
|
+
const chunks = splitHtmlChunks(text, 30)
|
|
487
|
+
expect(chunks.length).toBeGreaterThanOrEqual(3)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
test('defaults to 4000 maxLen', () => {
|
|
491
|
+
const text = 'a'.repeat(3999)
|
|
492
|
+
const chunks = splitHtmlChunks(text)
|
|
493
|
+
expect(chunks).toEqual([text])
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
test('does not split inside an HTML entity (&)', () => {
|
|
497
|
+
// Construct text where the natural cut would land inside &
|
|
498
|
+
// Position the entity so that maxLen falls between & and ;
|
|
499
|
+
const filler = 'x'.repeat(20)
|
|
500
|
+
// Cut would be at position 22, mid-entity
|
|
501
|
+
const html = filler + ' & more text after the entity'
|
|
502
|
+
const chunks = splitHtmlChunks(html, 22)
|
|
503
|
+
// The entity should not be broken — we should see the full & in
|
|
504
|
+
// some chunk, never &am or amp;.
|
|
505
|
+
for (const c of chunks) {
|
|
506
|
+
expect(c).not.toMatch(/&am$/)
|
|
507
|
+
expect(c).not.toMatch(/^p;/)
|
|
508
|
+
expect(c).not.toMatch(/^amp;/)
|
|
509
|
+
}
|
|
510
|
+
// Recombined text should equal original (allowing for the chunker's
|
|
511
|
+
// tag-rebalancing trim of leading newlines)
|
|
512
|
+
expect(chunks.join('')).toContain('&')
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
test('does not split inside a numeric HTML entity (💩)', () => {
|
|
516
|
+
const filler = 'a'.repeat(15)
|
|
517
|
+
const html = filler + ' 💩 more'
|
|
518
|
+
const chunks = splitHtmlChunks(html, 20)
|
|
519
|
+
for (const c of chunks) {
|
|
520
|
+
expect(c).not.toMatch(/$/)
|
|
521
|
+
expect(c).not.toMatch(/^4A9;/)
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
// ─── Regression: tag-name parsing must allow `-` so `tg-spoiler` and
|
|
526
|
+
// `tg-emoji` survive chunk boundaries instead of being truncated to `tg`.
|
|
527
|
+
test('preserves <tg-spoiler> across chunk boundaries', () => {
|
|
528
|
+
const html = '<tg-spoiler>' + 'x'.repeat(5000) + '</tg-spoiler>'
|
|
529
|
+
const chunks = splitHtmlChunks(html, 2000)
|
|
530
|
+
expect(chunks.length).toBeGreaterThan(1)
|
|
531
|
+
// Chunk0 must close with the FULL tag name, not a truncated `</tg>`
|
|
532
|
+
expect(chunks[0]).toMatch(/<\/tg-spoiler>$/)
|
|
533
|
+
expect(chunks[0]).not.toMatch(/<\/tg>$/)
|
|
534
|
+
// Chunk1 must reopen with the full tag name
|
|
535
|
+
expect(chunks[1]).toMatch(/^<tg-spoiler>/)
|
|
536
|
+
expect(chunks[1]).not.toMatch(/^<tg>/)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
test('preserves <tg-emoji> across chunk boundaries', () => {
|
|
540
|
+
const html = '<tg-emoji emoji-id="5368324170671202286">' + 'y'.repeat(5000) + '</tg-emoji>'
|
|
541
|
+
const chunks = splitHtmlChunks(html, 2000)
|
|
542
|
+
expect(chunks.length).toBeGreaterThan(1)
|
|
543
|
+
expect(chunks[0]).toMatch(/<\/tg-emoji>$/)
|
|
544
|
+
expect(chunks[1]).toMatch(/^<tg-emoji/)
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
// ─── Regression: reopening `<a href="...">` in the next chunk must
|
|
548
|
+
// preserve the href attribute. Previously the splitter emitted bare
|
|
549
|
+
// `<a>` which Telegram rejects.
|
|
550
|
+
test('preserves <a href="..."> attributes across chunk boundaries', () => {
|
|
551
|
+
const href = 'https://example.com/some/deep/path?x=1'
|
|
552
|
+
// Put a natural split point well into the link text so paragraph/space
|
|
553
|
+
// breaks don't land inside the opening tag itself.
|
|
554
|
+
const html = `<a href="${href}">` + 'word '.repeat(1000) + '</a>'
|
|
555
|
+
const chunks = splitHtmlChunks(html, 2000)
|
|
556
|
+
expect(chunks.length).toBeGreaterThan(1)
|
|
557
|
+
// First chunk must close the anchor
|
|
558
|
+
expect(chunks[0]).toMatch(/<\/a>$/)
|
|
559
|
+
// Second chunk must reopen with the FULL href attribute, not bare `<a>`
|
|
560
|
+
expect(chunks[1]).toMatch(new RegExp(`^<a href="${href.replace(/[.?/]/g, '\\$&')}">`))
|
|
561
|
+
expect(chunks[1]).not.toMatch(/^<a>/)
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
test('preserves <code class="language-ts"> attributes across boundaries', () => {
|
|
565
|
+
const html = '<pre><code class="language-ts">' + 'z '.repeat(2000) + '</code></pre>'
|
|
566
|
+
const chunks = splitHtmlChunks(html, 2000)
|
|
567
|
+
expect(chunks.length).toBeGreaterThan(1)
|
|
568
|
+
// Reopened chunk should carry the class attribute
|
|
569
|
+
expect(chunks[1]).toContain('<code class="language-ts">')
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
// ─── Regression: splitter must not cut INSIDE an open tag. Previously,
|
|
573
|
+
// `<a href="..."` followed by a long run of non-space text made the
|
|
574
|
+
// space-fallback pick position 2 (the space inside `<a href=`) and emit
|
|
575
|
+
// a chunk consisting of just `<a`, which Telegram rejects.
|
|
576
|
+
test('does not cut inside an open tag when tag contains the only nearby space', () => {
|
|
577
|
+
const html = '<a href="https://example.com/very/long/url">' + 'y'.repeat(5000) + '</a>'
|
|
578
|
+
const chunks = splitHtmlChunks(html, 2000)
|
|
579
|
+
// No chunk should end mid-tag (e.g. `<a` or `<a href="..`)
|
|
580
|
+
for (const c of chunks) {
|
|
581
|
+
// A chunk ending with `<` or `<tagname` with no closing `>` is malformed.
|
|
582
|
+
// Quick check: count unclosed `<`s by stripping complete tags.
|
|
583
|
+
const withoutTags = c.replace(/<[^>]*>/g, '')
|
|
584
|
+
expect(withoutTags).not.toContain('<')
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
test('backs off when the cut lands between < and > of an opening tag', () => {
|
|
589
|
+
// Construct a case where `cut` would naturally land inside `<b attr="...">`
|
|
590
|
+
const filler = 'a '.repeat(1000) // lots of spaces so splitter has choices
|
|
591
|
+
const html = filler + '<b class="very-long-classname-that-pushes-the-tag-past-cut">' + 'x'.repeat(5000) + '</b>'
|
|
592
|
+
const chunks = splitHtmlChunks(html, 2000)
|
|
593
|
+
// None of the chunks should contain a stray `<` without a matching `>`.
|
|
594
|
+
for (const c of chunks) {
|
|
595
|
+
const withoutTags = c.replace(/<[^>]*>/g, '')
|
|
596
|
+
expect(withoutTags).not.toContain('<')
|
|
597
|
+
expect(withoutTags).not.toContain('>')
|
|
598
|
+
}
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// File reference wrapping
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
|
|
606
|
+
describe('file reference wrapping', () => {
|
|
607
|
+
test('wraps .ts files', () => {
|
|
608
|
+
expect(markdownToHtml('Look at server.ts')).toContain('<code>server.ts</code>')
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
test('wraps .json files', () => {
|
|
612
|
+
expect(markdownToHtml('Check package.json')).toContain('<code>package.json</code>')
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
test('wraps .py files', () => {
|
|
616
|
+
expect(markdownToHtml('Run main.py')).toContain('<code>main.py</code>')
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
test('wraps complex filenames', () => {
|
|
620
|
+
expect(markdownToHtml('Edit my-component.tsx')).toContain('<code>my-component.tsx</code>')
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
test('does not wrap non-file extensions', () => {
|
|
624
|
+
const result = markdownToHtml('This is sentence.ending with a period')
|
|
625
|
+
// "sentence.ending" shouldn't be wrapped since "ending" is not in the ext list
|
|
626
|
+
expect(result).not.toContain('<code>sentence.ending</code>')
|
|
627
|
+
})
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
// Coalescing (unit-level: test the buffer/flush logic)
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
|
|
634
|
+
describe('coalescing logic', () => {
|
|
635
|
+
test('coalesceKey produces unique keys per chat+user', () => {
|
|
636
|
+
// We test the key format directly — the coalescing behavior is integration-level
|
|
637
|
+
const key1 = `chat1:user1`
|
|
638
|
+
const key2 = `chat1:user2`
|
|
639
|
+
const key3 = `chat2:user1`
|
|
640
|
+
expect(key1).not.toBe(key2)
|
|
641
|
+
expect(key1).not.toBe(key3)
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
test('messages combine with newline separator', () => {
|
|
645
|
+
// Simulate what the coalescing logic does: join texts with \n
|
|
646
|
+
const messages = ['Hello', 'How are you?', 'One more thing']
|
|
647
|
+
const combined = messages.join('\n')
|
|
648
|
+
expect(combined).toBe('Hello\nHow are you?\nOne more thing')
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
test('single message passes through unchanged', () => {
|
|
652
|
+
const messages = ['Hello']
|
|
653
|
+
const combined = messages.join('\n')
|
|
654
|
+
expect(combined).toBe('Hello')
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
test('empty messages produce empty combined text', () => {
|
|
658
|
+
const messages: string[] = []
|
|
659
|
+
const combined = messages.join('\n')
|
|
660
|
+
expect(combined).toBe('')
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
test('messages with newlines preserve internal structure', () => {
|
|
664
|
+
const messages = ['Line 1\nLine 2', 'Line 3']
|
|
665
|
+
const combined = messages.join('\n')
|
|
666
|
+
expect(combined).toBe('Line 1\nLine 2\nLine 3')
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// repairEscapedWhitespace — defends against LLM-side JSON escape bungles
|
|
672
|
+
// where real newlines come through as the literal two-char sequence `\n`.
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
describe('repairEscapedWhitespace', () => {
|
|
676
|
+
test('unescapes literal \\n when text has no real newlines', () => {
|
|
677
|
+
const input = 'Line one\\nLine two\\nLine three'
|
|
678
|
+
expect(repairEscapedWhitespace(input)).toBe('Line one\nLine two\nLine three')
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test('unescapes literal \\n\\n paragraph breaks', () => {
|
|
682
|
+
const input = 'Paragraph one.\\n\\nParagraph two.'
|
|
683
|
+
expect(repairEscapedWhitespace(input)).toBe('Paragraph one.\n\nParagraph two.')
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
test('handles the exact observed bug: html tags mixed with literal \\n', () => {
|
|
687
|
+
// Reproduces the actual stream_reply failure: a model produced a message
|
|
688
|
+
// with <b>/<code> tags and literal `\n` escape sequences instead of real
|
|
689
|
+
// newlines, and Telegram rendered the `\n` as visible characters.
|
|
690
|
+
const input = 'Audit done:\\n\\n<b>README.md</b>\\n• Missing <code>switchroom update</code>\\n• Missing <code>switchroom agent grant</code>'
|
|
691
|
+
const repaired = repairEscapedWhitespace(input)
|
|
692
|
+
expect(repaired).toBe('Audit done:\n\n<b>README.md</b>\n• Missing <code>switchroom update</code>\n• Missing <code>switchroom agent grant</code>')
|
|
693
|
+
// And the repaired text should still be recognized as Telegram HTML
|
|
694
|
+
// so the markdownToHtml pass-through works correctly.
|
|
695
|
+
expect(isLikelyTelegramHtml(repaired)).toBe(true)
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
test('leaves text alone when it already contains real newlines', () => {
|
|
699
|
+
// If the caller provided real newlines, we trust them completely and
|
|
700
|
+
// don't touch literal `\n` that may appear inside their content (e.g.
|
|
701
|
+
// a regex or shell snippet).
|
|
702
|
+
const input = 'Real newline here\nand a literal \\n in a regex example'
|
|
703
|
+
expect(repairEscapedWhitespace(input)).toBe(input)
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
test('leaves single-line text alone when it has no escape sequences', () => {
|
|
707
|
+
const input = 'Just a plain single-line message.'
|
|
708
|
+
expect(repairEscapedWhitespace(input)).toBe(input)
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
test('unescapes \\t and \\r as well', () => {
|
|
712
|
+
const input = 'Col1\\tCol2\\tCol3'
|
|
713
|
+
expect(repairEscapedWhitespace(input)).toBe('Col1\tCol2\tCol3')
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
test('unescapes \\" (quote) when present alongside \\n', () => {
|
|
717
|
+
const input = 'Say \\"hello\\"\\nnext line'
|
|
718
|
+
expect(repairEscapedWhitespace(input)).toBe('Say "hello"\nnext line')
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
test('preserves literal backslash sequences via \\\\', () => {
|
|
722
|
+
// `\\n` in the source is `\\` followed by `n`, which means the user
|
|
723
|
+
// literally wanted a backslash followed by the letter n, NOT a newline.
|
|
724
|
+
// Our order-aware unescape must protect `\\` before touching `\n`.
|
|
725
|
+
const input = 'Windows path: C:\\\\temp\\\\file.txt\\nnext line'
|
|
726
|
+
const out = repairEscapedWhitespace(input)
|
|
727
|
+
expect(out).toBe('Windows path: C:\\temp\\file.txt\nnext line')
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
test('end-to-end with markdownToHtml: repaired text renders correctly', () => {
|
|
731
|
+
// Full pipeline: broken input → repair → markdownToHtml → Telegram HTML.
|
|
732
|
+
const broken = '**Bold line**\\n\\n- bullet one\\n- bullet two'
|
|
733
|
+
const repaired = repairEscapedWhitespace(broken)
|
|
734
|
+
const html = markdownToHtml(repaired)
|
|
735
|
+
expect(html).toContain('<b>Bold line</b>')
|
|
736
|
+
// Real newlines should be present in the HTML output (Telegram renders
|
|
737
|
+
// them as actual line breaks in HTML parse mode).
|
|
738
|
+
expect(html).toContain('\n\n')
|
|
739
|
+
expect(html).toContain('- bullet one')
|
|
740
|
+
// Literal \n must not survive anywhere.
|
|
741
|
+
expect(html).not.toContain('\\n')
|
|
742
|
+
})
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
// sanitizeForTelegram — output invariants enforced pre-send
|
|
747
|
+
// ---------------------------------------------------------------------------
|
|
748
|
+
|
|
749
|
+
describe('sanitizeForTelegram', () => {
|
|
750
|
+
// ── Rule 1: strip ## headings ────────────────────────────────────────────
|
|
751
|
+
|
|
752
|
+
test('strips ## heading and converts to bold', () => {
|
|
753
|
+
const result = sanitizeForTelegram('## My Heading\n\nbody text')
|
|
754
|
+
expect(result).toContain('<b>My Heading</b>')
|
|
755
|
+
expect(result).not.toContain('## ')
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
test('strips ### heading and converts to bold', () => {
|
|
759
|
+
const result = sanitizeForTelegram('### Section\n\nbody')
|
|
760
|
+
expect(result).toContain('<b>Section</b>')
|
|
761
|
+
expect(result).not.toContain('### ')
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
test('strips #### heading and converts to bold', () => {
|
|
765
|
+
const result = sanitizeForTelegram('#### Sub\n\nbody')
|
|
766
|
+
expect(result).toContain('<b>Sub</b>')
|
|
767
|
+
expect(result).not.toContain('#### ')
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
test('strips # (h1) heading and converts to bold', () => {
|
|
771
|
+
const result = sanitizeForTelegram('# Title\n\nbody')
|
|
772
|
+
expect(result).toContain('<b>Title</b>')
|
|
773
|
+
expect(result).not.toContain('# Title')
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
// ── Rule 2: flatten nested bullets ──────────────────────────────────────
|
|
777
|
+
|
|
778
|
+
test('flattens 2-space-indented bullets', () => {
|
|
779
|
+
const result = sanitizeForTelegram('- top\n - sub')
|
|
780
|
+
expect(result).toContain('· sub')
|
|
781
|
+
expect(result).not.toContain(' - sub')
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
test('flattens 4-space-indented bullets', () => {
|
|
785
|
+
const result = sanitizeForTelegram('- top\n - deeply nested')
|
|
786
|
+
expect(result).toContain('· deeply nested')
|
|
787
|
+
expect(result).not.toContain(' - deeply nested')
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
test('flattens tab-indented bullets', () => {
|
|
791
|
+
const result = sanitizeForTelegram('- top\n\t- tabbed sub')
|
|
792
|
+
expect(result).toContain('· tabbed sub')
|
|
793
|
+
expect(result).not.toContain('\t- tabbed sub')
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
test('preserves unindented bullets unchanged', () => {
|
|
797
|
+
const result = sanitizeForTelegram('- item one\n- item two')
|
|
798
|
+
expect(result).toContain('- item one')
|
|
799
|
+
expect(result).toContain('- item two')
|
|
800
|
+
// No middle-dot substitution on top-level bullets
|
|
801
|
+
expect(result).not.toContain('· item one')
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
// ── Rule 3: collapse blank lines ────────────────────────────────────────
|
|
805
|
+
|
|
806
|
+
test('collapses 4 blank lines to 2', () => {
|
|
807
|
+
const result = sanitizeForTelegram('before\n\n\n\nafter')
|
|
808
|
+
expect(result).toBe('before\n\nafter')
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
test('collapses 3 blank lines to 2', () => {
|
|
812
|
+
const result = sanitizeForTelegram('a\n\n\nb')
|
|
813
|
+
expect(result).toBe('a\n\nb')
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
test('leaves exactly 2 blank lines alone', () => {
|
|
817
|
+
const result = sanitizeForTelegram('a\n\nb')
|
|
818
|
+
expect(result).toBe('a\n\nb')
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
// ── Rule 4: trailing whitespace ──────────────────────────────────────────
|
|
822
|
+
|
|
823
|
+
test('strips trailing spaces from lines', () => {
|
|
824
|
+
const result = sanitizeForTelegram('hello \nworld ')
|
|
825
|
+
expect(result).toBe('hello\nworld')
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
test('strips trailing tabs from lines', () => {
|
|
829
|
+
const result = sanitizeForTelegram('hello\t\t\nworld')
|
|
830
|
+
expect(result).toBe('hello\nworld')
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
// ── Rule 5: HTML escape inside code/pre ─────────────────────────────────
|
|
834
|
+
|
|
835
|
+
test('HTML-escapes bare < and > inside <code> block', () => {
|
|
836
|
+
const result = sanitizeForTelegram('<code>a < b && c > d</code>')
|
|
837
|
+
expect(result).toContain('<code>a < b')
|
|
838
|
+
expect(result).toContain('> d</code>')
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
test('HTML-escapes bare & inside <code> block', () => {
|
|
842
|
+
const result = sanitizeForTelegram('<code>foo & bar</code>')
|
|
843
|
+
expect(result).toContain('<code>foo & bar</code>')
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
test('HTML-escapes bare < and > inside <pre> block', () => {
|
|
847
|
+
const result = sanitizeForTelegram('<pre><code>if a < b</code></pre>')
|
|
848
|
+
expect(result).toContain('< b')
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
test('does not double-escape already-escaped & in <code>', () => {
|
|
852
|
+
const result = sanitizeForTelegram('<code>a & b</code>')
|
|
853
|
+
// Must remain single-escaped, not become &amp;
|
|
854
|
+
expect(result).toContain('<code>a & b</code>')
|
|
855
|
+
expect(result).not.toContain('&amp;')
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
test('does not double-escape < in <code>', () => {
|
|
859
|
+
const result = sanitizeForTelegram('<code><div></code>')
|
|
860
|
+
expect(result).toContain('<code><div></code>')
|
|
861
|
+
expect(result).not.toContain('&lt;')
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
test('does not double-escape { numeric entity in <code>', () => {
|
|
865
|
+
const result = sanitizeForTelegram('<code>{ x }</code>')
|
|
866
|
+
expect(result).toContain('{')
|
|
867
|
+
expect(result).not.toContain('&#123;')
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
// ── Code block exclusion from structural rules ───────────────────────────
|
|
871
|
+
|
|
872
|
+
test('does not strip ## heading inside <code> block', () => {
|
|
873
|
+
const result = sanitizeForTelegram('<code>## not a heading</code>')
|
|
874
|
+
// The ## stays; only < > & are touched inside code
|
|
875
|
+
expect(result).toContain('## not a heading')
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
test('does not flatten bullets inside <code> block', () => {
|
|
879
|
+
const result = sanitizeForTelegram('<code> - not flattened</code>')
|
|
880
|
+
// The indented bullet stays verbatim inside code
|
|
881
|
+
expect(result).toContain(' - not flattened')
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
test('does not strip ## heading inside <pre> block', () => {
|
|
885
|
+
const result = sanitizeForTelegram('<pre><code class="language-bash"># comment\n## heading\n</code></pre>')
|
|
886
|
+
expect(result).toContain('## heading')
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
// ── Idempotency ──────────────────────────────────────────────────────────
|
|
890
|
+
|
|
891
|
+
test('is idempotent for heading conversion', () => {
|
|
892
|
+
const once = sanitizeForTelegram('## Heading\n\nbody')
|
|
893
|
+
const twice = sanitizeForTelegram(once)
|
|
894
|
+
expect(twice).toBe(once)
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
test('is idempotent for bullet flattening', () => {
|
|
898
|
+
const once = sanitizeForTelegram('- top\n - sub\n - deep')
|
|
899
|
+
const twice = sanitizeForTelegram(once)
|
|
900
|
+
expect(twice).toBe(once)
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
test('is idempotent for blank-line collapse', () => {
|
|
904
|
+
const once = sanitizeForTelegram('a\n\n\n\nb')
|
|
905
|
+
const twice = sanitizeForTelegram(once)
|
|
906
|
+
expect(twice).toBe(once)
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
test('is idempotent for code-block escaping', () => {
|
|
910
|
+
const once = sanitizeForTelegram('<code>a < b & c > d</code>')
|
|
911
|
+
const twice = sanitizeForTelegram(once)
|
|
912
|
+
expect(twice).toBe(once)
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
test('is idempotent for a combined realistic message', () => {
|
|
916
|
+
const input = [
|
|
917
|
+
'## Status Report',
|
|
918
|
+
'',
|
|
919
|
+
'- top item',
|
|
920
|
+
' - sub item one',
|
|
921
|
+
' - sub item two',
|
|
922
|
+
'',
|
|
923
|
+
'',
|
|
924
|
+
'',
|
|
925
|
+
'Here is some <code>a < b</code> inline code.',
|
|
926
|
+
].join('\n')
|
|
927
|
+
const once = sanitizeForTelegram(input)
|
|
928
|
+
const twice = sanitizeForTelegram(once)
|
|
929
|
+
expect(twice).toBe(once)
|
|
930
|
+
})
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
// ---------------------------------------------------------------------------
|
|
934
|
+
// Markdown table rendering
|
|
935
|
+
// ---------------------------------------------------------------------------
|
|
936
|
+
|
|
937
|
+
describe('markdownToHtml — markdown table rendering', () => {
|
|
938
|
+
// 2-col 3-row → bullet list
|
|
939
|
+
test('2-col 3-row renders as bullet list', () => {
|
|
940
|
+
const input = [
|
|
941
|
+
'| Name | Value |',
|
|
942
|
+
'| --- | --- |',
|
|
943
|
+
'| Alpha | 1 |',
|
|
944
|
+
'| Beta | 2 |',
|
|
945
|
+
'| Gamma | 3 |',
|
|
946
|
+
].join('\n')
|
|
947
|
+
const result = markdownToHtml(input)
|
|
948
|
+
// Header line present
|
|
949
|
+
expect(result).toContain('Name / Value')
|
|
950
|
+
// Each row is a bullet with <b> first column
|
|
951
|
+
expect(result).toContain('• <b>Alpha</b>')
|
|
952
|
+
expect(result).toContain('• <b>Beta</b>')
|
|
953
|
+
expect(result).toContain('• <b>Gamma</b>')
|
|
954
|
+
// Values appended after dash
|
|
955
|
+
expect(result).toContain('— 1')
|
|
956
|
+
expect(result).toContain('— 2')
|
|
957
|
+
expect(result).toContain('— 3')
|
|
958
|
+
// Must NOT contain any raw table markdown pipes
|
|
959
|
+
expect(result).not.toContain('| --- |')
|
|
960
|
+
expect(result).not.toContain('<table>')
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
// 3-col 4-row → bullet list (still within ≤3 cols AND ≤6 rows)
|
|
964
|
+
test('3-col 4-row renders as bullet list', () => {
|
|
965
|
+
const input = [
|
|
966
|
+
'| Tool | Status | Notes |',
|
|
967
|
+
'| ---- | ------ | ----- |',
|
|
968
|
+
'| bun | ok | fast |',
|
|
969
|
+
'| tsc | ok | strict |',
|
|
970
|
+
'| eslint | warn | fixable |',
|
|
971
|
+
'| vitest | skip | optional |',
|
|
972
|
+
].join('\n')
|
|
973
|
+
const result = markdownToHtml(input)
|
|
974
|
+
expect(result).toContain('• <b>bun</b>')
|
|
975
|
+
expect(result).toContain('• <b>tsc</b>')
|
|
976
|
+
// Third column appended too
|
|
977
|
+
expect(result).toContain('fast')
|
|
978
|
+
expect(result).toContain('strict')
|
|
979
|
+
expect(result).not.toContain('| ---- |')
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
// 4-col 3-row → <pre> block (4 cols exceeds limit)
|
|
983
|
+
test('4-col 3-row renders as <pre> block', () => {
|
|
984
|
+
const input = [
|
|
985
|
+
'| A | B | C | D |',
|
|
986
|
+
'| - | - | - | - |',
|
|
987
|
+
'| 1 | 2 | 3 | 4 |',
|
|
988
|
+
'| 5 | 6 | 7 | 8 |',
|
|
989
|
+
'| 9 | 0 | 1 | 2 |',
|
|
990
|
+
].join('\n')
|
|
991
|
+
const result = markdownToHtml(input)
|
|
992
|
+
expect(result).toContain('<pre>')
|
|
993
|
+
expect(result).toContain('</pre>')
|
|
994
|
+
// Column headers should appear in the pre block
|
|
995
|
+
expect(result).toContain('A')
|
|
996
|
+
expect(result).toContain('B')
|
|
997
|
+
// Must not produce a bullet list
|
|
998
|
+
expect(result).not.toContain('• <b>')
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
// 3-col 8-row → <pre> block (8 rows exceeds ≤6 limit)
|
|
1002
|
+
test('3-col 8-row renders as <pre> block', () => {
|
|
1003
|
+
const rows = Array.from({ length: 8 }, (_, i) => `| Row${i + 1} | X${i} | Y${i} |`)
|
|
1004
|
+
const input = [
|
|
1005
|
+
'| Name | ColX | ColY |',
|
|
1006
|
+
'| ---- | ---- | ---- |',
|
|
1007
|
+
...rows,
|
|
1008
|
+
].join('\n')
|
|
1009
|
+
const result = markdownToHtml(input)
|
|
1010
|
+
expect(result).toContain('<pre>')
|
|
1011
|
+
expect(result).toContain('</pre>')
|
|
1012
|
+
expect(result).not.toContain('• <b>')
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
// Pipe in plain prose is NOT a table
|
|
1016
|
+
test('plain prose with a pipe is not converted to a table', () => {
|
|
1017
|
+
const input = 'Run echo foo | bar to see output'
|
|
1018
|
+
const result = markdownToHtml(input)
|
|
1019
|
+
expect(result).toContain('echo foo | bar')
|
|
1020
|
+
expect(result).not.toContain('• <b>')
|
|
1021
|
+
expect(result).not.toContain('<pre>')
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
// Pipe in code block is not a table
|
|
1025
|
+
test('pipe inside fenced code block is left verbatim', () => {
|
|
1026
|
+
const input = [
|
|
1027
|
+
'```bash',
|
|
1028
|
+
'| Name | Value |',
|
|
1029
|
+
'| --- | --- |',
|
|
1030
|
+
'| foo | bar |',
|
|
1031
|
+
'```',
|
|
1032
|
+
].join('\n')
|
|
1033
|
+
const result = markdownToHtml(input)
|
|
1034
|
+
// Should be inside <pre><code>, not a rendered table
|
|
1035
|
+
expect(result).toContain('<pre>')
|
|
1036
|
+
expect(result).toContain('| Name | Value |')
|
|
1037
|
+
expect(result).not.toContain('• <b>')
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
// Table with empty cells
|
|
1041
|
+
test('table with empty cells is handled gracefully', () => {
|
|
1042
|
+
const input = [
|
|
1043
|
+
'| Key | Value |',
|
|
1044
|
+
'| --- | ----- |',
|
|
1045
|
+
'| present | |',
|
|
1046
|
+
'| | orphan |',
|
|
1047
|
+
].join('\n')
|
|
1048
|
+
const result = markdownToHtml(input)
|
|
1049
|
+
// Should produce output without crashing; empty cells rendered as empty/—
|
|
1050
|
+
expect(result).toContain('• <b>present</b>')
|
|
1051
|
+
// No raw markdown pipes in output
|
|
1052
|
+
expect(result).not.toContain('| --- |')
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
// Table preceded and followed by paragraph text — only the table transforms
|
|
1056
|
+
test('table inside paragraph text: only the table block transforms', () => {
|
|
1057
|
+
const input = [
|
|
1058
|
+
'Before paragraph.',
|
|
1059
|
+
'',
|
|
1060
|
+
'| Name | Score |',
|
|
1061
|
+
'| ---- | ----- |',
|
|
1062
|
+
'| Alice | 95 |',
|
|
1063
|
+
'| Bob | 87 |',
|
|
1064
|
+
'',
|
|
1065
|
+
'After paragraph.',
|
|
1066
|
+
].join('\n')
|
|
1067
|
+
const result = markdownToHtml(input)
|
|
1068
|
+
// Prose preserved
|
|
1069
|
+
expect(result).toContain('Before paragraph.')
|
|
1070
|
+
expect(result).toContain('After paragraph.')
|
|
1071
|
+
// Table converted
|
|
1072
|
+
expect(result).toContain('• <b>Alice</b>')
|
|
1073
|
+
expect(result).toContain('• <b>Bob</b>')
|
|
1074
|
+
// No raw table markdown remains
|
|
1075
|
+
expect(result).not.toContain('| ---- |')
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
// HTML entities in cell content are properly escaped
|
|
1079
|
+
test('cell content with ampersand is safely escaped', () => {
|
|
1080
|
+
const input = [
|
|
1081
|
+
'| Operator | Meaning |',
|
|
1082
|
+
'| -------- | ------- |',
|
|
1083
|
+
'| AND | a & b |',
|
|
1084
|
+
'| OR | x & y |',
|
|
1085
|
+
].join('\n')
|
|
1086
|
+
const result = markdownToHtml(input)
|
|
1087
|
+
// & in cell content must be entity-escaped
|
|
1088
|
+
expect(result).toContain('&')
|
|
1089
|
+
// Output is still a bullet list
|
|
1090
|
+
expect(result).toContain('• <b>AND</b>')
|
|
1091
|
+
expect(result).toContain('• <b>OR</b>')
|
|
1092
|
+
})
|
|
1093
|
+
})
|