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,1045 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildDashboard,
|
|
4
|
+
buildDashboardText,
|
|
5
|
+
buildDashboardKeyboard,
|
|
6
|
+
buildRemoveConfirmKeyboard,
|
|
7
|
+
encodeCallbackData,
|
|
8
|
+
parseCallbackData,
|
|
9
|
+
isQuotaHot,
|
|
10
|
+
escapeHtml,
|
|
11
|
+
QUOTA_HOT_THRESHOLD_PCT,
|
|
12
|
+
type DashboardState,
|
|
13
|
+
type DashboardSlot,
|
|
14
|
+
} from "../auth-dashboard";
|
|
15
|
+
|
|
16
|
+
function mkSlot(overrides: Partial<DashboardSlot> = {}): DashboardSlot {
|
|
17
|
+
return {
|
|
18
|
+
slot: "default",
|
|
19
|
+
active: false,
|
|
20
|
+
health: "healthy",
|
|
21
|
+
quotaExhaustedUntil: null,
|
|
22
|
+
fiveHourPct: null,
|
|
23
|
+
sevenDayPct: null,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("encodeCallbackData / parseCallbackData round-trip", () => {
|
|
29
|
+
it("refresh preserves agent", () => {
|
|
30
|
+
const encoded = encodeCallbackData({ kind: "refresh", agent: "clerk" });
|
|
31
|
+
expect(encoded).toBe("auth:refresh:clerk");
|
|
32
|
+
expect(parseCallbackData(encoded)).toEqual({ kind: "refresh", agent: "clerk" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("reauth with slot preserves both", () => {
|
|
36
|
+
const encoded = encodeCallbackData({ kind: "reauth", agent: "klanker", slot: "personal" });
|
|
37
|
+
expect(parseCallbackData(encoded)).toEqual({ kind: "reauth", agent: "klanker", slot: "personal" });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("reauth without slot works", () => {
|
|
41
|
+
const encoded = encodeCallbackData({ kind: "reauth", agent: "klanker" });
|
|
42
|
+
expect(parseCallbackData(encoded)).toEqual({ kind: "reauth", agent: "klanker" });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("use requires a slot", () => {
|
|
46
|
+
const encoded = encodeCallbackData({ kind: "use", agent: "clerk", slot: "personal" });
|
|
47
|
+
expect(parseCallbackData(encoded)).toEqual({ kind: "use", agent: "clerk", slot: "personal" });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rm and confirm-rm round-trip", () => {
|
|
51
|
+
expect(parseCallbackData(encodeCallbackData({ kind: "rm", agent: "clerk", slot: "x" }))).toEqual({
|
|
52
|
+
kind: "rm",
|
|
53
|
+
agent: "clerk",
|
|
54
|
+
slot: "x",
|
|
55
|
+
});
|
|
56
|
+
expect(parseCallbackData(encodeCallbackData({ kind: "confirm-rm", agent: "clerk", slot: "x" }))).toEqual({
|
|
57
|
+
kind: "confirm-rm",
|
|
58
|
+
agent: "clerk",
|
|
59
|
+
slot: "x",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("fallback/usage/add all round-trip", () => {
|
|
64
|
+
for (const kind of ["fallback", "usage", "add"] as const) {
|
|
65
|
+
const encoded = encodeCallbackData({ kind, agent: "clerk" });
|
|
66
|
+
expect(parseCallbackData(encoded)).toEqual({ kind, agent: "clerk" });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("rejects unknown verbs as noop", () => {
|
|
71
|
+
expect(parseCallbackData("auth:unknown:clerk")).toEqual({ kind: "noop" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects malicious agent names as noop (injection guard)", () => {
|
|
75
|
+
expect(parseCallbackData("auth:reauth:evil;rm -rf /")).toEqual({ kind: "noop" });
|
|
76
|
+
expect(parseCallbackData("auth:reauth:$(whoami)")).toEqual({ kind: "noop" });
|
|
77
|
+
expect(parseCallbackData("auth:reauth:../../etc/passwd")).toEqual({ kind: "noop" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects malicious slot names as noop", () => {
|
|
81
|
+
expect(parseCallbackData("auth:use:clerk:evil slot")).toEqual({ kind: "noop" });
|
|
82
|
+
expect(parseCallbackData("auth:use:clerk:../../x")).toEqual({ kind: "noop" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("rejects non-auth prefixes as noop", () => {
|
|
86
|
+
expect(parseCallbackData("perm:allow:abc")).toEqual({ kind: "noop" });
|
|
87
|
+
expect(parseCallbackData("random garbage")).toEqual({ kind: "noop" });
|
|
88
|
+
expect(parseCallbackData("")).toEqual({ kind: "noop" });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("realistic-length payloads fit Telegram's 64-byte callback_data cap", () => {
|
|
92
|
+
// Longest prefix is 'auth:confirm-rm:' = 16 chars. Typical agent
|
|
93
|
+
// names (clerk / klanker / lawgpt) and slot names (default /
|
|
94
|
+
// personal / work / backup) are <= 16 chars each in practice.
|
|
95
|
+
//
|
|
96
|
+
// Agent names CAN go up to 64 chars in the config schema, but if a
|
|
97
|
+
// user picks e.g. a 40-char agent name their dashboard callbacks
|
|
98
|
+
// would exceed the cap and break silently. Document as an
|
|
99
|
+
// upstream limit rather than enforce here; the scaffold CLI also
|
|
100
|
+
// doesn't warn on this today.
|
|
101
|
+
const encoded = encodeCallbackData({
|
|
102
|
+
kind: "confirm-rm",
|
|
103
|
+
agent: "a".repeat(16),
|
|
104
|
+
slot: "b".repeat(16),
|
|
105
|
+
});
|
|
106
|
+
expect(encoded.length).toBeLessThanOrEqual(64);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("buildDashboardText", () => {
|
|
111
|
+
const base: DashboardState = {
|
|
112
|
+
agent: "clerk",
|
|
113
|
+
bankId: "assistant",
|
|
114
|
+
plan: "max",
|
|
115
|
+
slots: [mkSlot({ slot: "default", active: true })],
|
|
116
|
+
quotaHot: false,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
it("renders header with agent + bank + plan", () => {
|
|
120
|
+
const text = buildDashboardText(base);
|
|
121
|
+
expect(text).toContain("Auth");
|
|
122
|
+
expect(text).toContain("clerk");
|
|
123
|
+
expect(text).toContain("assistant");
|
|
124
|
+
expect(text).toContain("max");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("marks the active slot with ● and a label", () => {
|
|
128
|
+
const text = buildDashboardText(base);
|
|
129
|
+
expect(text).toContain("●");
|
|
130
|
+
expect(text).toContain("<code>default</code>");
|
|
131
|
+
expect(text).toContain("(active)");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("renders quota-exhausted slots with resets-in hint", () => {
|
|
135
|
+
const until = Date.now() + 30 * 60_000;
|
|
136
|
+
const text = buildDashboardText({
|
|
137
|
+
...base,
|
|
138
|
+
slots: [mkSlot({ slot: "default", active: true, health: "quota-exhausted", quotaExhaustedUntil: until })],
|
|
139
|
+
});
|
|
140
|
+
expect(text).toMatch(/resets in ~\d+m/);
|
|
141
|
+
expect(text).toContain("⚠️");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("renders utilization when present", () => {
|
|
145
|
+
const text = buildDashboardText({
|
|
146
|
+
...base,
|
|
147
|
+
slots: [mkSlot({ slot: "default", active: true, fiveHourPct: 42, sevenDayPct: 61 })],
|
|
148
|
+
});
|
|
149
|
+
expect(text).toContain("5h: 42%");
|
|
150
|
+
expect(text).toContain("7d: 61%");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("escapes HTML in agent + slot names (XSS/injection guard)", () => {
|
|
154
|
+
const text = buildDashboardText({
|
|
155
|
+
...base,
|
|
156
|
+
agent: "<evil>",
|
|
157
|
+
slots: [mkSlot({ slot: "<also>", active: true })],
|
|
158
|
+
});
|
|
159
|
+
expect(text).toContain("<evil>");
|
|
160
|
+
expect(text).toContain("<also>");
|
|
161
|
+
expect(text).not.toContain("<evil>");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("shows empty-state message when no slots exist", () => {
|
|
165
|
+
const text = buildDashboardText({ ...base, slots: [] });
|
|
166
|
+
expect(text).toMatch(/no account slots/i);
|
|
167
|
+
expect(text).toContain("Add slot");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("buildDashboardKeyboard", () => {
|
|
172
|
+
it("shows [Reauth active] + [Add slot] in the first row when active slot exists", () => {
|
|
173
|
+
const kb = buildDashboardKeyboard({
|
|
174
|
+
agent: "clerk",
|
|
175
|
+
bankId: "assistant",
|
|
176
|
+
plan: "max",
|
|
177
|
+
slots: [mkSlot({ slot: "default", active: true })],
|
|
178
|
+
quotaHot: false,
|
|
179
|
+
});
|
|
180
|
+
const row0 = kb.inline_keyboard[0];
|
|
181
|
+
expect(row0[0].text).toMatch(/Reauth/);
|
|
182
|
+
expect(row0[0].text).toContain("default");
|
|
183
|
+
expect(row0[1].text).toMatch(/Add slot/);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("renders [Use: X] for every non-active slot (up to 3)", () => {
|
|
187
|
+
const kb = buildDashboardKeyboard({
|
|
188
|
+
agent: "clerk",
|
|
189
|
+
bankId: "assistant",
|
|
190
|
+
slots: [
|
|
191
|
+
mkSlot({ slot: "default", active: true }),
|
|
192
|
+
mkSlot({ slot: "personal", active: false }),
|
|
193
|
+
mkSlot({ slot: "work", active: false }),
|
|
194
|
+
],
|
|
195
|
+
quotaHot: false,
|
|
196
|
+
});
|
|
197
|
+
const useButtons = kb.inline_keyboard.flat().filter((b) => b.text.startsWith("Use:"));
|
|
198
|
+
expect(useButtons).toHaveLength(2);
|
|
199
|
+
expect(useButtons[0].text).toContain("personal");
|
|
200
|
+
expect(useButtons[1].text).toContain("work");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("never shows [Fall back now] (removed in v0.6.11 — Switch primary is the operator-facing surface; the auto-fallback poller handles the automatic case)", () => {
|
|
204
|
+
const cold = buildDashboardKeyboard({
|
|
205
|
+
agent: "clerk",
|
|
206
|
+
bankId: "a",
|
|
207
|
+
slots: [mkSlot({ active: true })],
|
|
208
|
+
quotaHot: false,
|
|
209
|
+
});
|
|
210
|
+
const hot = buildDashboardKeyboard({
|
|
211
|
+
agent: "clerk",
|
|
212
|
+
bankId: "a",
|
|
213
|
+
slots: [mkSlot({ active: true, health: "quota-exhausted" })],
|
|
214
|
+
quotaHot: true,
|
|
215
|
+
});
|
|
216
|
+
const coldTexts = cold.inline_keyboard.flat().map((b) => b.text);
|
|
217
|
+
const hotTexts = hot.inline_keyboard.flat().map((b) => b.text);
|
|
218
|
+
expect(coldTexts.some((t) => t.includes("Fall back"))).toBe(false);
|
|
219
|
+
expect(hotTexts.some((t) => t.includes("Fall back"))).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("always ends with a Refresh button", () => {
|
|
223
|
+
const kb = buildDashboardKeyboard({
|
|
224
|
+
agent: "clerk",
|
|
225
|
+
bankId: "a",
|
|
226
|
+
slots: [mkSlot({ active: true })],
|
|
227
|
+
quotaHot: false,
|
|
228
|
+
});
|
|
229
|
+
const lastRow = kb.inline_keyboard[kb.inline_keyboard.length - 1];
|
|
230
|
+
expect(lastRow[0].text).toContain("Refresh");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("encodes agent + slot correctly in callback_data", () => {
|
|
234
|
+
const kb = buildDashboardKeyboard({
|
|
235
|
+
agent: "klanker",
|
|
236
|
+
bankId: "assistant",
|
|
237
|
+
slots: [mkSlot({ slot: "default", active: true }), mkSlot({ slot: "backup", active: false })],
|
|
238
|
+
quotaHot: false,
|
|
239
|
+
});
|
|
240
|
+
const flat = kb.inline_keyboard.flat();
|
|
241
|
+
for (const btn of flat) {
|
|
242
|
+
if ("callback_data" in btn && btn.callback_data) {
|
|
243
|
+
expect(btn.callback_data.startsWith("auth:")).toBe(true);
|
|
244
|
+
// All payloads fit within Telegram's callback_data cap.
|
|
245
|
+
expect(btn.callback_data.length).toBeLessThanOrEqual(64);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("buildRemoveConfirmKeyboard", () => {
|
|
252
|
+
it("shows a confirm + cancel two-button keyboard", () => {
|
|
253
|
+
const kb = buildRemoveConfirmKeyboard("clerk", "personal");
|
|
254
|
+
const flat = kb.inline_keyboard.flat();
|
|
255
|
+
expect(flat).toHaveLength(2);
|
|
256
|
+
expect(flat[0].text).toContain("Confirm remove");
|
|
257
|
+
expect(flat[0].text).toContain("personal");
|
|
258
|
+
expect(flat[1].text).toContain("Cancel");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("confirm button uses confirm-rm action; cancel refreshes", () => {
|
|
262
|
+
const kb = buildRemoveConfirmKeyboard("clerk", "personal");
|
|
263
|
+
const flat = kb.inline_keyboard.flat();
|
|
264
|
+
if ("callback_data" in flat[0] && flat[0].callback_data) {
|
|
265
|
+
expect(flat[0].callback_data).toBe("auth:confirm-rm:clerk:personal");
|
|
266
|
+
}
|
|
267
|
+
if ("callback_data" in flat[1] && flat[1].callback_data) {
|
|
268
|
+
expect(flat[1].callback_data).toBe("auth:refresh:clerk");
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("isQuotaHot", () => {
|
|
274
|
+
it("returns true when any slot is quota-exhausted", () => {
|
|
275
|
+
expect(isQuotaHot([mkSlot({ health: "quota-exhausted" })])).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("returns true when 5h utilization crosses the threshold", () => {
|
|
279
|
+
expect(isQuotaHot([mkSlot({ fiveHourPct: QUOTA_HOT_THRESHOLD_PCT })])).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("returns true when 7d utilization crosses the threshold", () => {
|
|
283
|
+
expect(isQuotaHot([mkSlot({ sevenDayPct: QUOTA_HOT_THRESHOLD_PCT })])).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("returns false when all slots are cool", () => {
|
|
287
|
+
expect(isQuotaHot([mkSlot({ fiveHourPct: 20, sevenDayPct: 40 })])).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("returns false on empty slot set", () => {
|
|
291
|
+
expect(isQuotaHot([])).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("buildDashboard — full integration", () => {
|
|
296
|
+
it("returns { text, keyboard }", () => {
|
|
297
|
+
const result = buildDashboard({
|
|
298
|
+
agent: "clerk",
|
|
299
|
+
bankId: "assistant",
|
|
300
|
+
plan: "max",
|
|
301
|
+
slots: [mkSlot({ slot: "default", active: true })],
|
|
302
|
+
quotaHot: false,
|
|
303
|
+
});
|
|
304
|
+
expect(result.text.length).toBeGreaterThan(0);
|
|
305
|
+
expect(result.keyboard.inline_keyboard.length).toBeGreaterThan(0);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("escapeHtml", () => {
|
|
310
|
+
it("escapes angle brackets, ampersands, and quotes", () => {
|
|
311
|
+
expect(escapeHtml('<foo bar="baz">&')).toBe("<foo bar="baz">&");
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ─── Account-level dashboard ──────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
import {
|
|
318
|
+
buildAccountConfirmKeyboard,
|
|
319
|
+
ACCOUNTS_DISPLAY_CAP,
|
|
320
|
+
CALLBACK_BUDGET_BYTES,
|
|
321
|
+
isSafeAccountLabel,
|
|
322
|
+
type AccountSummary,
|
|
323
|
+
type AccountHealth,
|
|
324
|
+
} from "../auth-dashboard";
|
|
325
|
+
|
|
326
|
+
function mkAccount(overrides: Partial<AccountSummary> = {}): AccountSummary {
|
|
327
|
+
return {
|
|
328
|
+
label: "default",
|
|
329
|
+
health: "healthy",
|
|
330
|
+
enabledHere: false,
|
|
331
|
+
...overrides,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function mkState(overrides: Partial<DashboardState> = {}): DashboardState {
|
|
336
|
+
return {
|
|
337
|
+
agent: "clerk",
|
|
338
|
+
bankId: "clerk",
|
|
339
|
+
plan: "max",
|
|
340
|
+
rateLimitTier: null,
|
|
341
|
+
slots: [mkSlot({ active: true, health: "active" })],
|
|
342
|
+
quotaHot: false,
|
|
343
|
+
generatedAt: "2026-05-03T12:00:00Z",
|
|
344
|
+
pendingSessionSlot: null,
|
|
345
|
+
...overrides,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
describe("isSafeAccountLabel", () => {
|
|
350
|
+
it("accepts the CLI-validated regex including '.' for labels like acme.team", () => {
|
|
351
|
+
expect(isSafeAccountLabel("default")).toBe(true);
|
|
352
|
+
expect(isSafeAccountLabel("acme.team")).toBe(true);
|
|
353
|
+
expect(isSafeAccountLabel("ken_personal")).toBe(true);
|
|
354
|
+
expect(isSafeAccountLabel("co-2024")).toBe(true);
|
|
355
|
+
expect(isSafeAccountLabel("a")).toBe(true);
|
|
356
|
+
expect(isSafeAccountLabel("a".repeat(64))).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("accepts email-shaped labels (@ + . _ - allowed)", () => {
|
|
360
|
+
// Mirror of the CLI's regex expansion — labels can be the
|
|
361
|
+
// operator's actual Anthropic email so the JTBD's "the user
|
|
362
|
+
// manages accounts" reads as the identities they already know.
|
|
363
|
+
expect(isSafeAccountLabel("pixsoul@gmail.com")).toBe(true);
|
|
364
|
+
expect(isSafeAccountLabel("ken+work@example.com")).toBe(true);
|
|
365
|
+
expect(isSafeAccountLabel("a@b")).toBe(true);
|
|
366
|
+
expect(isSafeAccountLabel("user.name+tag@subdomain.example.co")).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("rejects empty, oversized, and dangerous characters", () => {
|
|
370
|
+
expect(isSafeAccountLabel("")).toBe(false);
|
|
371
|
+
expect(isSafeAccountLabel("a".repeat(65))).toBe(false);
|
|
372
|
+
expect(isSafeAccountLabel("with space")).toBe(false);
|
|
373
|
+
expect(isSafeAccountLabel("a/b")).toBe(false);
|
|
374
|
+
// `:` is the callback_data separator — must never be allowed in a
|
|
375
|
+
// label or the dashboard parser splits the wrong way.
|
|
376
|
+
expect(isSafeAccountLabel("a:b")).toBe(false);
|
|
377
|
+
expect(isSafeAccountLabel("a;rm -rf")).toBe(false);
|
|
378
|
+
expect(isSafeAccountLabel("../escape")).toBe(false);
|
|
379
|
+
expect(isSafeAccountLabel('foo"bar')).toBe(false);
|
|
380
|
+
expect(isSafeAccountLabel("foo'bar")).toBe(false);
|
|
381
|
+
expect(isSafeAccountLabel("foo|bar")).toBe(false);
|
|
382
|
+
expect(isSafeAccountLabel("foo&bar")).toBe(false);
|
|
383
|
+
expect(isSafeAccountLabel("foo<bar")).toBe(false);
|
|
384
|
+
expect(isSafeAccountLabel("foo>bar")).toBe(false);
|
|
385
|
+
// Unicode keeps out — labels stay ASCII for filesystem sanity.
|
|
386
|
+
expect(isSafeAccountLabel("fooébar")).toBe(false);
|
|
387
|
+
expect(isSafeAccountLabel("ken@gmaіl.com")).toBe(false); // Cyrillic і
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe("encodeCallbackData / parseCallbackData — account verbs", () => {
|
|
392
|
+
it("account-enable round-trips with a simple label", () => {
|
|
393
|
+
const action = { kind: "account-enable" as const, agent: "clerk", label: "work" };
|
|
394
|
+
const encoded = encodeCallbackData(action);
|
|
395
|
+
expect(encoded).toBe("auth:ae:clerk:work");
|
|
396
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("account-disable round-trips", () => {
|
|
400
|
+
const action = { kind: "account-disable" as const, agent: "clerk", label: "work" };
|
|
401
|
+
const encoded = encodeCallbackData(action);
|
|
402
|
+
expect(encoded).toBe("auth:ad:clerk:work");
|
|
403
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("confirm-account-enable round-trips", () => {
|
|
407
|
+
const action = { kind: "confirm-account-enable" as const, agent: "klanker", label: "work" };
|
|
408
|
+
const encoded = encodeCallbackData(action);
|
|
409
|
+
expect(encoded).toBe("auth:cae:klanker:work");
|
|
410
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("confirm-account-disable round-trips", () => {
|
|
414
|
+
const action = { kind: "confirm-account-disable" as const, agent: "klanker", label: "work" };
|
|
415
|
+
const encoded = encodeCallbackData(action);
|
|
416
|
+
expect(encoded).toBe("auth:cad:klanker:work");
|
|
417
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("share-fleet round-trips (no label segment)", () => {
|
|
421
|
+
const action = { kind: "share-fleet" as const, agent: "clerk" };
|
|
422
|
+
const encoded = encodeCallbackData(action);
|
|
423
|
+
expect(encoded).toBe("auth:sf:clerk");
|
|
424
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("preserves labels with '.' through the round-trip (acme.team)", () => {
|
|
428
|
+
const action = { kind: "account-enable" as const, agent: "clerk", label: "acme.team" };
|
|
429
|
+
const encoded = encodeCallbackData(action);
|
|
430
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("preserves email-shaped labels through the round-trip", () => {
|
|
434
|
+
// Headline use case of the regex expansion — operators want the
|
|
435
|
+
// dashboard's `✓ pixsoul@gmail.com` button to round-trip cleanly
|
|
436
|
+
// when tapped. The `@` and `+` chars must survive the colon-split
|
|
437
|
+
// parser without being mistaken for a separator.
|
|
438
|
+
const cases = [
|
|
439
|
+
{ kind: "account-enable" as const, agent: "clerk", label: "pixsoul@gmail.com" },
|
|
440
|
+
{ kind: "account-disable" as const, agent: "klanker", label: "ken+work@example.com" },
|
|
441
|
+
{ kind: "confirm-account-enable" as const, agent: "finn", label: "name.tag+filter@subdomain.co" },
|
|
442
|
+
];
|
|
443
|
+
for (const action of cases) {
|
|
444
|
+
const encoded = encodeCallbackData(action);
|
|
445
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("rejects malformed account labels (parses to noop)", () => {
|
|
450
|
+
expect(parseCallbackData("auth:ae:clerk:bad label")).toEqual({ kind: "noop" });
|
|
451
|
+
expect(parseCallbackData("auth:ae:clerk:..")).toEqual({ kind: "noop" });
|
|
452
|
+
expect(parseCallbackData("auth:ae:clerk:")).toEqual({ kind: "noop" });
|
|
453
|
+
expect(parseCallbackData("auth:ae:clerk")).toEqual({ kind: "noop" }); // missing label segment
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("rejects malformed agent in account verbs", () => {
|
|
457
|
+
expect(parseCallbackData("auth:ae:bad agent:work")).toEqual({ kind: "noop" });
|
|
458
|
+
expect(parseCallbackData("auth:ae::work")).toEqual({ kind: "noop" });
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("rejects payloads beyond the 64-byte cap as noop", () => {
|
|
462
|
+
const oversize = "auth:ae:" + "a".repeat(80) + ":" + "b".repeat(80);
|
|
463
|
+
expect(parseCallbackData(oversize)).toEqual({ kind: "noop" });
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe("buildDashboardKeyboard — accounts section", () => {
|
|
468
|
+
function rows(state: DashboardState): Array<Array<{ text: string; callback_data?: string }>> {
|
|
469
|
+
return buildDashboardKeyboard(state).inline_keyboard as unknown as Array<
|
|
470
|
+
Array<{ text: string; callback_data?: string }>
|
|
471
|
+
>;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function flatTexts(state: DashboardState): string[] {
|
|
475
|
+
return rows(state).flat().map((b) => b.text);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
it("renders nothing when accounts is undefined (degraded fallback)", () => {
|
|
479
|
+
const state = mkState({ accounts: undefined });
|
|
480
|
+
const texts = flatTexts(state);
|
|
481
|
+
expect(texts.find((t) => t.startsWith("✓") || t.startsWith("○"))).toBeUndefined();
|
|
482
|
+
expect(texts).not.toContain("🌐 Share to fleet");
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("renders the bootstrap button when accounts is empty AND canBootstrapShare is true", () => {
|
|
486
|
+
const state = mkState({ accounts: [], canBootstrapShare: true });
|
|
487
|
+
const texts = flatTexts(state);
|
|
488
|
+
expect(texts).toContain("🌐 Share to fleet");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("hides the bootstrap button when canBootstrapShare is false", () => {
|
|
492
|
+
const state = mkState({ accounts: [], canBootstrapShare: false });
|
|
493
|
+
const texts = flatTexts(state);
|
|
494
|
+
expect(texts).not.toContain("🌐 Share to fleet");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// v3c: per-account drilldown buttons removed from the main board.
|
|
498
|
+
// The text already names every account; the picker (`🔀 Switch
|
|
499
|
+
// primary`) replaces per-account button rows. Tests below cover what
|
|
500
|
+
// remains visible on the main board.
|
|
501
|
+
|
|
502
|
+
it("does NOT render per-account drilldown buttons on the main board (v3c)", () => {
|
|
503
|
+
const state = mkState({
|
|
504
|
+
accounts: [
|
|
505
|
+
mkAccount({ label: "work", enabledHere: true, activeForThisAgent: true }),
|
|
506
|
+
mkAccount({ label: "fallback", enabledHere: true }),
|
|
507
|
+
],
|
|
508
|
+
});
|
|
509
|
+
const allButtons = rows(state).flat();
|
|
510
|
+
const drilldowns = allButtons.filter((b) => b.callback_data?.startsWith("auth:av:"));
|
|
511
|
+
expect(drilldowns.length).toBe(0);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("renders the Switch primary picker entry when fallbacks exist", () => {
|
|
515
|
+
const state = mkState({
|
|
516
|
+
accounts: [
|
|
517
|
+
mkAccount({ label: "work", activeForThisAgent: true }),
|
|
518
|
+
mkAccount({ label: "fallback" }),
|
|
519
|
+
],
|
|
520
|
+
});
|
|
521
|
+
const allButtons = rows(state).flat();
|
|
522
|
+
const picker = allButtons.find((b) => b.text.includes("Switch primary"));
|
|
523
|
+
expect(picker?.callback_data).toBe("auth:spv:clerk");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("hides the Switch primary picker when accounts exceed display cap (truncated noop still shown)", () => {
|
|
527
|
+
const tooMany: AccountSummary[] = [];
|
|
528
|
+
for (let i = 0; i < ACCOUNTS_DISPLAY_CAP + 2; i++) {
|
|
529
|
+
tooMany.push(mkAccount({ label: `acct-${i}` }));
|
|
530
|
+
}
|
|
531
|
+
// Mark the first one active so the picker WOULD appear if visible
|
|
532
|
+
// contained both active and fallbacks. ACCOUNTS_DISPLAY_CAP slice
|
|
533
|
+
// means visible may still include both, so this test verifies the
|
|
534
|
+
// truncated row appears regardless.
|
|
535
|
+
tooMany[0] = mkAccount({ label: tooMany[0].label, activeForThisAgent: true });
|
|
536
|
+
const state = mkState({ accounts: tooMany, accountsTruncated: true });
|
|
537
|
+
const allButtons = rows(state).flat();
|
|
538
|
+
const truncated = allButtons.find((b) => b.text.startsWith("…"));
|
|
539
|
+
expect(truncated?.callback_data).toBe("auth:noop");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("hides the bootstrap button once accounts exist (Switch primary takes over)", () => {
|
|
543
|
+
const state = mkState({
|
|
544
|
+
accounts: [mkAccount({ label: "work" })],
|
|
545
|
+
canBootstrapShare: true,
|
|
546
|
+
});
|
|
547
|
+
const texts = flatTexts(state);
|
|
548
|
+
expect(texts).not.toContain("🌐 Share to fleet");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("Switch primary callback encodes well under the 64-byte budget", () => {
|
|
552
|
+
expect(
|
|
553
|
+
Buffer.byteLength(
|
|
554
|
+
encodeCallbackData({ kind: "switch-primary-view", agent: "clerk" }),
|
|
555
|
+
"utf8",
|
|
556
|
+
),
|
|
557
|
+
).toBeLessThanOrEqual(CALLBACK_BUDGET_BYTES);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe("buildAccountConfirmKeyboard", () => {
|
|
562
|
+
it("emits an enable confirm + cancel row", () => {
|
|
563
|
+
const kb = buildAccountConfirmKeyboard("clerk", "work", "enable");
|
|
564
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
565
|
+
expect(buttons).toHaveLength(2);
|
|
566
|
+
expect(buttons[0].text).toBe("⚠️ Confirm enable: work");
|
|
567
|
+
expect(buttons[0].callback_data).toBe("auth:cae:clerk:work");
|
|
568
|
+
expect(buttons[1].text).toBe("↩️ Cancel");
|
|
569
|
+
expect(buttons[1].callback_data).toBe("auth:refresh:clerk");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("emits a disable confirm with the disable callback", () => {
|
|
573
|
+
const kb = buildAccountConfirmKeyboard("clerk", "work", "disable");
|
|
574
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
575
|
+
expect(buttons[0].text).toBe("⚠️ Confirm disable: work");
|
|
576
|
+
expect(buttons[0].callback_data).toBe("auth:cad:clerk:work");
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
describe("buildDashboardText — accounts summary line", () => {
|
|
581
|
+
it("omits the line when accounts is undefined", () => {
|
|
582
|
+
const text = buildDashboardText(mkState({ accounts: undefined }));
|
|
583
|
+
expect(text).not.toMatch(/Accounts:/);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("omits the line when accounts is an empty array (no totals to summarise)", () => {
|
|
587
|
+
const text = buildDashboardText(mkState({ accounts: [] }));
|
|
588
|
+
expect(text).not.toMatch(/Accounts:/);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("renders account list with labels when accounts exist (v3a: accounts-first layout)", () => {
|
|
592
|
+
// v3a: the summary line "Accounts: N/M shared" is replaced by a
|
|
593
|
+
// proper section header + per-account rows. The text now shows each
|
|
594
|
+
// account label. The old "N/M shared" summary is gone — sub-views
|
|
595
|
+
// carry the per-account detail instead.
|
|
596
|
+
const text = buildDashboardText(
|
|
597
|
+
mkState({
|
|
598
|
+
accounts: [
|
|
599
|
+
mkAccount({ label: "work", enabledHere: true }),
|
|
600
|
+
mkAccount({ label: "home", enabledHere: false }),
|
|
601
|
+
mkAccount({ label: "test", enabledHere: false }),
|
|
602
|
+
],
|
|
603
|
+
}),
|
|
604
|
+
);
|
|
605
|
+
expect(text).toMatch(/Anthropic accounts \(3\)/);
|
|
606
|
+
expect(text).toContain("<code>work</code>");
|
|
607
|
+
expect(text).toContain("<code>home</code>");
|
|
608
|
+
expect(text).toContain("<code>test</code>");
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const _AccountHealthCheck: AccountHealth = "healthy"; // type-import smoke
|
|
613
|
+
void _AccountHealthCheck;
|
|
614
|
+
|
|
615
|
+
// ─── v3a: new callback kinds ──────────────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
import {
|
|
618
|
+
buildAccountSubViewText,
|
|
619
|
+
buildAccountSubViewKeyboard,
|
|
620
|
+
buildAccountRemoveConfirmKeyboard,
|
|
621
|
+
} from "../auth-dashboard";
|
|
622
|
+
|
|
623
|
+
describe("encodeCallbackData / parseCallbackData — v3a account sub-view verbs", () => {
|
|
624
|
+
it("account-view round-trips", () => {
|
|
625
|
+
const action = { kind: "account-view" as const, agent: "clerk", label: "work" };
|
|
626
|
+
const encoded = encodeCallbackData(action);
|
|
627
|
+
expect(encoded).toBe("auth:av:clerk:work");
|
|
628
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("account-rm round-trips", () => {
|
|
632
|
+
const action = { kind: "account-rm" as const, agent: "clerk", label: "work" };
|
|
633
|
+
const encoded = encodeCallbackData(action);
|
|
634
|
+
expect(encoded).toBe("auth:arm:clerk:work");
|
|
635
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("account-rm-confirm round-trips", () => {
|
|
639
|
+
const action = { kind: "account-rm-confirm" as const, agent: "clerk", label: "work" };
|
|
640
|
+
const encoded = encodeCallbackData(action);
|
|
641
|
+
expect(encoded).toBe("auth:armc:clerk:work");
|
|
642
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("account-reauth round-trips", () => {
|
|
646
|
+
const action = { kind: "account-reauth" as const, agent: "clerk", label: "work" };
|
|
647
|
+
const encoded = encodeCallbackData(action);
|
|
648
|
+
expect(encoded).toBe("auth:ara:clerk:work");
|
|
649
|
+
expect(parseCallbackData(encoded)).toEqual(action);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("rejects malformed agent in v3a verbs", () => {
|
|
653
|
+
expect(parseCallbackData("auth:av:bad agent:work")).toEqual({ kind: "noop" });
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("rejects malformed label in v3a verbs", () => {
|
|
657
|
+
expect(parseCallbackData("auth:av:clerk:bad label")).toEqual({ kind: "noop" });
|
|
658
|
+
expect(parseCallbackData("auth:arm:clerk:..")).toEqual({ kind: "noop" });
|
|
659
|
+
expect(parseCallbackData("auth:armc:clerk:")).toEqual({ kind: "noop" });
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("v3a verbs fit within 64-byte cap for typical names", () => {
|
|
663
|
+
for (const kind of ["account-view", "account-rm", "account-rm-confirm", "account-reauth"] as const) {
|
|
664
|
+
const encoded = encodeCallbackData({ kind, agent: "clerk", label: "work" });
|
|
665
|
+
expect(Buffer.byteLength(encoded, "utf8")).toBeLessThanOrEqual(CALLBACK_BUDGET_BYTES);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
describe("buildAccountSubViewText", () => {
|
|
671
|
+
it("includes label, agent, and health in the sub-view body", () => {
|
|
672
|
+
const acc: AccountSummary = { label: "work", health: "healthy", enabledHere: true };
|
|
673
|
+
const text = buildAccountSubViewText("clerk", acc);
|
|
674
|
+
expect(text).toContain("work");
|
|
675
|
+
expect(text).toContain("clerk");
|
|
676
|
+
expect(text).toContain("healthy");
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("escapes HTML in label and agent", () => {
|
|
680
|
+
const acc: AccountSummary = { label: "a&b", health: "healthy", enabledHere: false };
|
|
681
|
+
const text = buildAccountSubViewText("<evil>", acc);
|
|
682
|
+
expect(text).toContain("&");
|
|
683
|
+
expect(text).toContain("<evil>");
|
|
684
|
+
expect(text).not.toContain("<evil>");
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("shows subscriptionType when present", () => {
|
|
688
|
+
const acc: AccountSummary = { label: "work", health: "healthy", enabledHere: true, subscriptionType: "max_5x" };
|
|
689
|
+
const text = buildAccountSubViewText("clerk", acc);
|
|
690
|
+
expect(text).toContain("max_5x");
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
describe("buildAccountSubViewKeyboard", () => {
|
|
695
|
+
it("has Reauth, Remove, and back-to-Accounts buttons", () => {
|
|
696
|
+
const kb = buildAccountSubViewKeyboard("clerk", "work");
|
|
697
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
698
|
+
expect(buttons.find((b) => b.text === "🔁 Reauth")).toBeTruthy();
|
|
699
|
+
expect(buttons.find((b) => b.text === "🗑 Remove")).toBeTruthy();
|
|
700
|
+
expect(buttons.find((b) => b.text === "← Accounts")).toBeTruthy();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("Reauth uses account-reauth callback", () => {
|
|
704
|
+
const kb = buildAccountSubViewKeyboard("clerk", "work");
|
|
705
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
706
|
+
const btn = buttons.find((b) => b.text === "🔁 Reauth");
|
|
707
|
+
expect(btn?.callback_data).toBe("auth:ara:clerk:work");
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it("Remove uses account-rm callback", () => {
|
|
711
|
+
const kb = buildAccountSubViewKeyboard("clerk", "work");
|
|
712
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
713
|
+
const btn = buttons.find((b) => b.text === "🗑 Remove");
|
|
714
|
+
expect(btn?.callback_data).toBe("auth:arm:clerk:work");
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("back button returns to main dashboard via refresh", () => {
|
|
718
|
+
const kb = buildAccountSubViewKeyboard("clerk", "work");
|
|
719
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
720
|
+
const btn = buttons.find((b) => b.text === "← Accounts");
|
|
721
|
+
expect(btn?.callback_data).toBe("auth:refresh:clerk");
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
describe("buildAccountRemoveConfirmKeyboard", () => {
|
|
726
|
+
it("has Yes-remove and Cancel buttons", () => {
|
|
727
|
+
const kb = buildAccountRemoveConfirmKeyboard("clerk", "work");
|
|
728
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
729
|
+
expect(buttons.find((b) => b.text === "✓ Yes, remove")).toBeTruthy();
|
|
730
|
+
expect(buttons.find((b) => b.text === "✗ Cancel")).toBeTruthy();
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("Yes button uses account-rm-confirm callback", () => {
|
|
734
|
+
const kb = buildAccountRemoveConfirmKeyboard("clerk", "work");
|
|
735
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
736
|
+
const btn = buttons.find((b) => b.text === "✓ Yes, remove");
|
|
737
|
+
expect(btn?.callback_data).toBe("auth:armc:clerk:work");
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it("Cancel button returns to account sub-view via account-view callback", () => {
|
|
741
|
+
const kb = buildAccountRemoveConfirmKeyboard("clerk", "work");
|
|
742
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
743
|
+
const btn = buttons.find((b) => b.text === "✗ Cancel");
|
|
744
|
+
expect(btn?.callback_data).toBe("auth:av:clerk:work");
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("Cancel button falls back to noop when account-view payload exceeds budget", () => {
|
|
748
|
+
// A very long label pushes the encoded account-view callback over the
|
|
749
|
+
// 64-byte cap — the Cancel button must use the noop fallback so the
|
|
750
|
+
// Telegram Bot API doesn't reject the keyboard.
|
|
751
|
+
const longLabel = "a".repeat(51);
|
|
752
|
+
const kb = buildAccountRemoveConfirmKeyboard("clerk", longLabel);
|
|
753
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
754
|
+
const btn = buttons.find((b) => b.text === "✗ Cancel");
|
|
755
|
+
// Verify the encoded cancel payload would exceed budget
|
|
756
|
+
const cancelEncoded = encodeCallbackData({ kind: "account-view", agent: "clerk", label: longLabel });
|
|
757
|
+
expect(Buffer.byteLength(cancelEncoded, "utf8")).toBeGreaterThan(CALLBACK_BUDGET_BYTES);
|
|
758
|
+
// Cancel must fall back to noop when over budget
|
|
759
|
+
expect(btn?.callback_data).toBe("auth:noop");
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
describe("account-view not-found path — keyboard/text surface", () => {
|
|
764
|
+
// The gateway handler for account-view fires answerCallbackQuery with an
|
|
765
|
+
// error toast when the label is not found in the current dashboard state,
|
|
766
|
+
// then refreshes the main dashboard. This test verifies the Cancel button
|
|
767
|
+
// on the remove-confirm keyboard always produces a valid callback so the
|
|
768
|
+
// user can escape back to a working state even when state is stale.
|
|
769
|
+
it("account-view callback encodes cleanly for a label that has since been removed", () => {
|
|
770
|
+
// Simulate: user opened remove-confirm for "old-account", then the
|
|
771
|
+
// account was removed out-of-band. The Cancel button's encoded payload
|
|
772
|
+
// must still parse to a valid (noop or account-view) action — it should
|
|
773
|
+
// never produce a malformed string that Telegram would reject.
|
|
774
|
+
const kb = buildAccountRemoveConfirmKeyboard("clerk", "old-account");
|
|
775
|
+
const buttons = (kb.inline_keyboard as unknown as Array<Array<{ text: string; callback_data?: string }>>).flat();
|
|
776
|
+
const btn = buttons.find((b) => b.text === "✗ Cancel");
|
|
777
|
+
const action = parseCallbackData(btn?.callback_data ?? "");
|
|
778
|
+
expect(["account-view", "noop"]).toContain(action.kind);
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
describe("account-view not-found path — gateway dispatch contract", () => {
|
|
783
|
+
// When the gateway receives an account-view callback but cannot find the
|
|
784
|
+
// account label in the current dashboard state (e.g. removed out-of-band
|
|
785
|
+
// between the button being rendered and tapped), the handler must:
|
|
786
|
+
// 1. Fire answerCallbackQuery with an error toast (not an empty ACK).
|
|
787
|
+
// 2. Refresh the main dashboard via editMessageText.
|
|
788
|
+
//
|
|
789
|
+
// This describe pins the pure-function contract that makes that path
|
|
790
|
+
// deterministic: parseCallbackData identifies the action correctly, and
|
|
791
|
+
// the sub-view builders are never called on absent accounts — the caller
|
|
792
|
+
// (gateway) is responsible for the early-return / toast path.
|
|
793
|
+
|
|
794
|
+
it("parseCallbackData correctly identifies account-view for a valid encoded label", () => {
|
|
795
|
+
// The gateway uses parseCallbackData to dispatch. An account that has
|
|
796
|
+
// since been removed still decodes to account-view (not noop) as long
|
|
797
|
+
// as the label itself is structurally valid — the gateway then does the
|
|
798
|
+
// state lookup and branches on not-found.
|
|
799
|
+
const encoded = encodeCallbackData({ kind: "account-view", agent: "clerk", label: "old-account" });
|
|
800
|
+
const action = parseCallbackData(encoded);
|
|
801
|
+
expect(action.kind).toBe("account-view");
|
|
802
|
+
if (action.kind === "account-view") {
|
|
803
|
+
expect(action.agent).toBe("clerk");
|
|
804
|
+
expect(action.label).toBe("old-account");
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("account lookup against an empty state returns undefined (triggers not-found toast)", () => {
|
|
809
|
+
// Simulate fetchDashboardState returning a state with no accounts.
|
|
810
|
+
// The gateway does: state?.accounts?.find(a => a.label === action.label)
|
|
811
|
+
// This must return undefined, which gates the error-toast branch.
|
|
812
|
+
const accounts: AccountSummary[] = [];
|
|
813
|
+
const found = accounts.find((a) => a.label === "old-account");
|
|
814
|
+
expect(found).toBeUndefined();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("account lookup against a state that no longer contains the label returns undefined", () => {
|
|
818
|
+
// The account existed when the keyboard was rendered but was removed
|
|
819
|
+
// before the user tapped the button.
|
|
820
|
+
const accounts: AccountSummary[] = [
|
|
821
|
+
{ label: "current", health: "healthy", enabledHere: true },
|
|
822
|
+
{ label: "other", health: "healthy", enabledHere: false },
|
|
823
|
+
];
|
|
824
|
+
const found = accounts.find((a) => a.label === "old-account");
|
|
825
|
+
expect(found).toBeUndefined();
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it("buildAccountSubViewText renders correctly for a present account (success path)", () => {
|
|
829
|
+
// Verifies the happy path that the gateway takes when the account IS found.
|
|
830
|
+
// The not-found branch must NOT call buildAccountSubViewText — this test
|
|
831
|
+
// pins what the success path looks like so any regression in the dispatch
|
|
832
|
+
// logic (e.g. calling sub-view builder before the not-found check) is
|
|
833
|
+
// visible.
|
|
834
|
+
const acc: AccountSummary = { label: "old-account", health: "healthy", enabledHere: true };
|
|
835
|
+
const text = buildAccountSubViewText("clerk", acc);
|
|
836
|
+
expect(text).toContain("old-account");
|
|
837
|
+
expect(text).toContain("clerk");
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it("error toast message format contains the label (matches gateway handler string)", () => {
|
|
841
|
+
// The gateway sends: `Account "${action.label}" not found.`
|
|
842
|
+
// Pin the label interpolation so a refactor of the toast string
|
|
843
|
+
// doesn't silently drop the label.
|
|
844
|
+
const label = "old-account";
|
|
845
|
+
const toastText = `Account "${label}" not found.`;
|
|
846
|
+
expect(toastText).toContain(label);
|
|
847
|
+
expect(toastText).toMatch(/not found/i);
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// ─── Per-account quota render ─────────────────────────────────────────
|
|
852
|
+
|
|
853
|
+
import {
|
|
854
|
+
formatAccountQuotaLine,
|
|
855
|
+
isAccountQuotaHot,
|
|
856
|
+
} from "../auth-dashboard";
|
|
857
|
+
|
|
858
|
+
describe("formatAccountQuotaLine", () => {
|
|
859
|
+
function acc(extra: Partial<AccountSummary> = {}): AccountSummary {
|
|
860
|
+
return {
|
|
861
|
+
label: "x@example.com",
|
|
862
|
+
health: "healthy",
|
|
863
|
+
enabledHere: true,
|
|
864
|
+
...extra,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
it("returns null when neither percentage is present", () => {
|
|
869
|
+
expect(formatAccountQuotaLine(acc())).toBeNull();
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it("renders both percentages joined with ' · '", () => {
|
|
873
|
+
const line = formatAccountQuotaLine(
|
|
874
|
+
acc({ fiveHourPct: 47, sevenDayPct: 12 }),
|
|
875
|
+
);
|
|
876
|
+
expect(line).toContain("5h:");
|
|
877
|
+
expect(line).toContain("47%");
|
|
878
|
+
expect(line).toContain("7d:");
|
|
879
|
+
expect(line).toContain("12%");
|
|
880
|
+
expect(line).toMatch(/·/);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it("rounds percentages but reserves '0%' for genuine idle (positives < 0.5% render as <1%)", () => {
|
|
884
|
+
const line = formatAccountQuotaLine(
|
|
885
|
+
acc({ fiveHourPct: 0.3, sevenDayPct: 0 }),
|
|
886
|
+
);
|
|
887
|
+
expect(line).toContain("<1%");
|
|
888
|
+
expect(line).toContain("0%");
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("renders only the known percentage when the other is missing", () => {
|
|
892
|
+
const line5 = formatAccountQuotaLine(acc({ fiveHourPct: 20 }));
|
|
893
|
+
expect(line5).toContain("5h:");
|
|
894
|
+
expect(line5).not.toContain("7d:");
|
|
895
|
+
const line7 = formatAccountQuotaLine(acc({ sevenDayPct: 8 }));
|
|
896
|
+
expect(line7).toContain("7d:");
|
|
897
|
+
expect(line7).not.toContain("5h:");
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it("shows the exhausted-state line with reset time when quotaExhaustedUntil is in the future", () => {
|
|
901
|
+
const now = 1_000_000;
|
|
902
|
+
const line = formatAccountQuotaLine(
|
|
903
|
+
acc({
|
|
904
|
+
quotaExhaustedUntil: now + 90 * 60_000,
|
|
905
|
+
fiveHourPct: 100,
|
|
906
|
+
sevenDayPct: 30,
|
|
907
|
+
}),
|
|
908
|
+
now,
|
|
909
|
+
);
|
|
910
|
+
expect(line).toContain("exhausted");
|
|
911
|
+
expect(line).toContain("1h 30m");
|
|
912
|
+
// Exhausted line takes priority — no percentage row.
|
|
913
|
+
expect(line).not.toContain("5h:");
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it("falls through to percentages when quotaExhaustedUntil is in the past", () => {
|
|
917
|
+
const now = 1_000_000;
|
|
918
|
+
const line = formatAccountQuotaLine(
|
|
919
|
+
acc({
|
|
920
|
+
quotaExhaustedUntil: now - 60_000,
|
|
921
|
+
fiveHourPct: 25,
|
|
922
|
+
sevenDayPct: 8,
|
|
923
|
+
}),
|
|
924
|
+
now,
|
|
925
|
+
);
|
|
926
|
+
expect(line).toContain("5h:");
|
|
927
|
+
expect(line).not.toContain("exhausted");
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
describe("isAccountQuotaHot", () => {
|
|
932
|
+
function acc(extra: Partial<AccountSummary> = {}): AccountSummary {
|
|
933
|
+
return {
|
|
934
|
+
label: "x@example.com",
|
|
935
|
+
health: "healthy",
|
|
936
|
+
enabledHere: true,
|
|
937
|
+
...extra,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
it("returns false on undefined / empty accounts", () => {
|
|
942
|
+
expect(isAccountQuotaHot(undefined)).toBe(false);
|
|
943
|
+
expect(isAccountQuotaHot([])).toBe(false);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it("returns false when all accounts are below threshold", () => {
|
|
947
|
+
expect(
|
|
948
|
+
isAccountQuotaHot([
|
|
949
|
+
acc({ fiveHourPct: 50, sevenDayPct: 20 }),
|
|
950
|
+
acc({ label: "y", fiveHourPct: 80, sevenDayPct: 30 }),
|
|
951
|
+
]),
|
|
952
|
+
).toBe(false);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it("returns true when any account crosses the 5h threshold", () => {
|
|
956
|
+
expect(
|
|
957
|
+
isAccountQuotaHot([
|
|
958
|
+
acc({ fiveHourPct: 50, sevenDayPct: 20 }),
|
|
959
|
+
acc({ label: "y", fiveHourPct: 95, sevenDayPct: 30 }),
|
|
960
|
+
]),
|
|
961
|
+
).toBe(true);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it("returns true when any account crosses the 7d threshold", () => {
|
|
965
|
+
expect(
|
|
966
|
+
isAccountQuotaHot([acc({ fiveHourPct: 30, sevenDayPct: 91 })]),
|
|
967
|
+
).toBe(true);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it("returns true when any account is server-side quota-exhausted", () => {
|
|
971
|
+
expect(isAccountQuotaHot([acc({ health: "quota-exhausted" })])).toBe(true);
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
describe("buildDashboardText — per-account quota line", () => {
|
|
976
|
+
function mkAcctState(accounts: AccountSummary[]): DashboardState {
|
|
977
|
+
return {
|
|
978
|
+
agent: "clerk",
|
|
979
|
+
bankId: "clerk",
|
|
980
|
+
plan: "max",
|
|
981
|
+
rateLimitTier: null,
|
|
982
|
+
slots: [mkSlot({ active: true, health: "active" })],
|
|
983
|
+
quotaHot: false,
|
|
984
|
+
generatedAt: "2026-05-05T12:00:00Z",
|
|
985
|
+
pendingSessionSlot: null,
|
|
986
|
+
accounts,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
it("hides the quota row for accounts with no quota data", () => {
|
|
991
|
+
const text = buildDashboardText(
|
|
992
|
+
mkAcctState([
|
|
993
|
+
{ label: "fresh@example.com", health: "healthy", enabledHere: true },
|
|
994
|
+
]),
|
|
995
|
+
);
|
|
996
|
+
expect(text).toContain("fresh@example.com");
|
|
997
|
+
expect(text).not.toMatch(/5h:|7d:/);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it("renders the quota row under each account that has data", () => {
|
|
1001
|
+
const text = buildDashboardText(
|
|
1002
|
+
mkAcctState([
|
|
1003
|
+
{
|
|
1004
|
+
label: "warm@example.com",
|
|
1005
|
+
health: "healthy",
|
|
1006
|
+
enabledHere: true,
|
|
1007
|
+
fiveHourPct: 47,
|
|
1008
|
+
sevenDayPct: 12,
|
|
1009
|
+
},
|
|
1010
|
+
]),
|
|
1011
|
+
);
|
|
1012
|
+
expect(text).toContain("warm@example.com");
|
|
1013
|
+
expect(text).toContain("47%");
|
|
1014
|
+
expect(text).toContain("12%");
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it("renders mixed cold + warm accounts: only the warm one gets a quota row", () => {
|
|
1018
|
+
const text = buildDashboardText(
|
|
1019
|
+
mkAcctState([
|
|
1020
|
+
{
|
|
1021
|
+
label: "warm@example.com",
|
|
1022
|
+
health: "healthy",
|
|
1023
|
+
enabledHere: true,
|
|
1024
|
+
fiveHourPct: 12,
|
|
1025
|
+
sevenDayPct: 3,
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
label: "cold@example.com",
|
|
1029
|
+
health: "healthy",
|
|
1030
|
+
enabledHere: false,
|
|
1031
|
+
},
|
|
1032
|
+
]),
|
|
1033
|
+
);
|
|
1034
|
+
// Two account labels.
|
|
1035
|
+
expect(text).toContain("warm@example.com");
|
|
1036
|
+
expect(text).toContain("cold@example.com");
|
|
1037
|
+
// One quota row (warm@); cold@ has no 5h/7d/percent string after it.
|
|
1038
|
+
const warmIdx = text.indexOf("warm@example.com");
|
|
1039
|
+
const coldIdx = text.indexOf("cold@example.com");
|
|
1040
|
+
const between = text.slice(warmIdx, coldIdx);
|
|
1041
|
+
expect(between).toMatch(/5h:/);
|
|
1042
|
+
const after = text.slice(coldIdx);
|
|
1043
|
+
expect(after).not.toMatch(/5h:/);
|
|
1044
|
+
});
|
|
1045
|
+
});
|