serverless-ircd 0.5.0 → 0.7.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/.github/workflows/ci.yml +40 -3
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +474 -0
- package/README.md +219 -22
- package/apps/aws-stack/README.md +33 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +74 -9
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +2 -2
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/src/origin-allowlist.ts +99 -0
- package/apps/cf-worker/src/worker.ts +89 -9
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
- package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
- package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
- package/apps/cf-worker/tests/smoke.test.ts +28 -2
- package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.test.toml +23 -0
- package/apps/cf-worker/wrangler.toml +59 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +9 -7
- package/apps/local-cli/tests/e2e.test.ts +112 -28
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
- package/apps/web/landing/index.html +217 -0
- package/apps/web/package.json +28 -0
- package/apps/web/scripts/build.mjs +125 -0
- package/apps/web/src/build-env.ts +43 -0
- package/apps/web/src/config-schema.ts +138 -0
- package/apps/web/static/config.json +28 -0
- package/apps/web/static/config.prod.json +28 -0
- package/apps/web/static/config.staging.json +28 -0
- package/apps/web/tests/build-env.test.ts +63 -0
- package/apps/web/tests/build-smoke.test.ts +103 -0
- package/apps/web/tests/config-schema.test.ts +432 -0
- package/apps/web/tests/workspace.test.ts +12 -0
- package/apps/web/tsconfig.json +10 -0
- package/apps/web/upstream/.browserslistrc +3 -0
- package/apps/web/upstream/.editorconfig +9 -0
- package/apps/web/upstream/.eslintignore +3 -0
- package/apps/web/upstream/.eslintrc.js +90 -0
- package/apps/web/upstream/.github/FUNDING.yml +12 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/apps/web/upstream/.github/workflows/push.yml +26 -0
- package/apps/web/upstream/.github/workflows/release.yml +33 -0
- package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
- package/apps/web/upstream/.prettierrc.js +9 -0
- package/apps/web/upstream/.stylelintignore +5 -0
- package/apps/web/upstream/.stylelintrc.js +21 -0
- package/apps/web/upstream/LICENSE +201 -0
- package/apps/web/upstream/README.md +76 -0
- package/apps/web/upstream/babel.config.js +25 -0
- package/apps/web/upstream/crowdin.yml +3 -0
- package/apps/web/upstream/index.html +35 -0
- package/apps/web/upstream/jest.config.js +7 -0
- package/apps/web/upstream/jsdoc.json +22 -0
- package/apps/web/upstream/package.json +80 -0
- package/apps/web/upstream/scripts/pre-commit +43 -0
- package/apps/web/upstream/src/components/App.vue +516 -0
- package/apps/web/upstream/src/components/AppSettings.vue +587 -0
- package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
- package/apps/web/upstream/src/components/Avatar.vue +105 -0
- package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
- package/apps/web/upstream/src/components/BufferKey.vue +121 -0
- package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
- package/apps/web/upstream/src/components/Captcha.vue +85 -0
- package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
- package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
- package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
- package/apps/web/upstream/src/components/ChannelList.vue +345 -0
- package/apps/web/upstream/src/components/Container.vue +401 -0
- package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
- package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
- package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
- package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
- package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
- package/apps/web/upstream/src/components/MessageList.vue +1028 -0
- package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
- package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
- package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
- package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
- package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
- package/apps/web/upstream/src/components/Nicklist.vue +309 -0
- package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
- package/apps/web/upstream/src/components/NotConnected.vue +271 -0
- package/apps/web/upstream/src/components/SelfUser.vue +283 -0
- package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
- package/apps/web/upstream/src/components/ServerView.vue +126 -0
- package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
- package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
- package/apps/web/upstream/src/components/Sidebar.vue +429 -0
- package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
- package/apps/web/upstream/src/components/SidebarState.vue +145 -0
- package/apps/web/upstream/src/components/StartupError.vue +35 -0
- package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
- package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
- package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
- package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
- package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
- package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
- package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
- package/apps/web/upstream/src/components/UserBox.vue +728 -0
- package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
- package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
- package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
- package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
- package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
- package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
- package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
- package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
- package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
- package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
- package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
- package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
- package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
- package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
- package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
- package/apps/web/upstream/src/helpers/Colours.js +128 -0
- package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
- package/apps/web/upstream/src/helpers/Md5.js +193 -0
- package/apps/web/upstream/src/helpers/Misc.js +401 -0
- package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
- package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
- package/apps/web/upstream/src/libs/AudioManager.js +60 -0
- package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
- package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
- package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
- package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
- package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
- package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
- package/apps/web/upstream/src/libs/IPC.js +51 -0
- package/apps/web/upstream/src/libs/InputHandler.js +921 -0
- package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
- package/apps/web/upstream/src/libs/Logger.js +71 -0
- package/apps/web/upstream/src/libs/Message.js +169 -0
- package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
- package/apps/web/upstream/src/libs/MessageParser.js +249 -0
- package/apps/web/upstream/src/libs/Notifications.js +78 -0
- package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
- package/apps/web/upstream/src/libs/ServerSession.js +102 -0
- package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
- package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
- package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
- package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
- package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
- package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
- package/apps/web/upstream/src/libs/bufferTools.js +177 -0
- package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
- package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
- package/apps/web/upstream/src/libs/settingTools.js +31 -0
- package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
- package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
- package/apps/web/upstream/src/libs/state/UserState.js +89 -0
- package/apps/web/upstream/src/libs/state/common.js +26 -0
- package/apps/web/upstream/src/libs/state.js +961 -0
- package/apps/web/upstream/src/libs/storage/Local.js +51 -0
- package/apps/web/upstream/src/main.js +566 -0
- package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
- package/apps/web/upstream/src/res/configTemplates.js +373 -0
- package/apps/web/upstream/src/res/globalStyle.css +277 -0
- package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
- package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
- package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
- package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
- package/apps/web/upstream/src/res/localesList.json +27 -0
- package/apps/web/upstream/src/res/logo.png +0 -0
- package/apps/web/upstream/src/thirdparty/about.html +43 -0
- package/apps/web/upstream/src/thirdparty/index.js +5 -0
- package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
- package/apps/web/upstream/static/config.json +28 -0
- package/apps/web/upstream/static/emoticons/smile.png +0 -0
- package/apps/web/upstream/static/favicon.png +0 -0
- package/apps/web/upstream/static/highlight.mp3 +0 -0
- package/apps/web/upstream/static/highlight.ogg +0 -0
- package/apps/web/upstream/static/locales/.gitignore +2 -0
- package/apps/web/upstream/static/plugins/customise.html.example +10 -0
- package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
- package/apps/web/upstream/static/themes/common/base.css +1064 -0
- package/apps/web/upstream/static/themes/dark/theme.css +196 -0
- package/apps/web/upstream/static/themes/default/theme.css +92 -0
- package/apps/web/upstream/static/themes/elite/theme.css +248 -0
- package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
- package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
- package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
- package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
- package/apps/web/upstream/static/themes/sky/theme.css +107 -0
- package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
- package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
- package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
- package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
- package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
- package/apps/web/upstream/vue.config.js +150 -0
- package/apps/web/upstream/yarn.lock +9767 -0
- package/apps/web/vitest.config.ts +13 -0
- package/biome.json +2 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +16 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/docs/WebClientGuide.md +535 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/handlers/default.ts +3 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
- package/packages/aws-adapter/src/serialize.ts +29 -1
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +9 -9
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +90 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/connection-do.ts +3 -0
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/reports/mutation/mutation.html +342 -0
- package/packages/irc-core/src/caps/capabilities.ts +37 -3
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +34 -0
- package/packages/irc-core/src/commands/isupport.ts +53 -0
- package/packages/irc-core/src/commands/mode.ts +8 -1
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- package/packages/irc-core/src/commands/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +65 -10
- package/packages/irc-core/src/effects.ts +14 -0
- package/packages/irc-core/src/ports.ts +389 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +15 -0
- package/packages/irc-core/src/protocol/outbound.ts +16 -1
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +112 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
- package/packages/irc-core/tests/commands/mode.test.ts +72 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +13 -0
- package/packages/irc-core/tests/parser.test.ts +109 -0
- package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +450 -3
- package/packages/irc-server/src/dispatch.ts +108 -16
- package/packages/irc-server/tests/actor.test.ts +1432 -39
- package/packages/irc-server/tests/dispatch.test.ts +263 -2
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Effect } from '../src/effects';
|
|
3
|
+
import type { Effect as EffectType, RawLine } from '../src/effects';
|
|
4
|
+
import {
|
|
5
|
+
LABELED_RESPONSE_CAP_NAME,
|
|
6
|
+
LABEL_TAG,
|
|
7
|
+
applyLabelToEffects,
|
|
8
|
+
extractLabel,
|
|
9
|
+
wrapLabeledResponse,
|
|
10
|
+
} from '../src/protocol/labeled-response';
|
|
11
|
+
|
|
12
|
+
const L = (text: string): RawLine => ({ text });
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// extractLabel — IRCv3 labeled-response client-tag reader
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
describe('extractLabel — +label client tag', () => {
|
|
19
|
+
it('returns the value when the +label tag is present', () => {
|
|
20
|
+
expect(extractLabel({ '+label': 'foo' })).toBe('foo');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns undefined when the +label tag is absent', () => {
|
|
24
|
+
expect(extractLabel({})).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns undefined when the +label tag is present but empty', () => {
|
|
28
|
+
expect(extractLabel({ '+label': '' })).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('ignores unrelated tags', () => {
|
|
32
|
+
expect(extractLabel({ '+typing': 'active', server_time: 'x' })).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('exposes the wire tag name as the +label constant', () => {
|
|
36
|
+
expect(LABEL_TAG).toBe('+label');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// wrapLabeledResponse — the always-emit BATCH framing helper
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
describe('wrapLabeledResponse — batch framing', () => {
|
|
45
|
+
it('wraps a non-empty line list with BATCH +label labeled-response / -label', () => {
|
|
46
|
+
const out = wrapLabeledResponse('foo', [L(':srv PONG :tok'), L(':srv 001 alice :Welcome')]);
|
|
47
|
+
expect(out.map((l) => l.text)).toEqual([
|
|
48
|
+
'BATCH +foo labeled-response',
|
|
49
|
+
':srv PONG :tok',
|
|
50
|
+
':srv 001 alice :Welcome',
|
|
51
|
+
'BATCH -foo',
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('emits an empty batch (start + end only) when there are no inner lines', () => {
|
|
56
|
+
const out = wrapLabeledResponse('foo', []);
|
|
57
|
+
expect(out.map((l) => l.text)).toEqual(['BATCH +foo labeled-response', 'BATCH -foo']);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('uses the labeled-response batch type token', () => {
|
|
61
|
+
const out = wrapLabeledResponse('x', [L('inner')]);
|
|
62
|
+
expect(out[0]?.text).toBe('BATCH +x labeled-response');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('uses the supplied label verbatim in both markers', () => {
|
|
66
|
+
const out = wrapLabeledResponse('my-label-7', [L('inner')]);
|
|
67
|
+
expect(out[0]?.text).toBe('BATCH +my-label-7 labeled-response');
|
|
68
|
+
expect(out[out.length - 1]?.text).toBe('BATCH -my-label-7');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('preserves the order of the inner lines', () => {
|
|
72
|
+
const out = wrapLabeledResponse('x', [L('one'), L('two'), L('three')]);
|
|
73
|
+
expect(out.map((l) => l.text)).toEqual([
|
|
74
|
+
'BATCH +x labeled-response',
|
|
75
|
+
'one',
|
|
76
|
+
'two',
|
|
77
|
+
'three',
|
|
78
|
+
'BATCH -x',
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('exposes the wire cap name as the labeled-response constant', () => {
|
|
83
|
+
expect(LABELED_RESPONSE_CAP_NAME).toBe('labeled-response');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// applyLabelToEffects — pure Effect[] transform
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
describe('applyLabelToEffects — effect transform', () => {
|
|
92
|
+
it('returns the effects unchanged when no label is set', () => {
|
|
93
|
+
const effects: EffectType[] = [Effect.send('c1', [L('PONG :tok')])];
|
|
94
|
+
expect(applyLabelToEffects(effects, { label: undefined, connId: 'c1' })).toEqual(effects);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('wraps a single Send-to-self effect in a labeled batch', () => {
|
|
98
|
+
const effects: EffectType[] = [Effect.send('c1', [L(':srv PONG :tok')])];
|
|
99
|
+
|
|
100
|
+
const out = applyLabelToEffects(effects, { label: 'foo', connId: 'c1' });
|
|
101
|
+
|
|
102
|
+
expect(out).toEqual<EffectType[]>([
|
|
103
|
+
Effect.send('c1', [L('BATCH +foo labeled-response'), L(':srv PONG :tok'), L('BATCH -foo')]),
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('accumulates multiple Send-to-self effects into a single labeled batch in order', () => {
|
|
108
|
+
const effects: EffectType[] = [
|
|
109
|
+
Effect.send('c1', [L('echo-a')]),
|
|
110
|
+
Effect.send('c1', [L('echo-b'), L('echo-c')]),
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const out = applyLabelToEffects(effects, { label: 'm', connId: 'c1' });
|
|
114
|
+
|
|
115
|
+
expect(out).toHaveLength(1);
|
|
116
|
+
expect(out[0]?.tag).toBe('Send');
|
|
117
|
+
const lines = (out[0] as Extract<EffectType, { tag: 'Send' }>).lines;
|
|
118
|
+
expect(lines.map((l) => l.text)).toEqual([
|
|
119
|
+
'BATCH +m labeled-response',
|
|
120
|
+
'echo-a',
|
|
121
|
+
'echo-b',
|
|
122
|
+
'echo-c',
|
|
123
|
+
'BATCH -m',
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('leaves a Send to a different connection untouched', () => {
|
|
128
|
+
const effects: EffectType[] = [Effect.send('c2', [L('to-other')])];
|
|
129
|
+
|
|
130
|
+
const out = applyLabelToEffects(effects, { label: 'foo', connId: 'c1' });
|
|
131
|
+
|
|
132
|
+
// No Send-to-self → an empty labeled batch is appended at the end, and the
|
|
133
|
+
// Send to c2 stays in place.
|
|
134
|
+
expect(out).toEqual<EffectType[]>([
|
|
135
|
+
Effect.send('c2', [L('to-other')]),
|
|
136
|
+
Effect.send('c1', [L('BATCH +foo labeled-response'), L('BATCH -foo')]),
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('emits an empty labeled batch when no Send-to-self effect exists', () => {
|
|
141
|
+
const effects: EffectType[] = [Effect.broadcast('#foo', [L(':a PRIVMSG #foo :hi')], 'c1')];
|
|
142
|
+
|
|
143
|
+
const out = applyLabelToEffects(effects, { label: 'foo', connId: 'c1' });
|
|
144
|
+
|
|
145
|
+
expect(out).toEqual<EffectType[]>([
|
|
146
|
+
Effect.broadcast('#foo', [L(':a PRIVMSG #foo :hi')], 'c1'),
|
|
147
|
+
Effect.send('c1', [L('BATCH +foo labeled-response'), L('BATCH -foo')]),
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('places the batch at the position of the first Send-to-self, preserving surrounding effects', () => {
|
|
152
|
+
const effects: EffectType[] = [
|
|
153
|
+
Effect.broadcast('#a', [L('to-a-peers')], 'c1'),
|
|
154
|
+
Effect.send('c1', [L('echo-a')]),
|
|
155
|
+
Effect.broadcast('#b', [L('to-b-peers')], 'c1'),
|
|
156
|
+
Effect.send('c1', [L('echo-b')]),
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const out = applyLabelToEffects(effects, { label: 'm', connId: 'c1' });
|
|
160
|
+
|
|
161
|
+
expect(out).toEqual<EffectType[]>([
|
|
162
|
+
Effect.broadcast('#a', [L('to-a-peers')], 'c1'),
|
|
163
|
+
Effect.send('c1', [L('BATCH +m labeled-response'), L('echo-a'), L('echo-b'), L('BATCH -m')]),
|
|
164
|
+
Effect.broadcast('#b', [L('to-b-peers')], 'c1'),
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('does not wrap effects addressed to self via non-Send tags (e.g. SendToNick from self)', () => {
|
|
169
|
+
const sendToNick = Effect.sendToNick('bob', 'c1', [L(':a PRIVMSG bob :hi')]);
|
|
170
|
+
const effects: EffectType[] = [sendToNick];
|
|
171
|
+
|
|
172
|
+
const out = applyLabelToEffects(effects, { label: 'foo', connId: 'c1' });
|
|
173
|
+
|
|
174
|
+
// SendToNick is not a Send-to-self; the requester sees no reply, so an
|
|
175
|
+
// empty labeled batch is appended and the SendToNick stays unchanged.
|
|
176
|
+
expect(out).toEqual<EffectType[]>([
|
|
177
|
+
sendToNick,
|
|
178
|
+
Effect.send('c1', [L('BATCH +foo labeled-response'), L('BATCH -foo')]),
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -78,6 +78,120 @@ describe('filterClientTags — cap gating', () => {
|
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// filterClientTags — draft/typing client-tag whitelist
|
|
83
|
+
//
|
|
84
|
+
// IRCv3 `draft/typing` carves out an exception to the message-tags cap
|
|
85
|
+
// requirement: a peer that announced `draft/typing` (but NOT message-tags)
|
|
86
|
+
// still receives the `+draft/typing` client tag on TAGMSG/PRIVMSG lines.
|
|
87
|
+
// Other client tags (`+foo`, `+typing` without the `draft/` prefix, …)
|
|
88
|
+
// remain stripped for non-message-tags recipients.
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
describe('filterClientTags — draft/typing whitelist', () => {
|
|
92
|
+
it('preserves +draft/typing for a draft/typing-only recipient', () => {
|
|
93
|
+
const line = L('@+draft/typing=active :alice TAGMSG #foo');
|
|
94
|
+
const out = filterClientTags(line, new Set<string>(['draft/typing']));
|
|
95
|
+
expect(out.text).toBe('@+draft/typing=active :alice TAGMSG #foo');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('preserves a valueless +draft/typing tag for a draft/typing-only recipient', () => {
|
|
99
|
+
const line = L('@+draft/typing :alice TAGMSG #foo');
|
|
100
|
+
const out = filterClientTags(line, new Set<string>(['draft/typing']));
|
|
101
|
+
expect(out.text).toBe('@+draft/typing :alice TAGMSG #foo');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('strips a non-typing client tag even when draft/typing is negotiated', () => {
|
|
105
|
+
const line = L('@+typing=1;+draft/typing=active :alice TAGMSG #foo');
|
|
106
|
+
const out = filterClientTags(line, new Set<string>(['draft/typing']));
|
|
107
|
+
expect(out.text).toBe('@+draft/typing=active :alice TAGMSG #foo');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('keeps +draft/typing alongside a server tag for a draft/typing-only recipient', () => {
|
|
111
|
+
const line = L('@msgid=abc;+draft/typing=active :alice TAGMSG #foo');
|
|
112
|
+
const out = filterClientTags(line, new Set<string>(['draft/typing']));
|
|
113
|
+
expect(out.text).toBe('@msgid=abc;+draft/typing=active :alice TAGMSG #foo');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('drops every client tag (including +draft/typing) for a cap-less recipient', () => {
|
|
117
|
+
const line = L('@+draft/typing=active;+foo=bar :alice TAGMSG #foo');
|
|
118
|
+
const out = filterClientTags(line, new Set<string>());
|
|
119
|
+
expect(out.text).toBe(':alice TAGMSG #foo');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('preserves all client tags for a recipient with both message-tags and draft/typing', () => {
|
|
123
|
+
const line = L('@+draft/typing=active;+foo=bar :alice TAGMSG #foo');
|
|
124
|
+
const out = filterClientTags(line, new Set<string>(['message-tags', 'draft/typing']));
|
|
125
|
+
expect(out.text).toBe('@+draft/typing=active;+foo=bar :alice TAGMSG #foo');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('drops the leading @ when only a non-typing client tag was present', () => {
|
|
129
|
+
const line = L('@+foo=bar;+draft/typing=active :alice TAGMSG #foo');
|
|
130
|
+
const out = filterClientTags(line, new Set<string>(['draft/typing']));
|
|
131
|
+
expect(out.text).toBe('@+draft/typing=active :alice TAGMSG #foo');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// filterClientTags — draft/read-marker client-tag whitelist
|
|
137
|
+
//
|
|
138
|
+
// IRCv3 `draft/read-marker` carves out the same exception `draft/typing`
|
|
139
|
+
// does: a peer that announced `draft/read-marker` (but NOT message-tags)
|
|
140
|
+
// still receives the `+draft/read-marker` client tag on TAGMSG lines so the
|
|
141
|
+
// read-marker update reaches the user's other connections. Other client
|
|
142
|
+
// tags remain stripped for non-message-tags recipients.
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
describe('filterClientTags — draft/read-marker whitelist', () => {
|
|
146
|
+
it('preserves +draft/read-marker for a draft/read-marker-only recipient', () => {
|
|
147
|
+
const line = L('@+draft/read-marker=m42 :alice TAGMSG #foo');
|
|
148
|
+
const out = filterClientTags(line, new Set<string>(['draft/read-marker']));
|
|
149
|
+
expect(out.text).toBe('@+draft/read-marker=m42 :alice TAGMSG #foo');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('preserves a valueless +draft/read-marker tag for a draft/read-marker-only recipient', () => {
|
|
153
|
+
const line = L('@+draft/read-marker :alice TAGMSG #foo');
|
|
154
|
+
const out = filterClientTags(line, new Set<string>(['draft/read-marker']));
|
|
155
|
+
expect(out.text).toBe('@+draft/read-marker :alice TAGMSG #foo');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('strips a non-read-marker client tag even when draft/read-marker is negotiated', () => {
|
|
159
|
+
const line = L('@+foo=bar;+draft/read-marker=m1 :alice TAGMSG #foo');
|
|
160
|
+
const out = filterClientTags(line, new Set<string>(['draft/read-marker']));
|
|
161
|
+
expect(out.text).toBe('@+draft/read-marker=m1 :alice TAGMSG #foo');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('keeps both +draft/typing and +draft/read-marker for a recipient with both draft caps', () => {
|
|
165
|
+
const line = L('@+draft/typing=active;+draft/read-marker=m1 :alice TAGMSG #foo');
|
|
166
|
+
const out = filterClientTags(line, new Set<string>(['draft/typing', 'draft/read-marker']));
|
|
167
|
+
expect(out.text).toBe('@+draft/typing=active;+draft/read-marker=m1 :alice TAGMSG #foo');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('keeps +draft/read-marker alongside a server tag for a draft/read-marker-only recipient', () => {
|
|
171
|
+
const line = L('@msgid=abc;+draft/read-marker=m1 :alice TAGMSG #foo');
|
|
172
|
+
const out = filterClientTags(line, new Set<string>(['draft/read-marker']));
|
|
173
|
+
expect(out.text).toBe('@msgid=abc;+draft/read-marker=m1 :alice TAGMSG #foo');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('drops every client tag (including +draft/read-marker) for a cap-less recipient', () => {
|
|
177
|
+
const line = L('@+draft/read-marker=m1;+foo=bar :alice TAGMSG #foo');
|
|
178
|
+
const out = filterClientTags(line, new Set<string>());
|
|
179
|
+
expect(out.text).toBe(':alice TAGMSG #foo');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('drops +draft/read-marker for a draft/typing-only recipient', () => {
|
|
183
|
+
const line = L('@+draft/typing=active;+draft/read-marker=m1 :alice TAGMSG #foo');
|
|
184
|
+
const out = filterClientTags(line, new Set<string>(['draft/typing']));
|
|
185
|
+
expect(out.text).toBe('@+draft/typing=active :alice TAGMSG #foo');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('preserves all client tags for a recipient with message-tags', () => {
|
|
189
|
+
const line = L('@+draft/read-marker=m1;+foo=bar :alice TAGMSG #foo');
|
|
190
|
+
const out = filterClientTags(line, new Set<string>(['message-tags', 'draft/read-marker']));
|
|
191
|
+
expect(out.text).toBe('@+draft/read-marker=m1;+foo=bar :alice TAGMSG #foo');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
81
195
|
// ============================================================================
|
|
82
196
|
// filterClientTags + applyServerTime composition (dispatch-layer ordering)
|
|
83
197
|
// ============================================================================
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { InMemoryMonitorStore, type MonitorPush } from '../src/ports';
|
|
3
|
+
|
|
4
|
+
describe('InMemoryMonitorStore — sync', () => {
|
|
5
|
+
it('is a no-op when before and after are both empty', () => {
|
|
6
|
+
const store = new InMemoryMonitorStore();
|
|
7
|
+
store.sync('c1', new Set(), new Set());
|
|
8
|
+
// No public surface to assert directly; subsequent drainPushes shows
|
|
9
|
+
// no subscriptions were recorded (an onNickOnline push would land
|
|
10
|
+
// if any were tracked).
|
|
11
|
+
store.onNickOnline('bob');
|
|
12
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('is a no-op when before equals after (no delta)', () => {
|
|
16
|
+
const store = new InMemoryMonitorStore();
|
|
17
|
+
store.sync('c1', new Set(['bob']), new Set(['bob']));
|
|
18
|
+
store.onNickOnline('bob');
|
|
19
|
+
// Already-monitored → no push because sync recorded nothing.
|
|
20
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('records added nicks in the reverse index', () => {
|
|
24
|
+
const store = new InMemoryMonitorStore();
|
|
25
|
+
store.sync('c1', new Set(), new Set(['bob', 'carol']));
|
|
26
|
+
store.onNickOnline('bob');
|
|
27
|
+
store.onNickOnline('carol');
|
|
28
|
+
|
|
29
|
+
const pushes = store.drainPushes();
|
|
30
|
+
expect(pushes).toContainEqual({ connId: 'c1', display: 'bob', status: 'online' });
|
|
31
|
+
expect(pushes).toContainEqual({ connId: 'c1', display: 'carol', status: 'online' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('removes dropped nicks from the reverse index', () => {
|
|
35
|
+
const store = new InMemoryMonitorStore();
|
|
36
|
+
store.sync('c1', new Set(), new Set(['bob', 'carol']));
|
|
37
|
+
store.sync('c1', new Set(['bob', 'carol']), new Set(['bob']));
|
|
38
|
+
store.onNickOnline('carol');
|
|
39
|
+
// carol was dropped → no push.
|
|
40
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('cleans up the watcher entry when its watchlist becomes empty', () => {
|
|
44
|
+
const store = new InMemoryMonitorStore();
|
|
45
|
+
store.sync('c1', new Set(), new Set(['bob']));
|
|
46
|
+
store.sync('c1', new Set(['bob']), new Set());
|
|
47
|
+
store.onNickOnline('bob');
|
|
48
|
+
// c1 is fully unsubscribed → no push.
|
|
49
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('records two watchers of the same nick independently', () => {
|
|
53
|
+
const store = new InMemoryMonitorStore();
|
|
54
|
+
store.sync('c1', new Set(), new Set(['bob']));
|
|
55
|
+
store.sync('c2', new Set(), new Set(['bob']));
|
|
56
|
+
store.onNickOnline('bob');
|
|
57
|
+
|
|
58
|
+
const pushes = store.drainPushes();
|
|
59
|
+
expect(pushes).toContainEqual({ connId: 'c1', display: 'bob', status: 'online' });
|
|
60
|
+
expect(pushes).toContainEqual({ connId: 'c2', display: 'bob', status: 'online' });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('drops a watcher without disturbing the others', () => {
|
|
64
|
+
const store = new InMemoryMonitorStore();
|
|
65
|
+
store.sync('c1', new Set(), new Set(['bob']));
|
|
66
|
+
store.sync('c2', new Set(), new Set(['bob']));
|
|
67
|
+
store.sync('c1', new Set(['bob']), new Set());
|
|
68
|
+
store.onNickOnline('bob');
|
|
69
|
+
|
|
70
|
+
const pushes = store.drainPushes();
|
|
71
|
+
expect(pushes).toEqual<MonitorPush[]>([{ connId: 'c2', display: 'bob', status: 'online' }]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('InMemoryMonitorStore — unsubscribe', () => {
|
|
76
|
+
it('is a no-op when the connection had no subscriptions', () => {
|
|
77
|
+
const store = new InMemoryMonitorStore();
|
|
78
|
+
store.unsubscribe('ghost');
|
|
79
|
+
store.onNickOnline('bob');
|
|
80
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('removes every subscription for a connection', () => {
|
|
84
|
+
const store = new InMemoryMonitorStore();
|
|
85
|
+
store.sync('c1', new Set(), new Set(['bob', 'carol']));
|
|
86
|
+
store.unsubscribe('c1');
|
|
87
|
+
store.onNickOnline('bob');
|
|
88
|
+
store.onNickOnline('carol');
|
|
89
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('leaves the reverse entry intact when another watcher remains', () => {
|
|
93
|
+
const store = new InMemoryMonitorStore();
|
|
94
|
+
store.sync('c1', new Set(), new Set(['bob']));
|
|
95
|
+
store.sync('c2', new Set(), new Set(['bob']));
|
|
96
|
+
store.unsubscribe('c1');
|
|
97
|
+
store.onNickOnline('bob');
|
|
98
|
+
|
|
99
|
+
// c2 still subscribed → push goes to c2 only.
|
|
100
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([
|
|
101
|
+
{ connId: 'c2', display: 'bob', status: 'online' },
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('InMemoryMonitorStore — onNickOnline / onNickOffline', () => {
|
|
107
|
+
it('onNickOnline enqueues a push for every watcher of the nick', () => {
|
|
108
|
+
const store = new InMemoryMonitorStore();
|
|
109
|
+
store.sync('c1', new Set(), new Set(['bob']));
|
|
110
|
+
store.sync('c2', new Set(), new Set(['bob']));
|
|
111
|
+
store.onNickOnline('Bob');
|
|
112
|
+
|
|
113
|
+
const pushes = store.drainPushes();
|
|
114
|
+
expect(pushes).toHaveLength(2);
|
|
115
|
+
expect(pushes.every((p) => p.display === 'Bob' && p.status === 'online')).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('onNickOnline is idempotent (second call enqueues nothing)', () => {
|
|
119
|
+
const store = new InMemoryMonitorStore();
|
|
120
|
+
store.sync('c1', new Set(), new Set(['bob']));
|
|
121
|
+
store.onNickOnline('bob');
|
|
122
|
+
store.onNickOnline('bob');
|
|
123
|
+
|
|
124
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([
|
|
125
|
+
{ connId: 'c1', display: 'bob', status: 'online' },
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('onNickOnline without any watchers records the online state but no push', () => {
|
|
130
|
+
const store = new InMemoryMonitorStore();
|
|
131
|
+
store.onNickOnline('bob');
|
|
132
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
133
|
+
expect(store.onlineNicks()).toEqual(['bob']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('onNickOffline enqueues a push only when the nick was previously online', () => {
|
|
137
|
+
const store = new InMemoryMonitorStore();
|
|
138
|
+
store.sync('c1', new Set(), new Set(['bob']));
|
|
139
|
+
// bob was never announced online → offline push is a no-op.
|
|
140
|
+
store.onNickOffline('bob');
|
|
141
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
142
|
+
|
|
143
|
+
// After online, offline produces a push.
|
|
144
|
+
store.onNickOnline('bob');
|
|
145
|
+
store.drainPushes();
|
|
146
|
+
store.onNickOffline('bob');
|
|
147
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([
|
|
148
|
+
{ connId: 'c1', display: 'bob', status: 'offline' },
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('onNickOffline is idempotent (second call enqueues nothing)', () => {
|
|
153
|
+
const store = new InMemoryMonitorStore();
|
|
154
|
+
store.sync('c1', new Set(), new Set(['bob']));
|
|
155
|
+
store.onNickOnline('bob');
|
|
156
|
+
store.drainPushes();
|
|
157
|
+
store.onNickOffline('bob');
|
|
158
|
+
store.onNickOffline('bob');
|
|
159
|
+
|
|
160
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([
|
|
161
|
+
{ connId: 'c1', display: 'bob', status: 'offline' },
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('onNickOffline without any watchers clears the online state', () => {
|
|
166
|
+
const store = new InMemoryMonitorStore();
|
|
167
|
+
store.onNickOnline('bob');
|
|
168
|
+
store.onNickOffline('bob');
|
|
169
|
+
expect(store.onlineNicks()).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('InMemoryMonitorStore — onlineNicks', () => {
|
|
174
|
+
it('returns the display spellings of every online nick', () => {
|
|
175
|
+
const store = new InMemoryMonitorStore();
|
|
176
|
+
store.onNickOnline('Bob');
|
|
177
|
+
store.onNickOnline('Carol');
|
|
178
|
+
expect(store.onlineNicks().sort()).toEqual(['Bob', 'Carol']);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('returns an empty array when no nick is online', () => {
|
|
182
|
+
const store = new InMemoryMonitorStore();
|
|
183
|
+
expect(store.onlineNicks()).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('InMemoryMonitorStore — drainPushes', () => {
|
|
188
|
+
it('returns the pending pushes and empties the queue', () => {
|
|
189
|
+
const store = new InMemoryMonitorStore();
|
|
190
|
+
store.sync('c1', new Set(), new Set(['bob']));
|
|
191
|
+
store.onNickOnline('bob');
|
|
192
|
+
expect(store.drainPushes()).toHaveLength(1);
|
|
193
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('returns an empty array when nothing is pending', () => {
|
|
197
|
+
const store = new InMemoryMonitorStore();
|
|
198
|
+
expect(store.drainPushes()).toEqual<MonitorPush[]>([]);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -162,6 +162,19 @@ describe('numericToName', () => {
|
|
|
162
162
|
it('returns undefined for an unregistered code', () => {
|
|
163
163
|
expect(numericToName.get(600)).toBeUndefined();
|
|
164
164
|
});
|
|
165
|
+
|
|
166
|
+
it('round-trips every entry in the Numerics registry (reverse-lookup parity)', () => {
|
|
167
|
+
// Pin the map-construction arrow function: every [name, code] pair in
|
|
168
|
+
// the forward table MUST appear inverted in numericToName. A mutated
|
|
169
|
+
// mapper (e.g. `() => undefined`) would leave the reverse table empty
|
|
170
|
+
// or fail to construct, which this exhaustive check catches.
|
|
171
|
+
const entries = Object.entries(Numerics) as ReadonlyArray<[NumericCode, number]>;
|
|
172
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
173
|
+
for (const [name, code] of entries) {
|
|
174
|
+
expect(numericToName.get(code)).toBe(name);
|
|
175
|
+
}
|
|
176
|
+
expect(numericToName.size).toBe(entries.length);
|
|
177
|
+
});
|
|
165
178
|
});
|
|
166
179
|
|
|
167
180
|
describe('isNumericCommand', () => {
|
|
@@ -133,31 +133,58 @@ describe('parse — framing tolerance', () => {
|
|
|
133
133
|
it('strips a lone trailing LF', () => {
|
|
134
134
|
expect(parse('PING :tok\n').params).toEqual(['tok']);
|
|
135
135
|
});
|
|
136
|
+
|
|
137
|
+
it('preserves CR/LF that appears inside the trailing param', () => {
|
|
138
|
+
// The CR/LF strip regex is anchored to the END of the line only; a CR/LF
|
|
139
|
+
// embedded in a trailing param must survive into the parsed value.
|
|
140
|
+
expect(parse('PRIVMSG #foo :a\rb').params[1]).toBe('a\rb');
|
|
141
|
+
expect(parse('PRIVMSG #foo :a\nb').params[1]).toBe('a\nb');
|
|
142
|
+
});
|
|
136
143
|
});
|
|
137
144
|
|
|
138
145
|
describe('parse — malformed input', () => {
|
|
146
|
+
// Each case asserts both the error class AND the diagnostic message text.
|
|
147
|
+
// The message assertion is what pins the failure to the *correct* guard:
|
|
148
|
+
// several guards fall through to a sibling throw of the same class, so a
|
|
149
|
+
// bare `toThrow(IrcParseError)` would miss removed/disabled guards.
|
|
150
|
+
|
|
139
151
|
it('throws IrcParseError on empty input', () => {
|
|
140
152
|
expect(() => parse('')).toThrow(IrcParseError);
|
|
153
|
+
expect(() => parse('')).toThrow('empty message');
|
|
141
154
|
});
|
|
142
155
|
|
|
143
156
|
it('throws IrcParseError on whitespace-only input', () => {
|
|
144
157
|
expect(() => parse(' ')).toThrow(IrcParseError);
|
|
158
|
+
expect(() => parse(' ')).toThrow('invalid command token');
|
|
145
159
|
});
|
|
146
160
|
|
|
147
161
|
it('throws IrcParseError when only a source prefix is present', () => {
|
|
148
162
|
expect(() => parse(':nick!user@host')).toThrow(IrcParseError);
|
|
163
|
+
expect(() => parse(':nick!user@host')).toThrow('source prefix with no command');
|
|
149
164
|
});
|
|
150
165
|
|
|
151
166
|
it('throws IrcParseError when a source is followed by no command', () => {
|
|
152
167
|
expect(() => parse(':irc.example.com ')).toThrow(IrcParseError);
|
|
168
|
+
expect(() => parse(':irc.example.com ')).toThrow('missing command');
|
|
153
169
|
});
|
|
154
170
|
|
|
155
171
|
it('throws IrcParseError when a tag section has no body', () => {
|
|
156
172
|
expect(() => parse('@time=now')).toThrow(IrcParseError);
|
|
173
|
+
expect(() => parse('@time=now')).toThrow('tag section with no message body');
|
|
157
174
|
});
|
|
158
175
|
|
|
159
176
|
it('throws IrcParseError on an invalid command token', () => {
|
|
160
177
|
expect(() => parse('12 hi')).toThrow(IrcParseError);
|
|
178
|
+
expect(() => parse('12 hi')).toThrow('invalid command token');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('sets the error `name` to "IrcParseError" for instanceof-equivalent checks', () => {
|
|
182
|
+
try {
|
|
183
|
+
parse('');
|
|
184
|
+
expect.fail('expected parse to throw');
|
|
185
|
+
} catch (err) {
|
|
186
|
+
expect((err as Error).name).toBe('IrcParseError');
|
|
187
|
+
}
|
|
161
188
|
});
|
|
162
189
|
});
|
|
163
190
|
|
|
@@ -203,3 +230,85 @@ describe('parse — RFC 1459 §2.3 input line-length cap', () => {
|
|
|
203
230
|
}
|
|
204
231
|
});
|
|
205
232
|
});
|
|
233
|
+
|
|
234
|
+
describe('parse — command token grammar (anchored regex)', () => {
|
|
235
|
+
// COMMAND_RE is anchored at both ends (^...$). A command token must be
|
|
236
|
+
// EITHER all letters OR exactly three digits — mixed alphanumerics are a
|
|
237
|
+
// protocol violation. These tests pin both anchors: removing ^ or $
|
|
238
|
+
// would let the partial-match mutations through.
|
|
239
|
+
|
|
240
|
+
it('rejects a command token with trailing digits (end anchor)', () => {
|
|
241
|
+
expect(() => parse('PRIVMSG123 #foo :hi')).toThrow(IrcParseError);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('rejects a command token with leading digits (start anchor)', () => {
|
|
245
|
+
expect(() => parse('123PRIVMSG #foo :hi')).toThrow(IrcParseError);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('rejects a two-digit numeric command (length pin)', () => {
|
|
249
|
+
expect(() => parse('12 #foo')).toThrow(IrcParseError);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('parse — source prefix shape (strict property presence)', () => {
|
|
254
|
+
// `user` and `host` must be ABSENT (not merely `undefined`) when the
|
|
255
|
+
// source has no `!user` / `@host` component. `toEqual` ignores
|
|
256
|
+
// undefined-valued properties, so these mutants would survive a normal
|
|
257
|
+
// equality check; `toStrictEqual` does not.
|
|
258
|
+
|
|
259
|
+
it('omits user AND host from a bare-servername source', () => {
|
|
260
|
+
expect(parse(':irc.example.com NOTICE * :hi').source).toStrictEqual({
|
|
261
|
+
name: 'irc.example.com',
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('omits host from a nick!user source (no @host)', () => {
|
|
266
|
+
expect(parse(':nick!user PRIVMSG #foo :hi').source).toStrictEqual({
|
|
267
|
+
name: 'nick',
|
|
268
|
+
user: 'user',
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('omits user from a nick@host source (no !user)', () => {
|
|
273
|
+
expect(parse(':nick@host PRIVMSG #foo :hi').source).toStrictEqual({
|
|
274
|
+
name: 'nick',
|
|
275
|
+
host: 'host',
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('does not set a source property on a source-less message', () => {
|
|
280
|
+
const msg = parse('PRIVMSG #foo :hi');
|
|
281
|
+
expect('source' in msg).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('parse — middle parameter whitespace collapsing', () => {
|
|
286
|
+
it('drops empty params produced by runs of spaces between middles', () => {
|
|
287
|
+
// The split-then-filter pipeline removes the empty strings that
|
|
288
|
+
// consecutive spaces generate. Removing the filter (or mutating the
|
|
289
|
+
// predicate to a tautology) would leak '' entries here.
|
|
290
|
+
expect(parse(':nick JOIN #a #b').params).toEqual(['#a', '#b']);
|
|
291
|
+
expect(parse(':nick JOIN #a #b ').params).toEqual(['#a', '#b']);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('unescapeTagValue — boundary conditions', () => {
|
|
296
|
+
// Imported indirectly through parse() tag values; these pin the loop
|
|
297
|
+
// bounds so off-by-one mutations on the backslash lookahead can't survive.
|
|
298
|
+
|
|
299
|
+
it('keeps a lone trailing backslash verbatim', () => {
|
|
300
|
+
// `\` at the end with no following char must not read past the end of
|
|
301
|
+
// the string and must be emitted literally.
|
|
302
|
+
expect(parse('@a=foo\\ PRIVMSG #foo :hi').tags.a).toBe('foo\\');
|
|
303
|
+
expect(parse('@a=\\ PRIVMSG #foo :hi').tags.a).toBe('\\');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('handles a tag value that is only a backslash', () => {
|
|
307
|
+
expect(parse('@a=\\ PRIVMSG #foo :hi').tags.a).toBe('\\');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('decodes a trailing escape sequence with no following character partially', () => {
|
|
311
|
+
// A complete `\\` pair decodes to a single backslash.
|
|
312
|
+
expect(parse('@a=x\\\\ PRIVMSG #foo :hi').tags.a).toBe('x\\');
|
|
313
|
+
});
|
|
314
|
+
});
|