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
|
@@ -18,6 +18,9 @@ function fakeRuntime(calls: RecordedCall[]): IrcRuntime {
|
|
|
18
18
|
async broadcast(chan, lines, except) {
|
|
19
19
|
calls.push({ method: 'broadcast', args: [chan, lines, except] });
|
|
20
20
|
},
|
|
21
|
+
async broadcastWallops(lines, except) {
|
|
22
|
+
calls.push({ method: 'broadcastWallops', args: [lines, except] });
|
|
23
|
+
},
|
|
21
24
|
async disconnect(conn, reason) {
|
|
22
25
|
calls.push({ method: 'disconnect', args: [conn, reason] });
|
|
23
26
|
},
|
|
@@ -60,6 +63,10 @@ function fakeRuntime(calls: RecordedCall[]): IrcRuntime {
|
|
|
60
63
|
async listChannels() {
|
|
61
64
|
return [];
|
|
62
65
|
},
|
|
66
|
+
async reloadConfig() {
|
|
67
|
+
calls.push({ method: 'reloadConfig', args: [] });
|
|
68
|
+
throw new Error('reloadConfig not configured for this fake');
|
|
69
|
+
},
|
|
63
70
|
};
|
|
64
71
|
}
|
|
65
72
|
|
|
@@ -126,6 +133,24 @@ describe('dispatch', () => {
|
|
|
126
133
|
});
|
|
127
134
|
});
|
|
128
135
|
|
|
136
|
+
it('routes BroadcastWallops with lines and except', async () => {
|
|
137
|
+
const calls: RecordedCall[] = [];
|
|
138
|
+
await dispatch([Effect.broadcastWallops([L(':a WALLOPS :hi')], 'c1')], fakeRuntime(calls));
|
|
139
|
+
expect(calls[0]).toEqual({
|
|
140
|
+
method: 'broadcastWallops',
|
|
141
|
+
args: [[{ text: ':a WALLOPS :hi' }], 'c1'],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('routes BroadcastWallops preserving undefined except', async () => {
|
|
146
|
+
const calls: RecordedCall[] = [];
|
|
147
|
+
await dispatch([Effect.broadcastWallops([L(':a WALLOPS :hi')])], fakeRuntime(calls));
|
|
148
|
+
expect(calls[0]).toEqual({
|
|
149
|
+
method: 'broadcastWallops',
|
|
150
|
+
args: [[{ text: ':a WALLOPS :hi' }], undefined],
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
129
154
|
it('calls reserveNick, changeNick, releaseNick in order', async () => {
|
|
130
155
|
const calls: RecordedCall[] = [];
|
|
131
156
|
await dispatch(
|
|
@@ -247,6 +272,7 @@ function capAwareRuntime(members: ReadonlyMap<ConnId, MemberInfo>): {
|
|
|
247
272
|
async broadcast(chan, lines, except) {
|
|
248
273
|
broadcasts.push({ chan, lines, except });
|
|
249
274
|
},
|
|
275
|
+
async broadcastWallops() {},
|
|
250
276
|
async disconnect() {},
|
|
251
277
|
async sendToNick() {},
|
|
252
278
|
async reserveNick() {
|
|
@@ -268,7 +294,13 @@ function capAwareRuntime(members: ReadonlyMap<ConnId, MemberInfo>): {
|
|
|
268
294
|
registration: 'registered',
|
|
269
295
|
caps: info.caps,
|
|
270
296
|
joinedChannels: new Set<string>(),
|
|
271
|
-
userModes: {
|
|
297
|
+
userModes: {
|
|
298
|
+
invisible: false,
|
|
299
|
+
oper: false,
|
|
300
|
+
wallops: false,
|
|
301
|
+
serverNotices: false,
|
|
302
|
+
tls: false,
|
|
303
|
+
},
|
|
272
304
|
lastSeen: 0,
|
|
273
305
|
connectedSince: 0,
|
|
274
306
|
};
|
|
@@ -304,6 +336,9 @@ function capAwareRuntime(members: ReadonlyMap<ConnId, MemberInfo>): {
|
|
|
304
336
|
async listChannels() {
|
|
305
337
|
return [];
|
|
306
338
|
},
|
|
339
|
+
async reloadConfig() {
|
|
340
|
+
throw new Error('reloadConfig not configured for this fake');
|
|
341
|
+
},
|
|
307
342
|
};
|
|
308
343
|
return { runtime, sends, broadcasts };
|
|
309
344
|
}
|
|
@@ -382,6 +417,7 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
382
417
|
sends.push({ to, lines });
|
|
383
418
|
},
|
|
384
419
|
async broadcast() {},
|
|
420
|
+
async broadcastWallops() {},
|
|
385
421
|
async disconnect() {},
|
|
386
422
|
async sendToNick() {},
|
|
387
423
|
async reserveNick() {
|
|
@@ -410,6 +446,9 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
410
446
|
async listChannels() {
|
|
411
447
|
return [];
|
|
412
448
|
},
|
|
449
|
+
async reloadConfig() {
|
|
450
|
+
throw new Error('reloadConfig not configured for this fake');
|
|
451
|
+
},
|
|
413
452
|
};
|
|
414
453
|
|
|
415
454
|
await dispatch([Effect.broadcast('#nope', [L('x')], undefined, 'chghost')], runtime);
|
|
@@ -425,6 +464,7 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
425
464
|
sends.push({ to, lines });
|
|
426
465
|
},
|
|
427
466
|
async broadcast() {},
|
|
467
|
+
async broadcastWallops() {},
|
|
428
468
|
async disconnect() {},
|
|
429
469
|
async sendToNick() {},
|
|
430
470
|
async reserveNick() {
|
|
@@ -445,7 +485,13 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
445
485
|
registration: 'registered',
|
|
446
486
|
caps: new Set(['chghost']),
|
|
447
487
|
joinedChannels: new Set<string>(),
|
|
448
|
-
userModes: {
|
|
488
|
+
userModes: {
|
|
489
|
+
invisible: false,
|
|
490
|
+
oper: false,
|
|
491
|
+
wallops: false,
|
|
492
|
+
serverNotices: false,
|
|
493
|
+
tls: false,
|
|
494
|
+
},
|
|
449
495
|
lastSeen: 0,
|
|
450
496
|
connectedSince: 0,
|
|
451
497
|
};
|
|
@@ -483,6 +529,9 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
483
529
|
async listChannels() {
|
|
484
530
|
return [];
|
|
485
531
|
},
|
|
532
|
+
async reloadConfig() {
|
|
533
|
+
throw new Error('reloadConfig not configured for this fake');
|
|
534
|
+
},
|
|
486
535
|
};
|
|
487
536
|
|
|
488
537
|
await dispatch(
|
|
@@ -495,6 +544,255 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
495
544
|
});
|
|
496
545
|
});
|
|
497
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
|
+
|
|
498
796
|
// ===========================================================================
|
|
499
797
|
// dispatch observability
|
|
500
798
|
// ===========================================================================
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient declaration so `?raw` imports (Vite's built-in transform that
|
|
3
|
+
* inlines a file's text at bundle time) typecheck under the strict
|
|
4
|
+
* `tsconfig.test.json`. Used by source-level regression guards that
|
|
5
|
+
* assert the actor default-branch triage stays accurate.
|
|
6
|
+
*/
|
|
7
|
+
declare module '*?raw' {
|
|
8
|
+
const content: string;
|
|
9
|
+
// eslint-disable-next-line import/no-default-export
|
|
10
|
+
export default content;
|
|
11
|
+
}
|
|
@@ -34,6 +34,10 @@ describe('IrcRuntime contract', () => {
|
|
|
34
34
|
async listChannels() {
|
|
35
35
|
return [];
|
|
36
36
|
},
|
|
37
|
+
async reloadConfig() {
|
|
38
|
+
throw new Error('reloadConfig not configured for this fake');
|
|
39
|
+
},
|
|
40
|
+
async broadcastWallops() {},
|
|
37
41
|
};
|
|
38
42
|
expect(runtime).toBeDefined();
|
|
39
43
|
expect(typeof runtime.send).toBe('function');
|
|
@@ -48,5 +52,8 @@ describe('IrcRuntime contract', () => {
|
|
|
48
52
|
expect(typeof runtime.getConnectionInfo).toBe('function');
|
|
49
53
|
expect(typeof runtime.getConnection).toBe('function');
|
|
50
54
|
expect(typeof runtime.getChannelSnapshot).toBe('function');
|
|
55
|
+
expect(typeof runtime.listChannels).toBe('function');
|
|
56
|
+
expect(typeof runtime.reloadConfig).toBe('function');
|
|
57
|
+
expect(typeof runtime.broadcastWallops).toBe('function');
|
|
51
58
|
});
|
|
52
59
|
});
|
|
@@ -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';
|
|
@@ -192,7 +192,15 @@ export function runIrcScenarios(factories: readonly IrcHarnessFactory[]): void {
|
|
|
192
192
|
await alice.waitForLine((l) => l.includes('JOIN #bar'));
|
|
193
193
|
alice.received.length = 0;
|
|
194
194
|
await alice.send('JOIN 0');
|
|
195
|
-
|
|
195
|
+
// The two PART echoes arrive as separate deliveries (one per
|
|
196
|
+
// channel, with an ApplyChannelDelta in between). On distributed
|
|
197
|
+
// runtimes (cf-adapter) they land in distinct WS messages with
|
|
198
|
+
// cross-DO latency between them, so waiting for the first generic
|
|
199
|
+
// PART and then snapshotting `received` races the second one.
|
|
200
|
+
// Wait for each channel's PART explicitly to make the observation
|
|
201
|
+
// deterministic across in-memory and DO-backed runtimes.
|
|
202
|
+
await alice.waitForLine((l) => l.includes('PART') && l.includes('#foo'));
|
|
203
|
+
await alice.waitForLine((l) => l.includes('PART') && l.includes('#bar'));
|
|
196
204
|
const parts = alice.received.filter((l) => l.includes('PART'));
|
|
197
205
|
expect(parts.length).toBe(2);
|
|
198
206
|
expect(parts.some((l) => l.includes('#foo'))).toBe(true);
|
|
@@ -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
|
+
});
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -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
|
+
});
|