serverless-ircd 0.5.0 → 0.6.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.
Files changed (167) hide show
  1. package/.github/workflows/ci.yml +34 -2
  2. package/.github/workflows/deploy-aws.yml +1 -2
  3. package/.github/workflows/deploy-cf-tcp.yml +1 -5
  4. package/.github/workflows/deploy-cf.yml +1 -2
  5. package/CHANGELOG.md +260 -0
  6. package/README.md +132 -22
  7. package/apps/aws-stack/README.md +33 -0
  8. package/apps/aws-stack/package.json +1 -1
  9. package/apps/aws-stack/src/aws-stack.ts +74 -9
  10. package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
  11. package/apps/aws-stack/tests/stack.test.ts +2 -2
  12. package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
  13. package/apps/aws-stack/tests/vitest.setup.ts +18 -0
  14. package/apps/aws-stack/vitest.config.ts +3 -0
  15. package/apps/cf-tcp-container/package.json +1 -1
  16. package/apps/cf-tcp-container/src/config-loader.ts +6 -7
  17. package/apps/cf-tcp-container/src/container-server.ts +3 -0
  18. package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
  19. package/apps/cf-tcp-container/wrangler.toml +1 -1
  20. package/apps/cf-worker/package.json +2 -1
  21. package/apps/cf-worker/vitest.config.ts +1 -1
  22. package/apps/cf-worker/wrangler.toml +5 -3
  23. package/apps/local-cli/package.json +1 -1
  24. package/apps/local-cli/src/config-loader.ts +6 -6
  25. package/apps/local-cli/src/main.ts +20 -0
  26. package/apps/local-cli/src/server.ts +9 -7
  27. package/apps/local-cli/tests/e2e.test.ts +112 -28
  28. package/apps/local-cli/tests/tcp.test.ts +71 -19
  29. package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
  30. package/biome.json +1 -0
  31. package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
  32. package/docs/ADR-002-location-of-authority.md +82 -0
  33. package/docs/ADR-003-durable-object-sharding.md +110 -0
  34. package/docs/ADR-004-dynamodb-schema.md +96 -0
  35. package/docs/ADR-005-wss-only-transport-v1.md +83 -0
  36. package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
  37. package/docs/ADR-007-deterministic-ports.md +82 -0
  38. package/docs/ADR-008-monorepo-tooling.md +60 -0
  39. package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
  40. package/docs/ADR-Index.md +15 -0
  41. package/docs/AWS-Adapter-Architecture.md +496 -0
  42. package/docs/AWS-Deployment.md +1275 -0
  43. package/docs/AWS-TCP-Deployment.md +410 -0
  44. package/docs/Cloudflare-Deployment-Guide.md +719 -0
  45. package/docs/Cloudflare-TCP-Deployment.md +510 -0
  46. package/docs/Home.md +15 -0
  47. package/docs/Observability.md +87 -0
  48. package/docs/PlanExtensions.md +812 -0
  49. package/docs/PlanIRCv3Websocket.md +503 -0
  50. package/docs/PlanWebClient.md +451 -0
  51. package/docs/Release-Process.md +443 -0
  52. package/package.json +2 -2
  53. package/packages/aws-adapter/package.json +1 -1
  54. package/packages/aws-adapter/src/handlers/default.ts +3 -0
  55. package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
  56. package/packages/aws-adapter/src/serialize.ts +29 -1
  57. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
  58. package/packages/aws-adapter/tests/account-store.test.ts +4 -7
  59. package/packages/aws-adapter/tests/aws-harness.ts +5 -2
  60. package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
  61. package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
  62. package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
  63. package/packages/aws-adapter/tests/connect.test.ts +9 -9
  64. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
  65. package/packages/aws-adapter/tests/global-setup.ts +55 -30
  66. package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
  67. package/packages/aws-adapter/tests/handlers.test.ts +90 -4
  68. package/packages/aws-adapter/tests/message-store.test.ts +6 -5
  69. package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
  70. package/packages/cf-adapter/package.json +2 -1
  71. package/packages/cf-adapter/src/connection-do.ts +3 -0
  72. package/packages/cf-adapter/src/serialize.ts +25 -4
  73. package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
  74. package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
  75. package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
  76. package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
  77. package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
  78. package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
  79. package/packages/cf-adapter/tests/serialize.test.ts +308 -0
  80. package/packages/cf-adapter/vitest.config.ts +1 -1
  81. package/packages/in-memory-runtime/package.json +1 -1
  82. package/packages/irc-core/package.json +1 -1
  83. package/packages/irc-core/src/caps/capabilities.ts +37 -3
  84. package/packages/irc-core/src/caps/index.ts +1 -0
  85. package/packages/irc-core/src/caps/sts.ts +84 -0
  86. package/packages/irc-core/src/commands/account-notify.ts +53 -0
  87. package/packages/irc-core/src/commands/away.ts +9 -3
  88. package/packages/irc-core/src/commands/cap.ts +23 -5
  89. package/packages/irc-core/src/commands/chathistory.ts +40 -11
  90. package/packages/irc-core/src/commands/index.ts +34 -0
  91. package/packages/irc-core/src/commands/isupport.ts +53 -0
  92. package/packages/irc-core/src/commands/mode.ts +8 -1
  93. package/packages/irc-core/src/commands/monitor.ts +327 -0
  94. package/packages/irc-core/src/commands/multiline.ts +256 -0
  95. package/packages/irc-core/src/commands/pre-away.ts +112 -0
  96. package/packages/irc-core/src/commands/privmsg.ts +19 -12
  97. package/packages/irc-core/src/commands/read-marker.ts +108 -0
  98. package/packages/irc-core/src/commands/registration.ts +8 -0
  99. package/packages/irc-core/src/commands/sasl.ts +24 -1
  100. package/packages/irc-core/src/commands/tagmsg.ts +71 -1
  101. package/packages/irc-core/src/commands/whois.ts +5 -0
  102. package/packages/irc-core/src/config.ts +65 -10
  103. package/packages/irc-core/src/effects.ts +14 -0
  104. package/packages/irc-core/src/ports.ts +389 -0
  105. package/packages/irc-core/src/protocol/index.ts +14 -0
  106. package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
  107. package/packages/irc-core/src/protocol/numerics.ts +15 -0
  108. package/packages/irc-core/src/protocol/outbound.ts +16 -1
  109. package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
  110. package/packages/irc-core/src/state/connection.ts +32 -1
  111. package/packages/irc-core/src/types.ts +112 -0
  112. package/packages/irc-core/stryker.commands.conf.json +1 -2
  113. package/packages/irc-core/tests/away-store.test.ts +73 -0
  114. package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
  115. package/packages/irc-core/tests/caps/sts.test.ts +123 -0
  116. package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
  117. package/packages/irc-core/tests/commands/away.test.ts +85 -2
  118. package/packages/irc-core/tests/commands/cap.test.ts +165 -0
  119. package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
  120. package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
  121. package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
  122. package/packages/irc-core/tests/commands/mode.test.ts +72 -0
  123. package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
  124. package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
  125. package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
  126. package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
  127. package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
  128. package/packages/irc-core/tests/commands/registration.test.ts +64 -0
  129. package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
  130. package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
  131. package/packages/irc-core/tests/commands/whois.test.ts +34 -0
  132. package/packages/irc-core/tests/config.test.ts +103 -13
  133. package/packages/irc-core/tests/labeled-response.test.ts +181 -0
  134. package/packages/irc-core/tests/message-tags.test.ts +114 -0
  135. package/packages/irc-core/tests/monitor-store.test.ts +200 -0
  136. package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
  137. package/packages/irc-core/tests/standard-replies.test.ts +412 -0
  138. package/packages/irc-core/tests/types.test.ts +27 -0
  139. package/packages/irc-server/package.json +1 -1
  140. package/packages/irc-server/src/actor.ts +450 -3
  141. package/packages/irc-server/src/dispatch.ts +108 -16
  142. package/packages/irc-server/tests/actor.test.ts +1432 -39
  143. package/packages/irc-server/tests/dispatch.test.ts +263 -2
  144. package/packages/irc-test-support/package.json +1 -1
  145. package/packages/irc-test-support/src/index.ts +6 -0
  146. package/packages/irc-test-support/src/test-config.ts +54 -0
  147. package/packages/irc-test-support/tests/test-config.test.ts +51 -0
  148. package/tools/ci-hardening/package.json +1 -1
  149. package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
  150. package/tools/load-test/package.json +33 -0
  151. package/tools/load-test/src/client.ts +351 -0
  152. package/tools/load-test/src/config.ts +313 -0
  153. package/tools/load-test/src/harness.ts +116 -0
  154. package/tools/load-test/src/main.ts +120 -0
  155. package/tools/load-test/src/metrics.ts +168 -0
  156. package/tools/load-test/src/report.ts +106 -0
  157. package/tools/load-test/tests/client.test.ts +212 -0
  158. package/tools/load-test/tests/config.test.ts +152 -0
  159. package/tools/load-test/tests/framing.test.ts +37 -0
  160. package/tools/load-test/tests/harness.test.ts +165 -0
  161. package/tools/load-test/tests/metrics.test.ts +174 -0
  162. package/tools/load-test/tests/report.test.ts +161 -0
  163. package/tools/load-test/tests/smoke.test.ts +67 -0
  164. package/tools/load-test/tsconfig.build.json +12 -0
  165. package/tools/load-test/tsconfig.test.json +10 -0
  166. package/tools/load-test/vitest.config.ts +29 -0
  167. package/tools/tcp-ws-forwarder/package.json +1 -1
@@ -294,7 +294,13 @@ function capAwareRuntime(members: ReadonlyMap<ConnId, MemberInfo>): {
294
294
  registration: 'registered',
295
295
  caps: info.caps,
296
296
  joinedChannels: new Set<string>(),
297
- userModes: { invisible: false, oper: false, wallops: false, serverNotices: false },
297
+ userModes: {
298
+ invisible: false,
299
+ oper: false,
300
+ wallops: false,
301
+ serverNotices: false,
302
+ tls: false,
303
+ },
298
304
  lastSeen: 0,
299
305
  connectedSince: 0,
300
306
  };
@@ -479,7 +485,13 @@ describe('dispatch — Broadcast cap / capLines', () => {
479
485
  registration: 'registered',
480
486
  caps: new Set(['chghost']),
481
487
  joinedChannels: new Set<string>(),
482
- userModes: { invisible: false, oper: false, wallops: false, serverNotices: false },
488
+ userModes: {
489
+ invisible: false,
490
+ oper: false,
491
+ wallops: false,
492
+ serverNotices: false,
493
+ tls: false,
494
+ },
483
495
  lastSeen: 0,
484
496
  connectedSince: 0,
485
497
  };
@@ -532,6 +544,255 @@ describe('dispatch — Broadcast cap / capLines', () => {
532
544
  });
533
545
  });
534
546
 
547
+ // ============================================================================
548
+ // dispatch — Broadcast `caps` (OR semantics)
549
+ //
550
+ // IRCv3 `draft/typing` ships a TAGMSG to peers that announced EITHER
551
+ // `draft/typing` OR `message-tags`. The reducer annotates the Broadcast
552
+ // effect with `caps` (plural); dispatch resolves per-recipient with OR
553
+ // semantics: a member receives `lines` iff they hold ANY of the listed caps.
554
+ // ============================================================================
555
+
556
+ describe('dispatch — Broadcast caps (OR semantics)', () => {
557
+ it('delivers lines to a member holding any of the listed caps', async () => {
558
+ const members = memberMap([
559
+ ['c1', { caps: new Set(['message-tags']) }],
560
+ ['c2', { caps: new Set(['draft/typing']) }],
561
+ ['c3', { caps: new Set<string>() }],
562
+ ]);
563
+ const { runtime, sends, broadcasts } = capAwareRuntime(members);
564
+
565
+ await dispatch(
566
+ [
567
+ Effect.broadcast(
568
+ '#foo',
569
+ [L('@+draft/typing=active :a!u@h TAGMSG #foo')],
570
+ 'c0',
571
+ undefined,
572
+ undefined,
573
+ ['message-tags', 'draft/typing'],
574
+ ),
575
+ ],
576
+ runtime,
577
+ );
578
+
579
+ expect(broadcasts).toHaveLength(0);
580
+ expect(sends.map((s) => s.to).sort()).toEqual(['c1', 'c2']);
581
+ expect(sends[0]?.lines).toEqual([L('@+draft/typing=active :a!u@h TAGMSG #foo')]);
582
+ });
583
+
584
+ it('does NOT double-deliver to a member holding both caps', async () => {
585
+ const members = memberMap([['c1', { caps: new Set(['message-tags', 'draft/typing']) }]]);
586
+ const { runtime, sends } = capAwareRuntime(members);
587
+
588
+ await dispatch(
589
+ [
590
+ Effect.broadcast('#foo', [L('hi')], undefined, undefined, undefined, [
591
+ 'message-tags',
592
+ 'draft/typing',
593
+ ]),
594
+ ],
595
+ runtime,
596
+ );
597
+
598
+ expect(sends).toHaveLength(1);
599
+ expect(sends[0]?.to).toBe('c1');
600
+ });
601
+
602
+ it('honors `except` alongside `caps`', async () => {
603
+ const members = memberMap([
604
+ ['c1', { caps: new Set(['message-tags']) }],
605
+ ['c2', { caps: new Set(['draft/typing']) }],
606
+ ]);
607
+ const { runtime, sends } = capAwareRuntime(members);
608
+
609
+ await dispatch(
610
+ [
611
+ Effect.broadcast('#foo', [L('hi')], 'c1', undefined, undefined, [
612
+ 'message-tags',
613
+ 'draft/typing',
614
+ ]),
615
+ ],
616
+ runtime,
617
+ );
618
+
619
+ expect(sends.map((s) => s.to)).toEqual(['c2']);
620
+ });
621
+
622
+ it('skips members whose connection info is unavailable (treats as no cap)', async () => {
623
+ const members = memberMap([['c1', { caps: new Set(['draft/typing']) }]]);
624
+ const { runtime, sends } = capAwareRuntime(members);
625
+
626
+ // Override getConnectionInfo to return null for c1 (connection gone).
627
+ const runtimeWithGone: IrcRuntime = {
628
+ ...runtime,
629
+ async getConnectionInfo() {
630
+ return null;
631
+ },
632
+ };
633
+
634
+ await dispatch(
635
+ [Effect.broadcast('#foo', [L('hi')], undefined, undefined, undefined, ['draft/typing'])],
636
+ runtimeWithGone,
637
+ );
638
+
639
+ expect(sends).toHaveLength(0);
640
+ });
641
+
642
+ it('is a no-op when the channel snapshot is null', async () => {
643
+ const sends: { to: ConnId; lines: RawLine[] }[] = [];
644
+ const runtime: IrcRuntime = {
645
+ async send(to, lines) {
646
+ sends.push({ to, lines });
647
+ },
648
+ async broadcast() {},
649
+ async broadcastWallops() {},
650
+ async disconnect() {},
651
+ async sendToNick() {},
652
+ async reserveNick() {
653
+ return { ok: true };
654
+ },
655
+ async changeNick() {
656
+ return true;
657
+ },
658
+ async releaseNick() {},
659
+ async applyChannelDelta() {},
660
+ async lookupNick() {
661
+ return null;
662
+ },
663
+ async getConnectionInfo() {
664
+ return null;
665
+ },
666
+ async getConnection() {
667
+ return null;
668
+ },
669
+ async getChannelSnapshot() {
670
+ return null;
671
+ },
672
+ async getChannelConnections() {
673
+ return new Map();
674
+ },
675
+ async listChannels() {
676
+ return [];
677
+ },
678
+ async reloadConfig() {
679
+ throw new Error('reloadConfig not configured for this fake');
680
+ },
681
+ };
682
+
683
+ await dispatch(
684
+ [Effect.broadcast('#nope', [L('x')], undefined, undefined, undefined, ['draft/typing'])],
685
+ runtime,
686
+ );
687
+
688
+ expect(sends).toHaveLength(0);
689
+ });
690
+ });
691
+
692
+ // ===========================================================================
693
+ // dispatch — Broadcast client-tag gating (message-tags)
694
+ //
695
+ // Recipients that did NOT negotiate `message-tags` must never receive a line
696
+ // beginning with `@+<client-tag>`. The dispatch broadcast per-recipient path
697
+ // applies filterClientTags so draft/multiline's `+draft/multiline` joined
698
+ // PRIVMSG is stripped for legacy peers, while message-tags peers still see it.
699
+ // ===========================================================================
700
+
701
+ describe('dispatch — Broadcast client-tag gating', () => {
702
+ it('strips +draft/multiline for a non-message-tags recipient (split mode)', async () => {
703
+ const members = memberMap([
704
+ ['c1', { caps: new Set(['draft/multiline']) }],
705
+ ['c2', { caps: new Set<string>() }],
706
+ ]);
707
+ const { runtime, sends } = capAwareRuntime(members);
708
+
709
+ await dispatch(
710
+ [
711
+ Effect.broadcast(
712
+ '#foo',
713
+ [L('@+draft/multiline :a!a@a PRIVMSG #foo :line one\nline two')],
714
+ 'c0',
715
+ 'draft/multiline',
716
+ [L('BATCH +b1 draft/multiline #foo'), L('BATCH -b1')],
717
+ ),
718
+ ],
719
+ runtime,
720
+ );
721
+
722
+ const toC2 = sends.find((s) => s.to === 'c2');
723
+ expect(toC2?.lines).toEqual([L(':a!a@a PRIVMSG #foo :line one\nline two')]);
724
+ });
725
+
726
+ it('preserves +draft/multiline for a message-tags recipient (split mode)', async () => {
727
+ const members = memberMap([
728
+ ['c1', { caps: new Set(['draft/multiline']) }],
729
+ ['c2', { caps: new Set(['message-tags']) }],
730
+ ]);
731
+ const { runtime, sends } = capAwareRuntime(members);
732
+
733
+ await dispatch(
734
+ [
735
+ Effect.broadcast(
736
+ '#foo',
737
+ [L('@+draft/multiline :a!a@a PRIVMSG #foo :line one\nline two')],
738
+ 'c0',
739
+ 'draft/multiline',
740
+ [L('BATCH +b1 draft/multiline #foo'), L('BATCH -b1')],
741
+ ),
742
+ ],
743
+ runtime,
744
+ );
745
+
746
+ const toC2 = sends.find((s) => s.to === 'c2');
747
+ expect(toC2?.lines).toEqual([L('@+draft/multiline :a!a@a PRIVMSG #foo :line one\nline two')]);
748
+ });
749
+
750
+ it('leaves capLines (BATCH replay) untouched for multiline-cap recipients', async () => {
751
+ const members = memberMap([['c1', { caps: new Set(['draft/multiline', 'message-tags']) }]]);
752
+ const { runtime, sends } = capAwareRuntime(members);
753
+
754
+ await dispatch(
755
+ [
756
+ Effect.broadcast(
757
+ '#foo',
758
+ [L('@+draft/multiline :a!a@a PRIVMSG #foo :joined')],
759
+ 'c0',
760
+ 'draft/multiline',
761
+ [L('BATCH +b1 draft/multiline #foo'), L(':a!a@a PRIVMSG #foo :one'), L('BATCH -b1')],
762
+ ),
763
+ ],
764
+ runtime,
765
+ );
766
+
767
+ const toC1 = sends.find((s) => s.to === 'c1');
768
+ expect(toC1?.lines).toEqual([
769
+ L('BATCH +b1 draft/multiline #foo'),
770
+ L(':a!a@a PRIVMSG #foo :one'),
771
+ L('BATCH -b1'),
772
+ ]);
773
+ });
774
+
775
+ it('strips client tags in cap-only mode too (no capLines)', async () => {
776
+ const members = memberMap([
777
+ ['c1', { caps: new Set(['message-tags']) }],
778
+ ['c2', { caps: new Set<string>() }],
779
+ ]);
780
+ const { runtime, sends } = capAwareRuntime(members);
781
+
782
+ await dispatch(
783
+ [Effect.broadcast('#foo', [L('@+draft/typing=active :a TAGMSG #foo')], 'c0', 'message-tags')],
784
+ runtime,
785
+ );
786
+
787
+ // c1 has message-tags → verbatim.
788
+ const toC1 = sends.find((s) => s.to === 'c1');
789
+ expect(toC1?.lines).toEqual([L('@+draft/typing=active :a TAGMSG #foo')]);
790
+ // c2 lacks message-tags AND the cap → receives nothing (cap-only mode).
791
+ const toC2 = sends.find((s) => s.to === 'c2');
792
+ expect(toC2).toBeUndefined();
793
+ });
794
+ });
795
+
535
796
  // ===========================================================================
536
797
  // dispatch observability
537
798
  // ===========================================================================
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/irc-test-support",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "description": "Shared IRC scenario runner + harness seam used by every adapter's integration suite",
6
6
  "license": "BSD-3-Clause",
@@ -25,3 +25,9 @@ export {
25
25
  inMemoryWsHarnessFactory,
26
26
  } from './in-memory-harness.js';
27
27
  export { runIrcScenarios } from './scenarios.js';
28
+ export {
29
+ TEST_NETWORK_NAME,
30
+ TEST_SERVER_NAME,
31
+ makeTestServerConfig,
32
+ type TestServerConfigOverrides,
33
+ } from './test-config.js';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Test-only server-config builder.
3
+ *
4
+ * Production config no longer ships a baseline `serverName` — every
5
+ * adapter's loader fails fast when the knob is missing. Test code that
6
+ * previously spread `DEFAULT_SERVER_CONFIG` needs an explicit, clearly
7
+ * test-scoped substitute so a forgotten override never silently advertises
8
+ * a placeholder hostname on the wire.
9
+ *
10
+ * The helper funnels its result through the shared `parseServerConfig` so
11
+ * the object shape stays schema-valid even when a test overrides
12
+ * individual knobs.
13
+ */
14
+
15
+ import { type ParsedServerConfig, parseServerConfig } from '@serverless-ircd/irc-core';
16
+
17
+ /**
18
+ * Input accepted by {@link makeTestServerConfig}: any subset of the
19
+ * schema's input shape. Fields the caller omits fall back to the
20
+ * test-only baseline.
21
+ */
22
+ export type TestServerConfigOverrides = Partial<Parameters<typeof parseServerConfig>[0]> &
23
+ Record<string, unknown>;
24
+
25
+ /**
26
+ * Test-only baseline hostname. Deliberately not `irc.example.com` (the
27
+ * retired placeholder) so any assertion that catches the placeholder
28
+ * leaking into production stays loud.
29
+ */
30
+ export const TEST_SERVER_NAME = 'irc.test.local';
31
+
32
+ /** Test-only baseline network name. */
33
+ export const TEST_NETWORK_NAME = 'TestNet';
34
+
35
+ /**
36
+ * Builds a fresh, schema-valid `ParsedServerConfig` for tests.
37
+ *
38
+ * Callers may omit `serverName`/`networkName` entirely — the helper
39
+ * supplies a test-only baseline that round-trips through
40
+ * `parseServerConfig`. Overrides are merged before parsing so the result
41
+ * always satisfies the schema (a bad override surfaces as a throw, same
42
+ * as a malformed production config).
43
+ *
44
+ * Each call returns an independent object; mutating the result never
45
+ * bleeds into subsequent calls.
46
+ */
47
+ export function makeTestServerConfig(overrides?: TestServerConfigOverrides): ParsedServerConfig {
48
+ const input: Record<string, unknown> = {
49
+ serverName: TEST_SERVER_NAME,
50
+ networkName: TEST_NETWORK_NAME,
51
+ ...overrides,
52
+ };
53
+ return parseServerConfig(input);
54
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Unit tests for the `makeTestServerConfig` test helper.
3
+ *
4
+ * The helper exists so test code can build a valid `ParsedServerConfig`
5
+ * without sourcing `serverName` from the production default constant
6
+ * (which no longer carries one). Every adapter's integration suite
7
+ * spreads this baseline; the helper guarantees the result still parses
8
+ * cleanly through the shared schema.
9
+ */
10
+
11
+ import { type ParsedServerConfig, parseServerConfig } from '@serverless-ircd/irc-core';
12
+ import { describe, expect, it } from 'vitest';
13
+ import { makeTestServerConfig } from '../src/test-config.js';
14
+
15
+ describe('makeTestServerConfig', () => {
16
+ it('returns a config whose serverName is not the public placeholder', () => {
17
+ const cfg = makeTestServerConfig();
18
+ expect(cfg.serverName).not.toBe('irc.example.com');
19
+ expect(cfg.serverName.length).toBeGreaterThan(0);
20
+ });
21
+
22
+ it('returns a config that round-trips through parseServerConfig', () => {
23
+ const cfg = makeTestServerConfig();
24
+ const reparsed = parseServerConfig(cfg);
25
+ expect(reparsed).toEqual(cfg);
26
+ });
27
+
28
+ it('supplies a non-empty networkName by default', () => {
29
+ const cfg = makeTestServerConfig();
30
+ expect(cfg.networkName.length).toBeGreaterThan(0);
31
+ });
32
+
33
+ it('lets the caller override serverName with an explicit test value', () => {
34
+ const cfg = makeTestServerConfig({ serverName: 'irc.test.local' });
35
+ expect(cfg.serverName).toBe('irc.test.local');
36
+ });
37
+
38
+ it('lets the caller override arbitrary knobs without dropping the baseline', () => {
39
+ const cfg = makeTestServerConfig({ maxClients: 7, nickLen: 9 });
40
+ expect(cfg.maxClients).toBe(7);
41
+ expect(cfg.nickLen).toBe(9);
42
+ expect(cfg.serverName.length).toBeGreaterThan(0);
43
+ });
44
+
45
+ it('returns a fresh object each call (no shared mutable baseline)', () => {
46
+ const a = makeTestServerConfig() as ParsedServerConfig & { motdLines: string[] };
47
+ const b = makeTestServerConfig() as ParsedServerConfig & { motdLines: string[] };
48
+ a.motdLines.push('mutated');
49
+ expect(b.motdLines).not.toContain('mutated');
50
+ });
51
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/ci-hardening",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "description": "CI hardening helpers: coverage-gate config validator + mutation-testing driver for irc-core",
6
6
  "license": "BSD-3-Clause",
@@ -0,0 +1,82 @@
1
+ /**
2
+ * CI capability self-checks for DynamoDB Local.
3
+ *
4
+ * The `aws-adapter` package's `globalSetup` boots a real DynamoDB Local
5
+ * instance so the integration suite (handlers/default.ts,
6
+ * handlers/nlb-stream.ts, aws-runtime.ts, …) can be exercised under
7
+ * coverage. Without a DynamoDB Local backend, 171 of 294 tests
8
+ * `describe.skipIf` away and `aws-adapter` coverage collapses to ~42%,
9
+ * blowing the 90% gate.
10
+ *
11
+ * These tests assert that the CI workflow wires the runner capability
12
+ * the globalSetup needs (a JRE for the official ZIP fallback) and that
13
+ * the strategy is pinned deterministically so the testcontainers
14
+ * probe does not burn ~60s of CI time before falling back.
15
+ */
16
+
17
+ import { readFileSync } from 'node:fs';
18
+ import { resolve } from 'node:path';
19
+ import { describe, expect, it } from 'vitest';
20
+
21
+ const REPO_ROOT = resolve(import.meta.dirname, '..', '..', '..');
22
+
23
+ function repoPath(rel: string): string {
24
+ return resolve(REPO_ROOT, rel);
25
+ }
26
+
27
+ function readText(rel: string): string {
28
+ return readFileSync(repoPath(rel), 'utf8');
29
+ }
30
+
31
+ describe('DynamoDB Local CI capability', () => {
32
+ it('installs a JRE on the CI runner so the ZIP fallback can fire', () => {
33
+ // The self-hosted Gitea runner image ships with neither Docker nor
34
+ // Java, so the globalSetup's testcontainers strategy fails fast
35
+ // ("Failed to connect to Reaper") and the ZIP strategy bails on
36
+ // `which('java') === null`. Installing a JRE via `setup-java`
37
+ // unblocks the ZIP path without requiring privileged mode or a
38
+ // `docker.sock` mount.
39
+ const ci = readText('.github/workflows/ci.yml');
40
+ expect(ci).toMatch(/setup-java/);
41
+ });
42
+
43
+ it('pins the DynamoDB Local strategy to ZIP in CI to skip the testcontainers probe', () => {
44
+ // testcontainers' Ryuk reaper container cannot start without a
45
+ // privileged runner / `docker.sock` mount; the probe blocks for
46
+ // tens of seconds (timeout) before falling back, which is flaky
47
+ // and slow. Pinning `DDB_LOCAL_STRATEGY=zip` on the coverage step
48
+ // makes globalSetup skip the Docker attempt entirely.
49
+ const ci = readText('.github/workflows/ci.yml');
50
+ expect(ci).toMatch(/DDB_LOCAL_STRATEGY/);
51
+ expect(ci).toMatch(/DDB_LOCAL_STRATEGY.*zip|zip.*DDB_LOCAL_STRATEGY/s);
52
+ });
53
+
54
+ it('documents the DynamoDB Local capability requirement in workflow comments', () => {
55
+ // The next contributor who touches the coverage job needs to know
56
+ // why Java is installed and why the strategy is pinned, so the
57
+ // capability is not silently dropped during a refactor.
58
+ const ci = readText('.github/workflows/ci.yml');
59
+ expect(ci).toMatch(/DynamoDB Local/i);
60
+ expect(ci).toMatch(/ZIP|Java/i);
61
+ });
62
+ });
63
+
64
+ describe('globalSetup strategy pinning', () => {
65
+ it('honors DDB_LOCAL_STRATEGY to skip the testcontainers attempt', () => {
66
+ // The globalSetup reads `DDB_LOCAL_STRATEGY` and short-circuits the
67
+ // Docker probe when set to `zip`, and the ZIP probe when set to
68
+ // `docker`. Default (`auto`) tries both for local dev parity.
69
+ const setup = readText('packages/aws-adapter/tests/global-setup.ts');
70
+ expect(setup).toMatch(/DDB_LOCAL_STRATEGY/);
71
+ // The two real strategies must be selectable explicitly.
72
+ expect(setup).toMatch(/['"]zip['"]/);
73
+ expect(setup).toMatch(/['"]docker['"]/);
74
+ });
75
+
76
+ it('still allows `auto` (both strategies) for local development', () => {
77
+ // The default behavior — try Docker, then ZIP — must remain so a
78
+ // developer with Docker Desktop still gets the testcontainers path.
79
+ const setup = readText('packages/aws-adapter/tests/global-setup.ts');
80
+ expect(setup).toMatch(/auto/);
81
+ });
82
+ });
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@serverless-ircd/load-test",
3
+ "version": "0.6.0",
4
+ "private": true,
5
+ "description": "Synthetic WebSocket IRC client farm for load testing ServerlessIRCd (concurrency, latency p50/p95/p99, drop rate)",
6
+ "license": "BSD-3-Clause",
7
+ "type": "module",
8
+ "main": "./dist/harness.js",
9
+ "bin": {
10
+ "load-test": "./dist/main.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.build.json",
14
+ "typecheck": "tsc -p tsconfig.test.json --noEmit",
15
+ "start": "node --enable-source-maps ./dist/main.js",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "coverage": "vitest run --coverage",
19
+ "clean": "rimraf dist coverage .tsbuildinfo .turbo"
20
+ },
21
+ "dependencies": {
22
+ "ws": "^8.18.0"
23
+ },
24
+ "devDependencies": {
25
+ "@serverless-ircd/local-cli": "workspace:*",
26
+ "@types/node": "^26.1.1",
27
+ "@types/ws": "^8.5.13",
28
+ "@vitest/coverage-v8": "^4.1.0",
29
+ "rimraf": "^6.0.0",
30
+ "typescript": "^5.6.0",
31
+ "vitest": "^4.1.0"
32
+ }
33
+ }