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,212 @@
|
|
|
1
|
+
import { type LocalServer, startLocalServer } from '@serverless-ircd/local-cli/dist/server.js';
|
|
2
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
3
|
+
import { WebSocketServer } from 'ws';
|
|
4
|
+
import { IrcLoadClient } from '../src/client';
|
|
5
|
+
import { resolveConfig } from '../src/config';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The synthetic IRC client is exercised against a real in-process IRC
|
|
9
|
+
* server (`startLocalServer` from local-cli, the project's e2e fixture
|
|
10
|
+
* target) so the lifecycle — CAP → NICK/USER → CAP END → 001 → JOIN →
|
|
11
|
+
* 366 → PRIVMSG echo — is faithful to what the load-test harness will
|
|
12
|
+
* drive against a deployed platform.
|
|
13
|
+
*/
|
|
14
|
+
describe('IrcLoadClient', () => {
|
|
15
|
+
let irc: LocalServer;
|
|
16
|
+
let target: string;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
irc = await startLocalServer({
|
|
20
|
+
port: 0,
|
|
21
|
+
hostname: '127.0.0.1',
|
|
22
|
+
serverName: 'irc.load-test.local',
|
|
23
|
+
networkName: 'LoadTestNet',
|
|
24
|
+
});
|
|
25
|
+
target = `ws://127.0.0.1:${irc.port}/`;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
// Best-effort: a fresh runtime per test keeps nick/channel state clean.
|
|
30
|
+
// local-cli's LocalServer has no per-test reset hook; we rely on unique
|
|
31
|
+
// nicks per test instead (see `cfg()` below).
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
await irc.close();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** Builds a tiny config against the live IRC server with a unique nick. */
|
|
39
|
+
function cfg(
|
|
40
|
+
overrides: Parameters<typeof resolveConfig>[0] = {},
|
|
41
|
+
): ReturnType<typeof resolveConfig> {
|
|
42
|
+
return resolveConfig({
|
|
43
|
+
target,
|
|
44
|
+
channel: `#load-${Math.random().toString(36).slice(2, 8)}`,
|
|
45
|
+
nickPrefix: 'load',
|
|
46
|
+
messagesPerClient: 0,
|
|
47
|
+
connectTimeoutMs: 2000,
|
|
48
|
+
registerTimeoutMs: 2000,
|
|
49
|
+
...overrides,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it('completes the full lifecycle (connect/register/join) with messagesPerClient=0', async () => {
|
|
54
|
+
const client = new IrcLoadClient({ id: 1, config: cfg({ messagesPerClient: 0 }) });
|
|
55
|
+
const result = await client.run();
|
|
56
|
+
expect(result.ok).toBe(true);
|
|
57
|
+
expect(result.samples.connect.length).toBe(1);
|
|
58
|
+
expect(result.samples.register.length).toBe(1);
|
|
59
|
+
expect(result.samples.join.length).toBe(1);
|
|
60
|
+
expect(result.samples.privmsg.length).toBe(0);
|
|
61
|
+
// Latencies are real and positive for a reached phase.
|
|
62
|
+
expect(result.samples.connect[0]).toBeGreaterThanOrEqual(0);
|
|
63
|
+
expect(result.samples.register[0]).toBeGreaterThan(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('measures a per-message echo latency when echo-message is negotiated', async () => {
|
|
67
|
+
const client = new IrcLoadClient({
|
|
68
|
+
id: 2,
|
|
69
|
+
config: cfg({ messagesPerClient: 3, messageIntervalMs: 0 }),
|
|
70
|
+
});
|
|
71
|
+
const result = await client.run();
|
|
72
|
+
expect(result.ok).toBe(true);
|
|
73
|
+
expect(result.samples.privmsg.length).toBe(3);
|
|
74
|
+
for (const lat of result.samples.privmsg) {
|
|
75
|
+
expect(lat).toBeGreaterThanOrEqual(0);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('reports ok=false with failedPhase=connect against a non-listening port', async () => {
|
|
80
|
+
// Pick an unused high port by binding+closing to free its number.
|
|
81
|
+
const client = new IrcLoadClient({
|
|
82
|
+
id: 3,
|
|
83
|
+
config: resolveConfig({
|
|
84
|
+
target: 'ws://127.0.0.1:9/', // port 9 (discard) — refused on most hosts
|
|
85
|
+
connectTimeoutMs: 1000,
|
|
86
|
+
registerTimeoutMs: 1000,
|
|
87
|
+
channel: '#x',
|
|
88
|
+
messagesPerClient: 0,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
const result = await client.run();
|
|
92
|
+
expect(result.ok).toBe(false);
|
|
93
|
+
expect(result.failedPhase).toBe('connect');
|
|
94
|
+
expect(result.error).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('reports ok=false with failedPhase=register against a silent server', async () => {
|
|
98
|
+
// A WS server that accepts the handshake but never speaks IRC → the
|
|
99
|
+
// client's wait for 001 must time out at the register phase.
|
|
100
|
+
const silent = await startSilentWsServer();
|
|
101
|
+
try {
|
|
102
|
+
const client = new IrcLoadClient({
|
|
103
|
+
id: 4,
|
|
104
|
+
config: resolveConfig({
|
|
105
|
+
target: `ws://127.0.0.1:${silent.port}/`,
|
|
106
|
+
connectTimeoutMs: 1000,
|
|
107
|
+
registerTimeoutMs: 250,
|
|
108
|
+
channel: '#x',
|
|
109
|
+
messagesPerClient: 0,
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
const result = await client.run();
|
|
113
|
+
expect(result.ok).toBe(false);
|
|
114
|
+
expect(result.failedPhase).toBe('register');
|
|
115
|
+
// connect succeeded, so a connect sample is present.
|
|
116
|
+
expect(result.samples.connect.length).toBe(1);
|
|
117
|
+
} finally {
|
|
118
|
+
await silent.close();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('reports ok=false with failedPhase=register when the server closes mid-handshake', async () => {
|
|
123
|
+
// A WS server that accepts the handshake then immediately closes → the
|
|
124
|
+
// client's pending register waiter is rejected via the close handler
|
|
125
|
+
// (exercises failWaiters with a live waiter, not a timeout).
|
|
126
|
+
const dropping = await startDroppingWsServer();
|
|
127
|
+
try {
|
|
128
|
+
const client = new IrcLoadClient({
|
|
129
|
+
id: 5,
|
|
130
|
+
config: resolveConfig({
|
|
131
|
+
target: `ws://127.0.0.1:${dropping.port}/`,
|
|
132
|
+
connectTimeoutMs: 1000,
|
|
133
|
+
registerTimeoutMs: 2000,
|
|
134
|
+
channel: '#x',
|
|
135
|
+
messagesPerClient: 0,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
const result = await client.run();
|
|
139
|
+
expect(result.ok).toBe(false);
|
|
140
|
+
expect(result.failedPhase).toBe('register');
|
|
141
|
+
expect(result.samples.connect.length).toBe(1);
|
|
142
|
+
} finally {
|
|
143
|
+
await dropping.close();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('paces PRIVMSGs by messageIntervalMs (>0 path)', async () => {
|
|
148
|
+
const client = new IrcLoadClient({
|
|
149
|
+
id: 6,
|
|
150
|
+
config: cfg({ messagesPerClient: 2, messageIntervalMs: 20 }),
|
|
151
|
+
});
|
|
152
|
+
const result = await client.run();
|
|
153
|
+
expect(result.ok).toBe(true);
|
|
154
|
+
expect(result.samples.privmsg.length).toBe(2);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/** A minimal WS server that completes the handshake but sends nothing. */
|
|
159
|
+
function startSilentWsServer(): Promise<{ port: number; close: () => Promise<void> }> {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const wss = new WebSocketServer({ port: 0 });
|
|
162
|
+
const onError = (err: Error): void => {
|
|
163
|
+
wss.close();
|
|
164
|
+
reject(err);
|
|
165
|
+
};
|
|
166
|
+
wss.once('listening', () => {
|
|
167
|
+
wss.removeListener('error', onError);
|
|
168
|
+
const addr = wss.address();
|
|
169
|
+
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
|
|
170
|
+
resolve({
|
|
171
|
+
port,
|
|
172
|
+
close: () =>
|
|
173
|
+
new Promise<void>((res) => {
|
|
174
|
+
wss.close(() => res());
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
wss.once('error', onError);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A WS server that accepts a connection then immediately closes it, so the
|
|
184
|
+
* client's pending register waiter is rejected via the close handler
|
|
185
|
+
* rather than a timeout.
|
|
186
|
+
*/
|
|
187
|
+
function startDroppingWsServer(): Promise<{ port: number; close: () => Promise<void> }> {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const wss = new WebSocketServer({ port: 0 });
|
|
190
|
+
const onError = (err: Error): void => {
|
|
191
|
+
wss.close();
|
|
192
|
+
reject(err);
|
|
193
|
+
};
|
|
194
|
+
wss.on('connection', (sock) => {
|
|
195
|
+
// Drop immediately; simulate a server that tears down mid-handshake.
|
|
196
|
+
sock.close();
|
|
197
|
+
});
|
|
198
|
+
wss.once('listening', () => {
|
|
199
|
+
wss.removeListener('error', onError);
|
|
200
|
+
const addr = wss.address();
|
|
201
|
+
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
|
|
202
|
+
resolve({
|
|
203
|
+
port,
|
|
204
|
+
close: () =>
|
|
205
|
+
new Promise<void>((res) => {
|
|
206
|
+
wss.close(() => res());
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
wss.once('error', onError);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { type LoadTestConfig, parseArgs, resolveConfig } from '../src/config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The config module owns defaulting + validation for a load-test run. It is
|
|
6
|
+
* the fail-fast gate: a malformed target URL or a nonsensical concurrency
|
|
7
|
+
* value throws here, before the harness opens a single socket.
|
|
8
|
+
*/
|
|
9
|
+
describe('resolveConfig — defaults', () => {
|
|
10
|
+
it('applies documented defaults when only the target is supplied', () => {
|
|
11
|
+
const cfg = resolveConfig({ target: 'ws://127.0.0.1:6667/' });
|
|
12
|
+
expect(cfg.target).toBe('ws://127.0.0.1:6667/');
|
|
13
|
+
expect(cfg.connections).toBeGreaterThan(0);
|
|
14
|
+
expect(cfg.channel.startsWith('#')).toBe(true);
|
|
15
|
+
expect(cfg.messagesPerClient).toBeGreaterThanOrEqual(0);
|
|
16
|
+
expect(cfg.connectConcurrency).toBeGreaterThanOrEqual(1);
|
|
17
|
+
expect(cfg.connectTimeoutMs).toBeGreaterThan(0);
|
|
18
|
+
expect(cfg.registerTimeoutMs).toBeGreaterThan(0);
|
|
19
|
+
expect(cfg.rampMs).toBeGreaterThanOrEqual(0);
|
|
20
|
+
expect(cfg.messageIntervalMs).toBeGreaterThanOrEqual(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('respects explicit overrides over defaults', () => {
|
|
24
|
+
const cfg = resolveConfig({
|
|
25
|
+
target: 'wss://irc.example.com/',
|
|
26
|
+
connections: 25000,
|
|
27
|
+
channel: '#perf',
|
|
28
|
+
messagesPerClient: 7,
|
|
29
|
+
messageIntervalMs: 50,
|
|
30
|
+
rampMs: 30_000,
|
|
31
|
+
connectConcurrency: 500,
|
|
32
|
+
});
|
|
33
|
+
expect(cfg.connections).toBe(25000);
|
|
34
|
+
expect(cfg.channel).toBe('#perf');
|
|
35
|
+
expect(cfg.messagesPerClient).toBe(7);
|
|
36
|
+
expect(cfg.messageIntervalMs).toBe(50);
|
|
37
|
+
expect(cfg.rampMs).toBe(30_000);
|
|
38
|
+
expect(cfg.connectConcurrency).toBe(500);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('resolveConfig — validation (fail-fast)', () => {
|
|
43
|
+
it('throws when the target is missing entirely', () => {
|
|
44
|
+
expect(() => resolveConfig({})).toThrow(/target/i);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws for a non-ws scheme', () => {
|
|
48
|
+
expect(() => resolveConfig({ target: 'http://example.com/' })).toThrow(/ws:\/\/|wss:\/\//);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('throws for an unparseable target URL', () => {
|
|
52
|
+
expect(() => resolveConfig({ target: 'not a url' })).toThrow(/target/i);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('throws when connections < 1', () => {
|
|
56
|
+
expect(() => resolveConfig({ target: 'ws://x/', connections: 0 })).toThrow(/connections/i);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('throws when connectConcurrency < 1', () => {
|
|
60
|
+
expect(() => resolveConfig({ target: 'ws://x/', connectConcurrency: 0 })).toThrow(
|
|
61
|
+
/concurrency/i,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('throws when messagesPerClient is negative', () => {
|
|
66
|
+
expect(() => resolveConfig({ target: 'ws://x/', messagesPerClient: -1 })).toThrow(/messages/i);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('throws when the channel does not start with #', () => {
|
|
70
|
+
expect(() => resolveConfig({ target: 'ws://x/', channel: 'loadtest' })).toThrow(/channel/i);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('parseArgs — CLI flag parsing', () => {
|
|
75
|
+
it('parses the full flag set into a resolved config', () => {
|
|
76
|
+
const cfg = parseArgs([
|
|
77
|
+
'--target',
|
|
78
|
+
'wss://irc.staging.example.com/',
|
|
79
|
+
'--connections',
|
|
80
|
+
'10000',
|
|
81
|
+
'--channel',
|
|
82
|
+
'#loadtest',
|
|
83
|
+
'--messages',
|
|
84
|
+
'3',
|
|
85
|
+
'--ramp-ms',
|
|
86
|
+
'60000',
|
|
87
|
+
'--concurrency',
|
|
88
|
+
'200',
|
|
89
|
+
'--platform',
|
|
90
|
+
'cf-staging',
|
|
91
|
+
]);
|
|
92
|
+
expect(cfg.target).toBe('wss://irc.staging.example.com/');
|
|
93
|
+
expect(cfg.connections).toBe(10000);
|
|
94
|
+
expect(cfg.channel).toBe('#loadtest');
|
|
95
|
+
expect(cfg.messagesPerClient).toBe(3);
|
|
96
|
+
expect(cfg.rampMs).toBe(60_000);
|
|
97
|
+
expect(cfg.connectConcurrency).toBe(200);
|
|
98
|
+
expect(cfg.platform).toBe('cf-staging');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('allows a bare positional target URL', () => {
|
|
102
|
+
const cfg = parseArgs(['ws://localhost:8787/']);
|
|
103
|
+
expect(cfg.target).toBe('ws://localhost:8787/');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('throws on an unknown flag', () => {
|
|
107
|
+
expect(() => parseArgs(['--target', 'ws://x/', '--bogus', '1'])).toThrow(/unknown argument/i);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('throws when --target is absent and no positional is given', () => {
|
|
111
|
+
expect(() => parseArgs([])).toThrow(/target/i);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('throws on a non-numeric --connections', () => {
|
|
115
|
+
expect(() => parseArgs(['--target', 'ws://x/', '--connections', 'lots'])).toThrow();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('disables echo-message with --no-echo-message', () => {
|
|
119
|
+
const cfg = parseArgs(['ws://x/', '--messages', '3', '--no-echo-message']);
|
|
120
|
+
expect(cfg.echoMessage).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('throws when a string flag has no following value', () => {
|
|
124
|
+
expect(() => parseArgs(['--target'])).toThrow(/requires a value/i);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('throws when a numeric flag has no following value', () => {
|
|
128
|
+
expect(() => parseArgs(['ws://x/', '--connections'])).toThrow(/requires a value/i);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('echo-message defaults on when --messages > 0 and off when 0', () => {
|
|
132
|
+
expect(parseArgs(['ws://x/', '--messages', '2']).echoMessage).toBe(true);
|
|
133
|
+
expect(parseArgs(['ws://x/', '--messages', '0']).echoMessage).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('emits a typed LoadTestConfig (compile-time shape check)', () => {
|
|
137
|
+
const cfg: LoadTestConfig = parseArgs(['ws://127.0.0.1:6667/']);
|
|
138
|
+
// Touch every field so a rename/refactor breaks this test loudly.
|
|
139
|
+
expect(typeof cfg.target).toBe('string');
|
|
140
|
+
expect(typeof cfg.platform).toBe('string');
|
|
141
|
+
expect(typeof cfg.connections).toBe('number');
|
|
142
|
+
expect(typeof cfg.rampMs).toBe('number');
|
|
143
|
+
expect(typeof cfg.connectConcurrency).toBe('number');
|
|
144
|
+
expect(typeof cfg.connectTimeoutMs).toBe('number');
|
|
145
|
+
expect(typeof cfg.registerTimeoutMs).toBe('number');
|
|
146
|
+
expect(typeof cfg.channel).toBe('string');
|
|
147
|
+
expect(typeof cfg.nickPrefix).toBe('string');
|
|
148
|
+
expect(typeof cfg.messagesPerClient).toBe('number');
|
|
149
|
+
expect(typeof cfg.messageIntervalMs).toBe('number');
|
|
150
|
+
expect(typeof cfg.echoMessage).toBe('boolean');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { wsFrameToText } from '../src/client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure unit tests for the ws payload normaliser. The `Buffer[]` /
|
|
6
|
+
* `ArrayBuffer` / `Uint8Array` variants only appear under non-default
|
|
7
|
+
* `binaryType` / fragmentation settings, so they are pinned here in
|
|
8
|
+
* isolation rather than through real sockets. Mirrors the forwarder's
|
|
9
|
+
* `toBuffer` contract.
|
|
10
|
+
*/
|
|
11
|
+
describe('wsFrameToText', () => {
|
|
12
|
+
it('returns a string payload unchanged', () => {
|
|
13
|
+
expect(wsFrameToText(':srv 001 nick :Hi')).toBe(':srv 001 nick :Hi');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('decodes a Buffer payload as UTF-8', () => {
|
|
17
|
+
expect(wsFrameToText(Buffer.from('hello', 'utf8'))).toBe('hello');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('concatenates a Buffer[] payload', () => {
|
|
21
|
+
expect(wsFrameToText([Buffer.from('foo', 'utf8'), Buffer.from('bar', 'utf8')])).toBe('foobar');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('wraps an ArrayBuffer payload', () => {
|
|
25
|
+
const src = new ArrayBuffer(3);
|
|
26
|
+
new Uint8Array(src).set([0x41, 0x42, 0x43]); // 'ABC'
|
|
27
|
+
expect(wsFrameToText(src)).toBe('ABC');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('copies a Uint8Array payload', () => {
|
|
31
|
+
expect(wsFrameToText(new Uint8Array([0x41, 0x42, 0x43]))).toBe('ABC');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('preserves multi-byte UTF-8 across the Buffer path', () => {
|
|
35
|
+
expect(wsFrameToText(Buffer.from('café', 'utf8'))).toBe('café');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveConfig } from '../src/config';
|
|
3
|
+
import { type SyntheticClient, runLoadTest } from '../src/harness';
|
|
4
|
+
import type { ClientResult } from '../src/metrics';
|
|
5
|
+
import { generateReport } from '../src/report';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The harness is the orchestrator: it spawns N synthetic clients under a
|
|
9
|
+
* concurrency cap, ramps them over a window, and aggregates their results.
|
|
10
|
+
*
|
|
11
|
+
* The smoke test against the real in-process IRC server is the ticket's
|
|
12
|
+
* explicit acceptance criterion ("load test script completes without
|
|
13
|
+
* crashing"); the fake-factory tests pin the concurrency cap, ramp, and
|
|
14
|
+
* aggregation math without touching real sockets.
|
|
15
|
+
*/
|
|
16
|
+
function cfg(
|
|
17
|
+
overrides: Parameters<typeof resolveConfig>[0] = {},
|
|
18
|
+
): ReturnType<typeof resolveConfig> {
|
|
19
|
+
return resolveConfig({
|
|
20
|
+
target: 'ws://127.0.0.1:1/',
|
|
21
|
+
connections: 10,
|
|
22
|
+
connectConcurrency: 3,
|
|
23
|
+
rampMs: 0,
|
|
24
|
+
messagesPerClient: 0,
|
|
25
|
+
channel: '#loadtest',
|
|
26
|
+
// Short timeouts so a fake that never resolves still completes quickly.
|
|
27
|
+
connectTimeoutMs: 100,
|
|
28
|
+
registerTimeoutMs: 100,
|
|
29
|
+
...overrides,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Builds a fake client factory whose clients resolve to scripted results. */
|
|
34
|
+
function scriptedFactory(results: ClientResult[]): (opts: unknown) => SyntheticClient {
|
|
35
|
+
let i = 0;
|
|
36
|
+
return () => {
|
|
37
|
+
const idx = i++;
|
|
38
|
+
const result = results[idx % results.length] ?? {
|
|
39
|
+
id: idx,
|
|
40
|
+
ok: true,
|
|
41
|
+
samples: { connect: [1], register: [], join: [], privmsg: [] },
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
run: async () => ({ ...result, id: idx }),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('runLoadTest — aggregation', () => {
|
|
50
|
+
it('collects every client result into the summary', async () => {
|
|
51
|
+
const results: ClientResult[] = [
|
|
52
|
+
{ id: 0, ok: true, samples: { connect: [10], register: [20], join: [5], privmsg: [] } },
|
|
53
|
+
{ id: 1, ok: true, samples: { connect: [12], register: [25], join: [6], privmsg: [] } },
|
|
54
|
+
{
|
|
55
|
+
id: 2,
|
|
56
|
+
ok: false,
|
|
57
|
+
error: 'register timeout',
|
|
58
|
+
failedPhase: 'register',
|
|
59
|
+
samples: { connect: [9], register: [], join: [], privmsg: [] },
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
const run = await runLoadTest(cfg({ connections: 3 }), {
|
|
63
|
+
clientFactory: scriptedFactory(results),
|
|
64
|
+
});
|
|
65
|
+
expect(run.summary.totalClients).toBe(3);
|
|
66
|
+
expect(run.summary.succeeded).toBe(2);
|
|
67
|
+
expect(run.summary.failed).toBe(1);
|
|
68
|
+
expect(run.summary.dropRate).toBeCloseTo(1 / 3, 5);
|
|
69
|
+
expect(run.summary.phases.connect.count).toBe(3);
|
|
70
|
+
expect(run.summary.phases.register.count).toBe(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns a LoadTestRun carrying config + timing + a renderable report', async () => {
|
|
74
|
+
const run = await runLoadTest(cfg({ connections: 2 }), {
|
|
75
|
+
clientFactory: scriptedFactory([
|
|
76
|
+
{ id: 0, ok: true, samples: { connect: [10], register: [20], join: [5], privmsg: [] } },
|
|
77
|
+
]),
|
|
78
|
+
});
|
|
79
|
+
expect(run.config.connections).toBe(2);
|
|
80
|
+
expect(run.durationMs).toBeGreaterThanOrEqual(0);
|
|
81
|
+
const md = generateReport(run);
|
|
82
|
+
expect(md).toContain('Latency');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('runLoadTest — concurrency cap', () => {
|
|
87
|
+
it('never exceeds connectConcurrency simultaneous in-flight clients', async () => {
|
|
88
|
+
const tracker = new ConcurrencyTracker();
|
|
89
|
+
const factory = () => ({
|
|
90
|
+
run: () =>
|
|
91
|
+
tracker.run(async () => {
|
|
92
|
+
const samples = { connect: [1], register: [], join: [], privmsg: [] };
|
|
93
|
+
return { id: 0, ok: true, samples } as ClientResult;
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
const run = await runLoadTest(cfg({ connections: 20, connectConcurrency: 4 }), {
|
|
97
|
+
clientFactory: factory,
|
|
98
|
+
});
|
|
99
|
+
expect(run.summary.totalClients).toBe(20);
|
|
100
|
+
expect(tracker.maxObserved).toBeLessThanOrEqual(4);
|
|
101
|
+
expect(tracker.maxObserved).toBeGreaterThanOrEqual(1);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('runLoadTest — ramp', () => {
|
|
106
|
+
it('spreads client launches across the ramp window', async () => {
|
|
107
|
+
const starts: number[] = [];
|
|
108
|
+
const t0 = Date.now();
|
|
109
|
+
const factory = () => ({
|
|
110
|
+
run: async () => {
|
|
111
|
+
starts.push(Date.now() - t0);
|
|
112
|
+
// a tiny yield so the event loop can interleave launches
|
|
113
|
+
await Promise.resolve();
|
|
114
|
+
return {
|
|
115
|
+
id: 0,
|
|
116
|
+
ok: true,
|
|
117
|
+
samples: { connect: [1], register: [], join: [], privmsg: [] },
|
|
118
|
+
} as ClientResult;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
// 5 clients, ramp 200ms → launches should spread ~50ms apart. With
|
|
122
|
+
// concurrency 5 they all queue immediately but each launch is delayed
|
|
123
|
+
// by its ramp share, so the last launch should land well after the first.
|
|
124
|
+
await runLoadTest(cfg({ connections: 5, connectConcurrency: 5, rampMs: 200 }), {
|
|
125
|
+
clientFactory: factory,
|
|
126
|
+
});
|
|
127
|
+
starts.sort((a, b) => a - b);
|
|
128
|
+
const first = starts[0] ?? 0;
|
|
129
|
+
const last = starts[4] ?? 0;
|
|
130
|
+
const spread = last - first;
|
|
131
|
+
expect(spread).toBeGreaterThanOrEqual(150);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('completes instantly when rampMs=0 (burst)', async () => {
|
|
135
|
+
const factory = () => ({
|
|
136
|
+
run: async () =>
|
|
137
|
+
({
|
|
138
|
+
id: 0,
|
|
139
|
+
ok: true,
|
|
140
|
+
samples: { connect: [1], register: [], join: [], privmsg: [] },
|
|
141
|
+
}) as ClientResult,
|
|
142
|
+
});
|
|
143
|
+
const t0 = Date.now();
|
|
144
|
+
await runLoadTest(cfg({ connections: 10, connectConcurrency: 10, rampMs: 0 }), {
|
|
145
|
+
clientFactory: factory,
|
|
146
|
+
});
|
|
147
|
+
expect(Date.now() - t0).toBeLessThan(500);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/** Tracks the high-water mark of concurrently in-flight fake clients. */
|
|
152
|
+
class ConcurrencyTracker {
|
|
153
|
+
active = 0;
|
|
154
|
+
maxObserved = 0;
|
|
155
|
+
|
|
156
|
+
async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
157
|
+
this.active++;
|
|
158
|
+
if (this.active > this.maxObserved) this.maxObserved = this.active;
|
|
159
|
+
try {
|
|
160
|
+
return await fn();
|
|
161
|
+
} finally {
|
|
162
|
+
this.active--;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|