serverless-ircd 0.4.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.
- package/.github/workflows/ci.yml +34 -2
- 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/CHANGELOG.md +435 -0
- package/README.md +206 -27
- package/apps/aws-stack/README.md +37 -3
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +106 -13
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +49 -3
- 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/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.toml +5 -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 +138 -28
- package/apps/local-cli/tests/e2e.test.ts +113 -29
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +262 -0
- package/biome.json +1 -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 +15 -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/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +69 -0
- package/packages/aws-adapter/src/handlers/connect.ts +36 -5
- package/packages/aws-adapter/src/handlers/default.ts +66 -5
- package/packages/aws-adapter/src/handlers/index.ts +41 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +18 -0
- package/packages/aws-adapter/src/index.ts +2 -0
- package/packages/aws-adapter/src/serialize.ts +40 -2
- package/packages/aws-adapter/src/stats.ts +80 -0
- 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 +140 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +100 -4
- 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 +238 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +62 -8
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/cf-adapter/package.json +6 -1
- package/packages/cf-adapter/src/cf-runtime.ts +66 -1
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/connection-do.ts +185 -54
- package/packages/cf-adapter/src/env.ts +25 -6
- package/packages/cf-adapter/src/index.ts +2 -0
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +1 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +4 -4
- package/packages/cf-adapter/tests/cf-runtime.test.ts +307 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +119 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +2 -2
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +108 -4
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +7 -7
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/cf-adapter/wrangler.test.toml +7 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +38 -4
- 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 +42 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/isupport.ts +59 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +12 -9
- 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/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- 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/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +72 -10
- package/packages/irc-core/src/effects.ts +41 -1
- package/packages/irc-core/src/index.ts +2 -0
- package/packages/irc-core/src/ports.ts +568 -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 +57 -11
- package/packages/irc-core/src/protocol/outbound.ts +36 -4
- 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 +120 -1
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/away-store.test.ts +73 -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/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +129 -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/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -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/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -0
- 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 +90 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -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/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +699 -19
- package/packages/irc-server/src/dispatch.ts +109 -16
- package/packages/irc-server/src/routing.ts +3 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +10 -7
- package/packages/irc-server/tests/actor.test.ts +2523 -42
- package/packages/irc-server/tests/dispatch.test.ts +300 -2
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +1 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- 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 +9 -1
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +1 -1
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/pnpm-workspace.yaml +1 -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/package.json +6 -1
- package/tools/seed-cf-accounts.ts +4 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
|
@@ -119,7 +119,13 @@ describe('privmsgChannelReducer — success', () => {
|
|
|
119
119
|
);
|
|
120
120
|
|
|
121
121
|
expect(out.effects).toEqual<EffectType[]>([
|
|
122
|
-
Effect.broadcast(
|
|
122
|
+
Effect.broadcast(
|
|
123
|
+
'#foo',
|
|
124
|
+
[L(':alice!alice@example.com PRIVMSG #foo :hello world')],
|
|
125
|
+
'c1',
|
|
126
|
+
'msgid',
|
|
127
|
+
[L('@msgid=nonce-0 :alice!alice@example.com PRIVMSG #foo :hello world')],
|
|
128
|
+
),
|
|
123
129
|
]);
|
|
124
130
|
});
|
|
125
131
|
|
|
@@ -140,6 +146,8 @@ describe('privmsgChannelReducer — success', () => {
|
|
|
140
146
|
'#foo',
|
|
141
147
|
[L(':alice!alice@example.com PRIVMSG #foo :hi there everyone')],
|
|
142
148
|
'c1',
|
|
149
|
+
'msgid',
|
|
150
|
+
[L('@msgid=nonce-0 :alice!alice@example.com PRIVMSG #foo :hi there everyone')],
|
|
143
151
|
),
|
|
144
152
|
);
|
|
145
153
|
});
|
|
@@ -415,7 +423,9 @@ describe('privmsgChannelReducer — rejections', () => {
|
|
|
415
423
|
);
|
|
416
424
|
|
|
417
425
|
expect(out.effects).toEqual<EffectType[]>([
|
|
418
|
-
Effect.broadcast('#foo', [L(':? PRIVMSG #foo :hi')], 'c1'
|
|
426
|
+
Effect.broadcast('#foo', [L(':? PRIVMSG #foo :hi')], 'c1', 'msgid', [
|
|
427
|
+
L('@msgid=nonce-0 :? PRIVMSG #foo :hi'),
|
|
428
|
+
]),
|
|
419
429
|
]);
|
|
420
430
|
});
|
|
421
431
|
});
|
|
@@ -439,7 +449,9 @@ describe('noticeChannelReducer', () => {
|
|
|
439
449
|
);
|
|
440
450
|
|
|
441
451
|
expect(out.effects).toEqual<EffectType[]>([
|
|
442
|
-
Effect.broadcast('#foo', [L(':alice!alice@example.com NOTICE #foo :psst')], 'c1'
|
|
452
|
+
Effect.broadcast('#foo', [L(':alice!alice@example.com NOTICE #foo :psst')], 'c1', 'msgid', [
|
|
453
|
+
L('@msgid=nonce-0 :alice!alice@example.com NOTICE #foo :psst'),
|
|
454
|
+
]),
|
|
443
455
|
]);
|
|
444
456
|
});
|
|
445
457
|
|
|
@@ -676,6 +688,145 @@ describe('noticeUserReducer', () => {
|
|
|
676
688
|
});
|
|
677
689
|
});
|
|
678
690
|
|
|
691
|
+
// ============================================================================
|
|
692
|
+
// privmsgChannelReducer — IRCv3 msgid tag (emit @msgid=<id> on the live
|
|
693
|
+
// fanout for cap-enabled peers; share the nonce with the chathistory record).
|
|
694
|
+
// ============================================================================
|
|
695
|
+
|
|
696
|
+
describe('privmsgChannelReducer — msgid tag', () => {
|
|
697
|
+
it('carries @msgid=<id> on the capLines delivered to msgid-cap peers', () => {
|
|
698
|
+
const chan = makeChan('#foo');
|
|
699
|
+
addMember(chan, 'c1', 'alice');
|
|
700
|
+
const conn = makeConn();
|
|
701
|
+
const ctx = makeCtx(conn);
|
|
702
|
+
|
|
703
|
+
const out = privmsgChannelReducer(
|
|
704
|
+
chan,
|
|
705
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
706
|
+
ctx,
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
const bcast = out.effects[0];
|
|
710
|
+
expect(bcast?.tag).toBe('Broadcast');
|
|
711
|
+
if (bcast?.tag === 'Broadcast') {
|
|
712
|
+
expect(bcast.cap).toBe('msgid');
|
|
713
|
+
expect(bcast.capLines?.[0]?.text).toBe(
|
|
714
|
+
'@msgid=nonce-0 :alice!alice@example.com PRIVMSG #foo :hi',
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('leaves the legacy lines bare so non-cap peers see no msgid tag', () => {
|
|
720
|
+
const chan = makeChan('#foo');
|
|
721
|
+
addMember(chan, 'c1', 'alice');
|
|
722
|
+
const conn = makeConn();
|
|
723
|
+
const ctx = makeCtx(conn);
|
|
724
|
+
|
|
725
|
+
const out = privmsgChannelReducer(
|
|
726
|
+
chan,
|
|
727
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
728
|
+
ctx,
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
const bcast = out.effects[0];
|
|
732
|
+
if (bcast?.tag === 'Broadcast') {
|
|
733
|
+
expect(bcast.lines[0]?.text).toBe(':alice!alice@example.com PRIVMSG #foo :hi');
|
|
734
|
+
expect(bcast.lines[0]?.text).not.toContain('msgid');
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('uses the same msgid on the live capLine and the chathistory record', () => {
|
|
739
|
+
const chan = makeChan('#foo');
|
|
740
|
+
addMember(chan, 'c1', 'alice');
|
|
741
|
+
const store = new InMemoryMessageStore();
|
|
742
|
+
const conn = makeConn();
|
|
743
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), store);
|
|
744
|
+
|
|
745
|
+
const out = privmsgChannelReducer(
|
|
746
|
+
chan,
|
|
747
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
748
|
+
ctx,
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
const recorded = store.query({ chan: '#foo', direction: 'latest', limit: 10 });
|
|
752
|
+
expect(recorded).toHaveLength(1);
|
|
753
|
+
const recordedMsgid = (recorded[0] as StoredMessage).msgid;
|
|
754
|
+
|
|
755
|
+
const bcast = out.effects[0];
|
|
756
|
+
if (bcast?.tag === 'Broadcast') {
|
|
757
|
+
const liveText = bcast.capLines?.[0]?.text ?? '';
|
|
758
|
+
expect(liveText).toContain(`@msgid=${recordedMsgid}`);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('still emits the msgid tag when chathistory is disabled (no MessageStore)', () => {
|
|
763
|
+
const chan = makeChan('#foo');
|
|
764
|
+
addMember(chan, 'c1', 'alice');
|
|
765
|
+
const conn = makeConn();
|
|
766
|
+
const ctx = makeCtx(conn); // no store
|
|
767
|
+
|
|
768
|
+
const out = privmsgChannelReducer(
|
|
769
|
+
chan,
|
|
770
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
771
|
+
ctx,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
const bcast = out.effects[0];
|
|
775
|
+
if (bcast?.tag === 'Broadcast') {
|
|
776
|
+
expect(bcast.cap).toBe('msgid');
|
|
777
|
+
expect(bcast.capLines?.[0]?.text).toContain('@msgid=');
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('echoes the msgid-decorated line to a sender with both echo-message and msgid', () => {
|
|
782
|
+
const chan = makeChan('#foo');
|
|
783
|
+
addMember(chan, 'c1', 'alice');
|
|
784
|
+
const conn = makeConn();
|
|
785
|
+
conn.caps.add('echo-message');
|
|
786
|
+
conn.caps.add('msgid');
|
|
787
|
+
const ctx = makeCtx(conn);
|
|
788
|
+
|
|
789
|
+
const out = privmsgChannelReducer(
|
|
790
|
+
chan,
|
|
791
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
792
|
+
ctx,
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
const echo = out.effects[1];
|
|
796
|
+
expect(echo?.tag).toBe('Send');
|
|
797
|
+
if (echo?.tag === 'Send') {
|
|
798
|
+
expect(echo.lines[0]?.text).toBe('@msgid=nonce-0 :alice!alice@example.com PRIVMSG #foo :hi');
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// ============================================================================
|
|
804
|
+
// noticeChannelReducer — IRCv3 msgid tag
|
|
805
|
+
// ============================================================================
|
|
806
|
+
|
|
807
|
+
describe('noticeChannelReducer — msgid tag', () => {
|
|
808
|
+
it('carries @msgid=<id> on the capLines delivered to msgid-cap peers', () => {
|
|
809
|
+
const chan = makeChan('#foo');
|
|
810
|
+
addMember(chan, 'c1', 'alice');
|
|
811
|
+
const conn = makeConn();
|
|
812
|
+
const ctx = makeCtx(conn);
|
|
813
|
+
|
|
814
|
+
const out = noticeChannelReducer(
|
|
815
|
+
chan,
|
|
816
|
+
{ command: 'NOTICE', params: ['#foo', 'note'], tags: {} },
|
|
817
|
+
ctx,
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
const bcast = out.effects[0];
|
|
821
|
+
if (bcast?.tag === 'Broadcast') {
|
|
822
|
+
expect(bcast.cap).toBe('msgid');
|
|
823
|
+
expect(bcast.capLines?.[0]?.text).toBe(
|
|
824
|
+
'@msgid=nonce-0 :alice!alice@example.com NOTICE #foo :note',
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
679
830
|
// ============================================================================
|
|
680
831
|
// chathistory recording — MessageStore.record is driven from the channel
|
|
681
832
|
// PRIVMSG/NOTICE reducer for every accepted message.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
READ_MARKER_CAP,
|
|
4
|
+
READ_MARKER_TAG,
|
|
5
|
+
isReadMarkerTagmsg,
|
|
6
|
+
persistReadMarker,
|
|
7
|
+
readMarkerMsgid,
|
|
8
|
+
seedReadMarkers,
|
|
9
|
+
} from '../../src/commands/read-marker';
|
|
10
|
+
import {
|
|
11
|
+
EmptyMotdProvider,
|
|
12
|
+
FakeClock,
|
|
13
|
+
InMemoryReadMarkerStore,
|
|
14
|
+
SequentialIdFactory,
|
|
15
|
+
} from '../../src/ports';
|
|
16
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
17
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
18
|
+
|
|
19
|
+
const serverConfig: ServerConfig = {
|
|
20
|
+
serverName: 'irc.example.com',
|
|
21
|
+
networkName: 'ExampleNet',
|
|
22
|
+
maxChannelsPerUser: 30,
|
|
23
|
+
maxTargetsPerCommand: 10,
|
|
24
|
+
maxListEntries: 50,
|
|
25
|
+
nickLen: 30,
|
|
26
|
+
channelLen: 50,
|
|
27
|
+
topicLen: 390,
|
|
28
|
+
quitMessage: 'Client Quit',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function makeConn(id = 'c1', account?: string): ConnectionState {
|
|
32
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
33
|
+
s.nick = 'alice';
|
|
34
|
+
s.user = 'alice';
|
|
35
|
+
s.host = 'example.com';
|
|
36
|
+
s.registration = 'registered';
|
|
37
|
+
if (account !== undefined) s.account = account;
|
|
38
|
+
return s;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeCtx(conn: ConnectionState, readMarkers?: InMemoryReadMarkerStore): Ctx {
|
|
42
|
+
return buildCtx({
|
|
43
|
+
serverConfig,
|
|
44
|
+
clock: new FakeClock(1_000),
|
|
45
|
+
ids: new SequentialIdFactory(),
|
|
46
|
+
motd: EmptyMotdProvider,
|
|
47
|
+
connection: conn,
|
|
48
|
+
...(readMarkers !== undefined ? { readMarkers } : {}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// constants
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
describe('read-marker constants', () => {
|
|
57
|
+
it('exposes the draft/read-marker cap name', () => {
|
|
58
|
+
expect(READ_MARKER_CAP).toBe('draft/read-marker');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('exposes the +draft/read-marker client-tag key', () => {
|
|
62
|
+
expect(READ_MARKER_TAG).toBe('+draft/read-marker');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// isReadMarkerTagmsg
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
describe('isReadMarkerTagmsg', () => {
|
|
71
|
+
it('returns true when the +draft/read-marker tag is present', () => {
|
|
72
|
+
expect(isReadMarkerTagmsg({ '+draft/read-marker': 'm1' })).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns true when the tag is valueless', () => {
|
|
76
|
+
expect(isReadMarkerTagmsg({ '+draft/read-marker': '' })).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns false when the tag is absent', () => {
|
|
80
|
+
expect(isReadMarkerTagmsg({ '+draft/typing': 'active' })).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns false for an empty tag bag', () => {
|
|
84
|
+
expect(isReadMarkerTagmsg({})).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// readMarkerMsgid
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
describe('readMarkerMsgid', () => {
|
|
93
|
+
it('returns the value of the +draft/read-marker tag', () => {
|
|
94
|
+
expect(readMarkerMsgid({ '+draft/read-marker': 'm42' })).toBe('m42');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns undefined when the tag is valueless', () => {
|
|
98
|
+
expect(readMarkerMsgid({ '+draft/read-marker': '' })).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns undefined when the tag is absent', () => {
|
|
102
|
+
expect(readMarkerMsgid({ '+draft/typing': 'active' })).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// persistReadMarker
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
describe('persistReadMarker', () => {
|
|
111
|
+
it('writes the marker to the store keyed by (account, channel)', () => {
|
|
112
|
+
const store = new InMemoryReadMarkerStore();
|
|
113
|
+
const ctx = makeCtx(makeConn('c1', 'alice'), store);
|
|
114
|
+
|
|
115
|
+
persistReadMarker(ctx, '#foo', 'm7');
|
|
116
|
+
|
|
117
|
+
expect(store.get('alice', '#foo')).toBe('m7');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('also advances the connection in-memory lastReadMarkers', () => {
|
|
121
|
+
const store = new InMemoryReadMarkerStore();
|
|
122
|
+
const conn = makeConn('c1', 'alice');
|
|
123
|
+
const ctx = makeCtx(conn, store);
|
|
124
|
+
|
|
125
|
+
persistReadMarker(ctx, '#foo', 'm7');
|
|
126
|
+
|
|
127
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m7');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('overwrites a previously stored marker for the same pair', () => {
|
|
131
|
+
const store = new InMemoryReadMarkerStore();
|
|
132
|
+
const ctx = makeCtx(makeConn('c1', 'alice'), store);
|
|
133
|
+
|
|
134
|
+
persistReadMarker(ctx, '#foo', 'm1');
|
|
135
|
+
persistReadMarker(ctx, '#foo', 'm2');
|
|
136
|
+
|
|
137
|
+
expect(store.get('alice', '#foo')).toBe('m2');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('is a no-op when no ReadMarkerStore is bound', () => {
|
|
141
|
+
const conn = makeConn('c1', 'alice');
|
|
142
|
+
const ctx = makeCtx(conn); // no store
|
|
143
|
+
|
|
144
|
+
persistReadMarker(ctx, '#foo', 'm1');
|
|
145
|
+
|
|
146
|
+
// In-memory marker still advances so the connection's own session tracks it.
|
|
147
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m1');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('is a no-op on the store when the connection has no account', () => {
|
|
151
|
+
const store = new InMemoryReadMarkerStore();
|
|
152
|
+
const conn = makeConn('c1'); // no account
|
|
153
|
+
const ctx = makeCtx(conn, store);
|
|
154
|
+
|
|
155
|
+
persistReadMarker(ctx, '#foo', 'm1');
|
|
156
|
+
|
|
157
|
+
expect(store.get('alice', '#foo')).toBeUndefined();
|
|
158
|
+
expect(store.forAccount('alice')).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// seedReadMarkers
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
describe('seedReadMarkers', () => {
|
|
167
|
+
it('populates lastReadMarkers from every persisted entry for the account', () => {
|
|
168
|
+
const store = new InMemoryReadMarkerStore();
|
|
169
|
+
store.set('alice', '#foo', 'm1');
|
|
170
|
+
store.set('alice', '#bar', 'm2');
|
|
171
|
+
const conn = makeConn();
|
|
172
|
+
|
|
173
|
+
seedReadMarkers(conn, store, 'alice');
|
|
174
|
+
|
|
175
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m1');
|
|
176
|
+
expect(conn.lastReadMarkers?.get('#bar')).toBe('m2');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('leaves existing in-memory markers untouched when the store is empty', () => {
|
|
180
|
+
const store = new InMemoryReadMarkerStore();
|
|
181
|
+
const conn = makeConn();
|
|
182
|
+
conn.lastReadMarkers = new Map<string, string>([['#foo', 'm9']]);
|
|
183
|
+
|
|
184
|
+
seedReadMarkers(conn, store, 'alice');
|
|
185
|
+
|
|
186
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m9');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('seeds markers for an account regardless of case spelling', () => {
|
|
190
|
+
const store = new InMemoryReadMarkerStore();
|
|
191
|
+
store.set('Alice', '#foo', 'm1');
|
|
192
|
+
const conn = makeConn();
|
|
193
|
+
|
|
194
|
+
seedReadMarkers(conn, store, 'alice');
|
|
195
|
+
|
|
196
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m1');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('does not overwrite an in-memory marker already present for a channel', () => {
|
|
200
|
+
const store = new InMemoryReadMarkerStore();
|
|
201
|
+
store.set('alice', '#foo', 'stored');
|
|
202
|
+
const conn = makeConn();
|
|
203
|
+
conn.lastReadMarkers = new Map<string, string>([['#foo', 'in-memory']]);
|
|
204
|
+
|
|
205
|
+
seedReadMarkers(conn, store, 'alice');
|
|
206
|
+
|
|
207
|
+
// The in-memory (session) marker wins: the connection may have already
|
|
208
|
+
// advanced past the persisted value this session.
|
|
209
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('in-memory');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -1102,3 +1102,67 @@ function expectedWelcomeWithHost(nick: string, hostmask: string): RawLine[] {
|
|
|
1102
1102
|
L(`${prefix} 422 ${nick} :MOTD File is missing`),
|
|
1103
1103
|
];
|
|
1104
1104
|
}
|
|
1105
|
+
|
|
1106
|
+
// ============================================================================
|
|
1107
|
+
// emitWelcomeIfReady — user mode `S` (TLS connected)
|
|
1108
|
+
// ============================================================================
|
|
1109
|
+
|
|
1110
|
+
describe('emitWelcomeIfReady — user mode `S` (TLS connected)', () => {
|
|
1111
|
+
it('sets user mode `S` (tls) at registration when the transport is secure', () => {
|
|
1112
|
+
const state = makeState();
|
|
1113
|
+
state.nick = 'alice';
|
|
1114
|
+
state.user = 'alice';
|
|
1115
|
+
state.host = 'example.com';
|
|
1116
|
+
state.realname = 'Alice';
|
|
1117
|
+
state.secure = true;
|
|
1118
|
+
const ctx = makeCtx(state);
|
|
1119
|
+
|
|
1120
|
+
emitWelcomeIfReady(state, ctx);
|
|
1121
|
+
|
|
1122
|
+
expect(state.registration).toBe('registered');
|
|
1123
|
+
expect(state.userModes.tls).toBe(true);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it('does not set user mode `S` when the transport is not secure', () => {
|
|
1127
|
+
const state = makeState();
|
|
1128
|
+
state.nick = 'alice';
|
|
1129
|
+
state.user = 'alice';
|
|
1130
|
+
state.host = 'example.com';
|
|
1131
|
+
state.realname = 'Alice';
|
|
1132
|
+
state.secure = false;
|
|
1133
|
+
const ctx = makeCtx(state);
|
|
1134
|
+
|
|
1135
|
+
emitWelcomeIfReady(state, ctx);
|
|
1136
|
+
|
|
1137
|
+
expect(state.registration).toBe('registered');
|
|
1138
|
+
expect(state.userModes.tls).toBe(false);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it('does not set user mode `S` when secure is absent (default plain transport)', () => {
|
|
1142
|
+
const state = makeState();
|
|
1143
|
+
state.nick = 'alice';
|
|
1144
|
+
state.user = 'alice';
|
|
1145
|
+
state.host = 'example.com';
|
|
1146
|
+
state.realname = 'Alice';
|
|
1147
|
+
const ctx = makeCtx(state);
|
|
1148
|
+
|
|
1149
|
+
emitWelcomeIfReady(state, ctx);
|
|
1150
|
+
|
|
1151
|
+
expect(state.registration).toBe('registered');
|
|
1152
|
+
expect(state.userModes.tls).toBe(false);
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it('completes registration over TLS via the NICK reducer end-to-end', () => {
|
|
1156
|
+
const state = makeState();
|
|
1157
|
+
state.user = 'alice';
|
|
1158
|
+
state.realname = 'Alice';
|
|
1159
|
+
state.registration = 'registering';
|
|
1160
|
+
state.secure = true;
|
|
1161
|
+
const ctx = makeCtx(state);
|
|
1162
|
+
|
|
1163
|
+
const out = nickReducer(state, { command: 'NICK', params: ['alice'], tags: {} }, ctx);
|
|
1164
|
+
|
|
1165
|
+
expect(out.state.registration).toBe('registered');
|
|
1166
|
+
expect(out.state.userModes.tls).toBe(true);
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildRehashTrailing, rehashLine, rehashReducer } from '../../src/commands/rehash';
|
|
3
|
+
import { Effect } from '../../src/effects';
|
|
4
|
+
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
5
|
+
import { EmptyMotdProvider, FakeClock, SequentialIdFactory } from '../../src/ports';
|
|
6
|
+
import type { IrcMessage } from '../../src/protocol/messages';
|
|
7
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
8
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
9
|
+
|
|
10
|
+
const baseServerConfig: ServerConfig = {
|
|
11
|
+
serverName: 'irc.example.com',
|
|
12
|
+
networkName: 'ExampleNet',
|
|
13
|
+
maxChannelsPerUser: 30,
|
|
14
|
+
maxTargetsPerCommand: 10,
|
|
15
|
+
maxListEntries: 50,
|
|
16
|
+
nickLen: 30,
|
|
17
|
+
channelLen: 50,
|
|
18
|
+
topicLen: 390,
|
|
19
|
+
quitMessage: 'Client Quit',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function makeCtx(state: ConnectionState, config: ServerConfig = baseServerConfig): Ctx {
|
|
23
|
+
return buildCtx({
|
|
24
|
+
serverConfig: config,
|
|
25
|
+
clock: new FakeClock(5_000),
|
|
26
|
+
ids: new SequentialIdFactory(),
|
|
27
|
+
motd: EmptyMotdProvider,
|
|
28
|
+
connection: state,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeState(oper = false): ConnectionState {
|
|
33
|
+
const s = createConnection({ id: 'c1', connectedSince: 0 });
|
|
34
|
+
s.nick = 'alice';
|
|
35
|
+
s.user = 'alice';
|
|
36
|
+
s.host = 'example.com';
|
|
37
|
+
s.realname = 'Alice';
|
|
38
|
+
s.registration = 'registered';
|
|
39
|
+
s.userModes.oper = oper;
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const L = (text: string): RawLine => ({ text });
|
|
44
|
+
|
|
45
|
+
const rehash = (): IrcMessage => ({ command: 'REHASH', params: [], tags: {} });
|
|
46
|
+
|
|
47
|
+
describe('rehashReducer — oper gating', () => {
|
|
48
|
+
it('emits 382 RPL_REHASHING and signals reload when the caller is oper', () => {
|
|
49
|
+
const state = makeState(true);
|
|
50
|
+
const ctx = makeCtx(state);
|
|
51
|
+
|
|
52
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
53
|
+
|
|
54
|
+
expect(out.shouldReload).toBe(true);
|
|
55
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
56
|
+
Effect.send('c1', [L(':irc.example.com 382 alice :Rehashing from KV')]),
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('emits 481 ERR_NOPRIVILEGES and does not signal reload for a non-oper', () => {
|
|
61
|
+
const state = makeState(false);
|
|
62
|
+
const ctx = makeCtx(state);
|
|
63
|
+
|
|
64
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
65
|
+
|
|
66
|
+
expect(out.shouldReload).toBe(false);
|
|
67
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
68
|
+
Effect.send('c1', [
|
|
69
|
+
L(":irc.example.com 481 alice :Permission Denied - You're not an IRC operator"),
|
|
70
|
+
]),
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts a lower-case command token', () => {
|
|
75
|
+
const state = makeState(true);
|
|
76
|
+
const ctx = makeCtx(state);
|
|
77
|
+
|
|
78
|
+
const out = rehashReducer(state, { command: 'rehash', params: [], tags: {} }, ctx, 'KV');
|
|
79
|
+
expect(out.shouldReload).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('rehashReducer — state handling', () => {
|
|
84
|
+
it('returns the same state reference (mutation permitted, no copy)', () => {
|
|
85
|
+
const state = makeState(true);
|
|
86
|
+
const ctx = makeCtx(state);
|
|
87
|
+
|
|
88
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
89
|
+
|
|
90
|
+
expect(out.state).toBe(state);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('updates connection lastSeen to ctx.clock.now()', () => {
|
|
94
|
+
const state = makeState(true);
|
|
95
|
+
expect(state.lastSeen).toBe(0);
|
|
96
|
+
const ctx = makeCtx(state);
|
|
97
|
+
|
|
98
|
+
rehashReducer(state, rehash(), ctx, 'KV');
|
|
99
|
+
|
|
100
|
+
expect(state.lastSeen).toBe(5_000);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('also updates lastSeen for the non-oper rejection path', () => {
|
|
104
|
+
const state = makeState(false);
|
|
105
|
+
const ctx = makeCtx(state);
|
|
106
|
+
|
|
107
|
+
rehashReducer(state, rehash(), ctx, 'KV');
|
|
108
|
+
|
|
109
|
+
expect(state.lastSeen).toBe(5_000);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('uses "*" as the nick in 382 when the connection has no nick', () => {
|
|
113
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
114
|
+
state.userModes.oper = true;
|
|
115
|
+
const ctx = makeCtx(state);
|
|
116
|
+
|
|
117
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
118
|
+
|
|
119
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
120
|
+
Effect.send('c1', [L(':irc.example.com 382 * :Rehashing from KV')]),
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('uses "*" as the nick in 481 when an unregistered non-oper has no nick', () => {
|
|
125
|
+
const state = createConnection({ id: 'c1', connectedSince: 0 });
|
|
126
|
+
const ctx = makeCtx(state);
|
|
127
|
+
|
|
128
|
+
const out = rehashReducer(state, rehash(), ctx, 'KV');
|
|
129
|
+
|
|
130
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
131
|
+
Effect.send('c1', [
|
|
132
|
+
L(":irc.example.com 481 * :Permission Denied - You're not an IRC operator"),
|
|
133
|
+
]),
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('reflects the configured source label in the 382 trailing text', () => {
|
|
138
|
+
const state = makeState(true);
|
|
139
|
+
const ctx = makeCtx(state);
|
|
140
|
+
|
|
141
|
+
const out = rehashReducer(state, rehash(), ctx, 'Secrets Manager');
|
|
142
|
+
|
|
143
|
+
expect((out.effects[0] as { lines: RawLine[] }).lines[0]?.text).toContain(
|
|
144
|
+
':Rehashing from Secrets Manager',
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('rehash formatting helpers', () => {
|
|
150
|
+
it('buildRehashTrailing composes the success phrase from the source label', () => {
|
|
151
|
+
expect(buildRehashTrailing('config file')).toBe('Rehashing from config file');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('buildRehashTrailing appends the error suffix when supplied', () => {
|
|
155
|
+
expect(buildRehashTrailing('KV', 'store unreachable')).toBe(
|
|
156
|
+
'Rehashing from KV failed: store unreachable',
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('rehashLine renders the full :server 382 nick :trailing wire line', () => {
|
|
161
|
+
expect(rehashLine('irc.example.com', 'alice', 'KV')).toEqual(
|
|
162
|
+
L(':irc.example.com 382 alice :Rehashing from KV'),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rehashLine renders the error-suffixed line for the failure path', () => {
|
|
167
|
+
expect(rehashLine('irc.example.com', 'alice', 'KV', 'boom')).toEqual(
|
|
168
|
+
L(':irc.example.com 382 alice :Rehashing from KV failed: boom'),
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|