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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* `RegistryDO` with sharding plan.
|
|
3
3
|
*
|
|
4
|
-
* TDD outline
|
|
4
|
+
* TDD outline:
|
|
5
5
|
* "Red on concurrent reserve from two stub ConnectionDOs; green."
|
|
6
6
|
*
|
|
7
7
|
* Tests run inside real `workerd` via miniflare so DO storage
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* The binding `REGISTRY_DO_REAL` (declared in `wrangler.test.toml`)
|
|
13
13
|
* points at the real {@link RegistryDO} class. Tests address a
|
|
14
14
|
* specific shard by name (the same `registryKeyForNick` result the
|
|
15
|
-
* CfRuntime will compute
|
|
15
|
+
* CfRuntime will compute), so a single DO instance
|
|
16
16
|
* stands in for one shard of the production fleet.
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -34,7 +34,7 @@ declare global {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Returns a stub for the registry shard that owns `nick` under the
|
|
37
|
-
* default shard count. Mirrors what CfRuntime will do
|
|
37
|
+
* default shard count. Mirrors what CfRuntime will do.
|
|
38
38
|
*/
|
|
39
39
|
function shardForNick(nick: string, shards = DEFAULT_REGISTRY_SHARDS): DurableObjectStub {
|
|
40
40
|
const key = registryKeyForNick(nick, shards);
|
|
@@ -53,6 +53,7 @@ interface RegistryRpc {
|
|
|
53
53
|
changeNick(conn: string, oldNick: string, newNick: string): Promise<boolean>;
|
|
54
54
|
releaseNick(nick: string): Promise<void>;
|
|
55
55
|
lookupNick(nick: string): Promise<string | null>;
|
|
56
|
+
listEntries(): Promise<Array<[nickLower: string, connId: string]>>;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
function rpc(stub: DurableObjectStub): RegistryRpc {
|
|
@@ -333,3 +334,106 @@ describe('RegistryDO — shard isolation', () => {
|
|
|
333
334
|
expect(await rpc(i).lookupNick('in-shard-31')).toBeNull();
|
|
334
335
|
});
|
|
335
336
|
});
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// changeNick — direct contract tests (sharding is a CfRuntime concern; the
|
|
340
|
+
// DO method itself operates on whatever keys it is given, so we address a
|
|
341
|
+
// single DO instance directly to exercise every branch without depending on
|
|
342
|
+
// the FNV-1a hash landing two nicks in the same shard).
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
describe('RegistryDO — changeNick branch coverage (direct on one shard)', () => {
|
|
346
|
+
// Pin every test to a single, stable shard instance. The DO method is
|
|
347
|
+
// agnostic to which shard it lives on; what matters is the storage
|
|
348
|
+
// state we put in place before the call.
|
|
349
|
+
const shard = () => shardByIndex(1);
|
|
350
|
+
|
|
351
|
+
it('returns false when the new nick is held by a different conn', async () => {
|
|
352
|
+
const s = shard();
|
|
353
|
+
// Seed storage so both `taken` and `current` are owned by different
|
|
354
|
+
// conns on this same DO.
|
|
355
|
+
expect(await rpc(s).reserveNick('taken-new', 'conn-owner')).toEqual({ ok: true });
|
|
356
|
+
expect(await rpc(s).reserveNick('current-old', 'conn-changer')).toEqual({ ok: true });
|
|
357
|
+
|
|
358
|
+
const ok = await rpc(s).changeNick('conn-changer', 'current-old', 'taken-new');
|
|
359
|
+
expect(ok).toBe(false);
|
|
360
|
+
// State untouched: owners stay as they were.
|
|
361
|
+
expect(await rpc(s).lookupNick('taken-new')).toBe('conn-owner');
|
|
362
|
+
expect(await rpc(s).lookupNick('current-old')).toBe('conn-changer');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('clears the old nick when it still points at the caller', async () => {
|
|
366
|
+
const s = shard();
|
|
367
|
+
expect(await rpc(s).reserveNick('cn-clear-old', 'conn-self')).toEqual({ ok: true });
|
|
368
|
+
|
|
369
|
+
const ok = await rpc(s).changeNick('conn-self', 'cn-clear-old', 'cn-clear-new');
|
|
370
|
+
expect(ok).toBe(true);
|
|
371
|
+
expect(await rpc(s).lookupNick('cn-clear-old')).toBeNull();
|
|
372
|
+
expect(await rpc(s).lookupNick('cn-clear-new')).toBe('conn-self');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('leaves a stale old nick alone when it no longer points at the caller', async () => {
|
|
376
|
+
// Crash-recovery / race defence: another conn grabbed `old` between
|
|
377
|
+
// our reserve and our changeNick. The conditional delete must not
|
|
378
|
+
// clobber that third party.
|
|
379
|
+
const s = shard();
|
|
380
|
+
// `old` is currently held by `conn-other`, not by `conn-caller`.
|
|
381
|
+
expect(await rpc(s).reserveNick('cn-stale-old', 'conn-other')).toEqual({ ok: true });
|
|
382
|
+
|
|
383
|
+
const ok = await rpc(s).changeNick('conn-caller', 'cn-stale-old', 'cn-stale-new');
|
|
384
|
+
expect(ok).toBe(true);
|
|
385
|
+
// New nick reserved for the caller...
|
|
386
|
+
expect(await rpc(s).lookupNick('cn-stale-new')).toBe('conn-caller');
|
|
387
|
+
// ...but `old` is untouched because it never pointed at the caller.
|
|
388
|
+
expect(await rpc(s).lookupNick('cn-stale-old')).toBe('conn-other');
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// listEntries — backs the global WALLOPS fan-out (CfRuntime.broadcastWallops
|
|
394
|
+
// walks every shard via this method).
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
describe('RegistryDO — listEntries', () => {
|
|
398
|
+
it('returns an empty array for a fresh shard', async () => {
|
|
399
|
+
expect(await rpc(shardByIndex(2)).listEntries()).toEqual([]);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('enumerates every (nickLower, connId) pair in this shard', async () => {
|
|
403
|
+
const s = shardByIndex(3);
|
|
404
|
+
await rpc(s).reserveNick('Alice', 'conn-a');
|
|
405
|
+
await rpc(s).reserveNick('Bob', 'conn-b');
|
|
406
|
+
await rpc(s).reserveNick('Carol', 'conn-c');
|
|
407
|
+
|
|
408
|
+
const entries = await rpc(s).listEntries();
|
|
409
|
+
// Keys are lowercased in storage; the strip recovers the lowercased
|
|
410
|
+
// nick (matches what CfRuntime expects when deduping conn ids).
|
|
411
|
+
const sorted = [...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
412
|
+
expect(sorted).toEqual([
|
|
413
|
+
['alice', 'conn-a'],
|
|
414
|
+
['bob', 'conn-b'],
|
|
415
|
+
['carol', 'conn-c'],
|
|
416
|
+
]);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('reflects releases: a freed nick drops out of the listing', async () => {
|
|
420
|
+
const s = shardByIndex(4);
|
|
421
|
+
await rpc(s).reserveNick('keep', 'conn-k');
|
|
422
|
+
await rpc(s).reserveNick('drop', 'conn-d');
|
|
423
|
+
await rpc(s).releaseNick('drop');
|
|
424
|
+
|
|
425
|
+
const entries = await rpc(s).listEntries();
|
|
426
|
+
expect(entries).toEqual([['keep', 'conn-k']]);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('strips the storage prefix so callers see a bare nickLower', async () => {
|
|
430
|
+
const s = shardByIndex(5);
|
|
431
|
+
await rpc(s).reserveNick('prefixed', 'conn-p');
|
|
432
|
+
const entries = await rpc(s).listEntries();
|
|
433
|
+
const nickLower = entries[0]?.[0];
|
|
434
|
+
// The internal key is `nick:prefixed`; the public API must not leak
|
|
435
|
+
// that implementation detail.
|
|
436
|
+
expect(nickLower).toBe('prefixed');
|
|
437
|
+
expect(nickLower?.startsWith('nick:')).toBe(false);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `serialize` / `deserialize` / `migrate`.
|
|
3
|
+
*
|
|
4
|
+
* These helpers bridge {@link ConnectionState} (which carries `Set` and
|
|
5
|
+
* `Map` fields that degrade under `JSON.stringify`) and the JSON-safe
|
|
6
|
+
* {@link PersistedConnectionState} shape stored in DO transactional KV.
|
|
7
|
+
*
|
|
8
|
+
* Coverage focus: every optional-field branch in both directions, the
|
|
9
|
+
* `secure` / `userModes.tls` backfill paths in `migrate`, and the
|
|
10
|
+
* rejection paths (non-object record, too-new version).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type ConnectionState, createConnection } from '@serverless-ircd/irc-core';
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
import { PERSISTED_STATE_VERSION, deserialize, migrate, serialize } from '../src/serialize';
|
|
16
|
+
|
|
17
|
+
/** Builds a fully-populated connection state for round-trip assertions. */
|
|
18
|
+
function fullState(): ConnectionState {
|
|
19
|
+
const s = createConnection({ id: 'conn-1', connectedSince: 100 });
|
|
20
|
+
s.registration = 'registered';
|
|
21
|
+
s.capNegotiating = false;
|
|
22
|
+
s.secure = true;
|
|
23
|
+
s.nick = 'Alice';
|
|
24
|
+
s.user = 'alice';
|
|
25
|
+
s.host = 'example.com';
|
|
26
|
+
s.realname = 'Alice Example';
|
|
27
|
+
s.passAttempt = 'hunter2';
|
|
28
|
+
s.account = 'alice-account';
|
|
29
|
+
s.away = 'brb';
|
|
30
|
+
s.caps = new Set(['server-time', 'message-tags']);
|
|
31
|
+
s.joinedChannels = new Set(['#one', '#two']);
|
|
32
|
+
s.userModes = {
|
|
33
|
+
invisible: true,
|
|
34
|
+
oper: false,
|
|
35
|
+
wallops: true,
|
|
36
|
+
serverNotices: false,
|
|
37
|
+
tls: true,
|
|
38
|
+
};
|
|
39
|
+
s.lastSeen = 200;
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('serialize — round trip', () => {
|
|
44
|
+
it('survives serialize → deserialize for a fully-populated state', () => {
|
|
45
|
+
const original = fullState();
|
|
46
|
+
const round = deserialize(serialize(original));
|
|
47
|
+
// Sets are rebuilt with the same members.
|
|
48
|
+
expect(round.caps).toEqual(new Set(['server-time', 'message-tags']));
|
|
49
|
+
expect(round.joinedChannels).toEqual(new Set(['#one', '#two']));
|
|
50
|
+
// Optional fields preserved.
|
|
51
|
+
expect(round.nick).toBe('Alice');
|
|
52
|
+
expect(round.user).toBe('alice');
|
|
53
|
+
expect(round.host).toBe('example.com');
|
|
54
|
+
expect(round.realname).toBe('Alice Example');
|
|
55
|
+
expect(round.passAttempt).toBe('hunter2');
|
|
56
|
+
expect(round.account).toBe('alice-account');
|
|
57
|
+
expect(round.away).toBe('brb');
|
|
58
|
+
// userModes cloned (mutating the round trip must not leak back).
|
|
59
|
+
expect(round.userModes).toEqual(original.userModes);
|
|
60
|
+
expect(round.userModes).not.toBe(original.userModes);
|
|
61
|
+
expect(round.secure).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('is JSON-stable: serialize → JSON.stringify → JSON.parse → deserialize', () => {
|
|
65
|
+
// This is the actual production path through DO storage: the runtime
|
|
66
|
+
// structured-clones the value, but the persisted shape must also be
|
|
67
|
+
// JSON-tolerant for older storage rows and snapshots written to R2.
|
|
68
|
+
const original = fullState();
|
|
69
|
+
const json = JSON.stringify(serialize(original));
|
|
70
|
+
const round = deserialize(JSON.parse(json));
|
|
71
|
+
expect(round.caps).toEqual(original.caps);
|
|
72
|
+
expect(round.nick).toBe(original.nick);
|
|
73
|
+
expect(round.away).toBe(original.away);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('stamps the current persisted schema version', () => {
|
|
77
|
+
expect(serialize(fullState()).version).toBe(PERSISTED_STATE_VERSION);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('serialize — optional-field branches', () => {
|
|
82
|
+
it('omits host when undefined', () => {
|
|
83
|
+
const s = createConnection({ id: 'c', connectedSince: 0 });
|
|
84
|
+
s.nick = 'n';
|
|
85
|
+
expect(serialize(s).host).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('omits passAttempt when undefined', () => {
|
|
89
|
+
const s = createConnection({ id: 'c', connectedSince: 0 });
|
|
90
|
+
expect(serialize(s).passAttempt).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('omits away when undefined', () => {
|
|
94
|
+
const s = createConnection({ id: 'c', connectedSince: 0 });
|
|
95
|
+
expect(serialize(s).away).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('clones caps and joinedChannels arrays away from the live Set', () => {
|
|
99
|
+
const s = createConnection({ id: 'c', connectedSince: 0 });
|
|
100
|
+
s.caps = new Set(['a']);
|
|
101
|
+
s.joinedChannels = new Set(['#x']);
|
|
102
|
+
const out = serialize(s);
|
|
103
|
+
expect(out.caps).toEqual(['a']);
|
|
104
|
+
expect(out.joinedChannels).toEqual(['#x']);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('deserialize — optional-field branches', () => {
|
|
109
|
+
it('populates host when the persisted record carries it', () => {
|
|
110
|
+
const s = createConnection({ id: 'c', connectedSince: 0 });
|
|
111
|
+
s.host = 'host.example';
|
|
112
|
+
expect(deserialize(serialize(s)).host).toBe('host.example');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('populates passAttempt when the persisted record carries it', () => {
|
|
116
|
+
const s = createConnection({ id: 'c', connectedSince: 0 });
|
|
117
|
+
s.passAttempt = 'pw';
|
|
118
|
+
expect(deserialize(serialize(s)).passAttempt).toBe('pw');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('populates account when the persisted record carries it', () => {
|
|
122
|
+
const s = createConnection({ id: 'c', connectedSince: 0 });
|
|
123
|
+
s.account = 'acc';
|
|
124
|
+
expect(deserialize(serialize(s)).account).toBe('acc');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('populates away when the persisted record carries it', () => {
|
|
128
|
+
const s = createConnection({ id: 'c', connectedSince: 0 });
|
|
129
|
+
s.away = 'afk';
|
|
130
|
+
expect(deserialize(serialize(s)).away).toBe('afk');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('leaves optional fields absent on a minimal pre-registration state', () => {
|
|
134
|
+
const s = createConnection({ id: 'c', connectedSince: 0 });
|
|
135
|
+
const round = deserialize(serialize(s));
|
|
136
|
+
expect(round.nick).toBeUndefined();
|
|
137
|
+
expect(round.host).toBeUndefined();
|
|
138
|
+
expect(round.away).toBeUndefined();
|
|
139
|
+
expect(round.account).toBeUndefined();
|
|
140
|
+
expect(round.passAttempt).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('migrate — backfills additive fields', () => {
|
|
145
|
+
it('defaults `secure` to false when the field is absent', () => {
|
|
146
|
+
// Pre-feature record: no `secure` key at all.
|
|
147
|
+
const legacy = {
|
|
148
|
+
version: 1,
|
|
149
|
+
id: 'c',
|
|
150
|
+
registration: 'registered' as const,
|
|
151
|
+
capNegotiating: false,
|
|
152
|
+
caps: [],
|
|
153
|
+
joinedChannels: [],
|
|
154
|
+
userModes: {
|
|
155
|
+
invisible: false,
|
|
156
|
+
oper: false,
|
|
157
|
+
wallops: false,
|
|
158
|
+
serverNotices: false,
|
|
159
|
+
},
|
|
160
|
+
lastSeen: 0,
|
|
161
|
+
connectedSince: 0,
|
|
162
|
+
};
|
|
163
|
+
const migrated = migrate(legacy);
|
|
164
|
+
expect(migrated.secure).toBe(false);
|
|
165
|
+
// The mutation landed on the original object reference (defensive
|
|
166
|
+
// backfill writes through to the record).
|
|
167
|
+
expect((legacy as Record<string, unknown>).secure).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('defaults `userModes.tls` to false when the field is absent', () => {
|
|
171
|
+
const legacy = {
|
|
172
|
+
version: 1,
|
|
173
|
+
id: 'c',
|
|
174
|
+
registration: 'registered' as const,
|
|
175
|
+
capNegotiating: false,
|
|
176
|
+
caps: [],
|
|
177
|
+
joinedChannels: [],
|
|
178
|
+
userModes: {
|
|
179
|
+
invisible: false,
|
|
180
|
+
oper: false,
|
|
181
|
+
wallops: false,
|
|
182
|
+
serverNotices: false,
|
|
183
|
+
},
|
|
184
|
+
lastSeen: 0,
|
|
185
|
+
connectedSince: 0,
|
|
186
|
+
};
|
|
187
|
+
const migrated = migrate(legacy);
|
|
188
|
+
expect(migrated.userModes.tls).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('leaves an explicit `secure: true` untouched', () => {
|
|
192
|
+
const rec = {
|
|
193
|
+
version: 1,
|
|
194
|
+
id: 'c',
|
|
195
|
+
registration: 'registered' as const,
|
|
196
|
+
capNegotiating: false,
|
|
197
|
+
secure: true,
|
|
198
|
+
caps: [],
|
|
199
|
+
joinedChannels: [],
|
|
200
|
+
userModes: {
|
|
201
|
+
invisible: false,
|
|
202
|
+
oper: false,
|
|
203
|
+
wallops: false,
|
|
204
|
+
serverNotices: false,
|
|
205
|
+
tls: true,
|
|
206
|
+
},
|
|
207
|
+
lastSeen: 0,
|
|
208
|
+
connectedSince: 0,
|
|
209
|
+
};
|
|
210
|
+
expect(migrate(rec).secure).toBe(true);
|
|
211
|
+
expect(migrate(rec).userModes.tls).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('tolerates a missing `userModes` object without throwing', () => {
|
|
215
|
+
// A record corrupted in storage (or hand-built by an older code
|
|
216
|
+
// path) might not carry userModes at all. Migrate must not crash.
|
|
217
|
+
const rec = {
|
|
218
|
+
version: 1,
|
|
219
|
+
id: 'c',
|
|
220
|
+
registration: 'registered' as const,
|
|
221
|
+
capNegotiating: false,
|
|
222
|
+
caps: [],
|
|
223
|
+
joinedChannels: [],
|
|
224
|
+
lastSeen: 0,
|
|
225
|
+
connectedSince: 0,
|
|
226
|
+
};
|
|
227
|
+
expect(() => migrate(rec)).not.toThrow();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('migrate — rejection paths', () => {
|
|
232
|
+
it('rejects a non-object record', () => {
|
|
233
|
+
for (const bad of [null, 'string', 42, true, undefined]) {
|
|
234
|
+
expect(() => migrate(bad)).toThrow(/not an object/u);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('rejects an array record (arrays are not plain objects)', () => {
|
|
239
|
+
expect(() => migrate([])).toThrow(/not an object/u);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('rejects a record persisted with a newer schema version', () => {
|
|
243
|
+
const future = {
|
|
244
|
+
version: PERSISTED_STATE_VERSION + 1,
|
|
245
|
+
id: 'c',
|
|
246
|
+
registration: 'registered' as const,
|
|
247
|
+
capNegotiating: false,
|
|
248
|
+
caps: [],
|
|
249
|
+
joinedChannels: [],
|
|
250
|
+
userModes: {
|
|
251
|
+
invisible: false,
|
|
252
|
+
oper: false,
|
|
253
|
+
wallops: false,
|
|
254
|
+
serverNotices: false,
|
|
255
|
+
tls: false,
|
|
256
|
+
},
|
|
257
|
+
lastSeen: 0,
|
|
258
|
+
connectedSince: 0,
|
|
259
|
+
};
|
|
260
|
+
expect(() => migrate(future)).toThrow(/newer than/u);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('treats a missing version as the legacy default (0)', () => {
|
|
264
|
+
// No `version` field — old shape. Migrate must accept it rather
|
|
265
|
+
// than fail the boot path; the backfill covers the additive keys.
|
|
266
|
+
const legacy = {
|
|
267
|
+
id: 'c',
|
|
268
|
+
registration: 'registered' as const,
|
|
269
|
+
capNegotiating: false,
|
|
270
|
+
caps: [],
|
|
271
|
+
joinedChannels: [],
|
|
272
|
+
userModes: {
|
|
273
|
+
invisible: false,
|
|
274
|
+
oper: false,
|
|
275
|
+
wallops: false,
|
|
276
|
+
serverNotices: false,
|
|
277
|
+
},
|
|
278
|
+
lastSeen: 0,
|
|
279
|
+
connectedSince: 0,
|
|
280
|
+
};
|
|
281
|
+
expect(() => migrate(legacy)).not.toThrow();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('deserialize — migrate integration', () => {
|
|
286
|
+
it('backfills `secure` and `userModes.tls` on a legacy record, then materializes', () => {
|
|
287
|
+
const legacy = {
|
|
288
|
+
id: 'c',
|
|
289
|
+
registration: 'registered' as const,
|
|
290
|
+
capNegotiating: false,
|
|
291
|
+
caps: ['server-time'],
|
|
292
|
+
joinedChannels: ['#x'],
|
|
293
|
+
userModes: {
|
|
294
|
+
invisible: false,
|
|
295
|
+
oper: false,
|
|
296
|
+
wallops: true,
|
|
297
|
+
serverNotices: false,
|
|
298
|
+
},
|
|
299
|
+
lastSeen: 5,
|
|
300
|
+
connectedSince: 1,
|
|
301
|
+
};
|
|
302
|
+
const state = deserialize(legacy);
|
|
303
|
+
expect(state.secure).toBe(false);
|
|
304
|
+
expect(state.userModes.tls).toBe(false);
|
|
305
|
+
expect(state.userModes.wallops).toBe(true);
|
|
306
|
+
expect(state.caps).toEqual(new Set(['server-time']));
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type ConnId, type ConnectionState, createConnection } from '@serverless-ircd/irc-core';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { CfStats } from '../src/stats.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal stub satisfying the structural interface CfStats needs (just
|
|
7
|
+
* `listChannels` + `getChannelConnections`). Keeps the unit test free of
|
|
8
|
+
* the workerd/miniflare harness the full CfRuntime integration suite uses.
|
|
9
|
+
*/
|
|
10
|
+
interface StatsRuntimeShape {
|
|
11
|
+
listChannels(): Promise<{ nameLower: string }[]>;
|
|
12
|
+
getChannelConnections(name: string): Promise<ReadonlyMap<ConnId, ConnectionState>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class StubRuntime implements StatsRuntimeShape {
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly channels: ReadonlyMap<string, ReadonlyMap<ConnId, ConnectionState>>,
|
|
18
|
+
) {}
|
|
19
|
+
async listChannels(): Promise<{ nameLower: string }[]> {
|
|
20
|
+
return Array.from(this.channels.keys(), (nameLower) => ({ nameLower }));
|
|
21
|
+
}
|
|
22
|
+
async getChannelConnections(name: string): Promise<ReadonlyMap<ConnId, ConnectionState>> {
|
|
23
|
+
return this.channels.get(name) ?? new Map();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeConn(
|
|
28
|
+
id: ConnId,
|
|
29
|
+
opts: { oper?: boolean; invisible?: boolean; registered?: boolean } = {},
|
|
30
|
+
): ConnectionState {
|
|
31
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
32
|
+
s.nick = id;
|
|
33
|
+
s.registration = opts.registered === false ? 'pre-registration' : 'registered';
|
|
34
|
+
if (opts.oper !== undefined) s.userModes.oper = opts.oper;
|
|
35
|
+
if (opts.invisible !== undefined) s.userModes.invisible = opts.invisible;
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('CfStats', () => {
|
|
40
|
+
it('returns zero counts for an empty deployment', async () => {
|
|
41
|
+
const rt = new StubRuntime(new Map());
|
|
42
|
+
const stats = new CfStats(rt, 1_000);
|
|
43
|
+
const snap = await stats.getStats();
|
|
44
|
+
expect(snap.users).toBe(0);
|
|
45
|
+
expect(snap.channels).toBe(0);
|
|
46
|
+
expect(snap.servers).toBe(1);
|
|
47
|
+
expect(snap.localConns).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('counts channels and their members (cross-channel dedup)', async () => {
|
|
51
|
+
const alice = makeConn('c1');
|
|
52
|
+
const bob = makeConn('c2', { invisible: true });
|
|
53
|
+
const carol = makeConn('c3', { oper: true });
|
|
54
|
+
// Alice and Bob share #a; Carol is alone in #b. Alice is also in #b
|
|
55
|
+
// (dup across channels) — must be counted once.
|
|
56
|
+
const channels = new Map<string, ReadonlyMap<ConnId, ConnectionState>>([
|
|
57
|
+
[
|
|
58
|
+
'#a',
|
|
59
|
+
new Map<ConnId, ConnectionState>([
|
|
60
|
+
['c1', alice],
|
|
61
|
+
['c2', bob],
|
|
62
|
+
]),
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
'#b',
|
|
66
|
+
new Map<ConnId, ConnectionState>([
|
|
67
|
+
['c1', alice],
|
|
68
|
+
['c3', carol],
|
|
69
|
+
]),
|
|
70
|
+
],
|
|
71
|
+
]);
|
|
72
|
+
const rt = new StubRuntime(channels);
|
|
73
|
+
|
|
74
|
+
const stats = new CfStats(rt, 1_000);
|
|
75
|
+
const snap = await stats.getStats();
|
|
76
|
+
|
|
77
|
+
expect(snap.channels).toBe(2);
|
|
78
|
+
// 3 unique connections (alice, bob, carol).
|
|
79
|
+
expect(snap.localConns).toBe(3);
|
|
80
|
+
// 3 registered, 1 invisible → 2 visible.
|
|
81
|
+
expect(snap.users).toBe(2);
|
|
82
|
+
expect(snap.invisible).toBe(1);
|
|
83
|
+
expect(snap.opers).toBe(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('counts a pre-registration member as an unknown connection', async () => {
|
|
87
|
+
const a = makeConn('c1');
|
|
88
|
+
const b = makeConn('c2', { registered: false });
|
|
89
|
+
const channels = new Map<string, ReadonlyMap<ConnId, ConnectionState>>([
|
|
90
|
+
[
|
|
91
|
+
'#a',
|
|
92
|
+
new Map<ConnId, ConnectionState>([
|
|
93
|
+
['c1', a],
|
|
94
|
+
['c2', b],
|
|
95
|
+
]),
|
|
96
|
+
],
|
|
97
|
+
]);
|
|
98
|
+
const rt = new StubRuntime(channels);
|
|
99
|
+
|
|
100
|
+
const stats = new CfStats(rt, 1_000);
|
|
101
|
+
const snap = await stats.getStats();
|
|
102
|
+
expect(snap.users).toBe(1);
|
|
103
|
+
expect(snap.unknownConnections).toBe(1);
|
|
104
|
+
expect(snap.localConns).toBe(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('echoes the supplied uptimeStartedAt', async () => {
|
|
108
|
+
const rt = new StubRuntime(new Map());
|
|
109
|
+
const stats = new CfStats(rt, 9_999);
|
|
110
|
+
const snap = await stats.getStats();
|
|
111
|
+
expect(snap.uptimeStartedAt).toBe(9_999);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns servers=1 (single-server deployment)', async () => {
|
|
115
|
+
const rt = new StubRuntime(new Map());
|
|
116
|
+
const stats = new CfStats(rt, 0);
|
|
117
|
+
const snap = await stats.getStats();
|
|
118
|
+
expect(snap.servers).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* and assert cross-DO interactions.
|
|
8
8
|
*
|
|
9
9
|
* Bindings:
|
|
10
|
-
* - `CONNECTION_DO` → `ConnectionDO` (real
|
|
10
|
+
* - `CONNECTION_DO` → `ConnectionDO` (real).
|
|
11
11
|
* - `REGISTRY_DO` → `RecordingRegistryDO` stub used by the
|
|
12
|
-
* ConnectionDO tests
|
|
12
|
+
* ConnectionDO tests. The stub records every RPC call
|
|
13
13
|
* into a per-instance log exposed via `runInDurableObject`.
|
|
14
|
-
* - `REGISTRY_DO_REAL` → `RegistryDO` (real
|
|
15
|
-
* the RegistryDO unit tests.
|
|
16
|
-
* over to this class once
|
|
17
|
-
* - `CHANNEL_DO` → `RecordingChannelDO` stub (real impl
|
|
18
|
-
*
|
|
14
|
+
* - `REGISTRY_DO_REAL` → `RegistryDO` (real). Used by
|
|
15
|
+
* the RegistryDO unit tests. The CfRuntime swaps `REGISTRY_DO`
|
|
16
|
+
* over to this class once it lands.
|
|
17
|
+
* - `CHANNEL_DO` → `RecordingChannelDO` stub (real impl in
|
|
18
|
+
* `ChannelDO`).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { ChannelDO } from '../../src/channel-do';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test-only {@link DurableObject} that records every cross-DO call from
|
|
3
|
-
* {@link ConnectionDO}. The real ChannelDO ships in
|
|
4
|
-
*
|
|
3
|
+
* {@link ConnectionDO}. The real ChannelDO ships in `channel-do.ts`;
|
|
4
|
+
* we only need to verify that ConnectionDO fan-outs (broadcast,
|
|
5
5
|
* applyChannelDelta) reach it.
|
|
6
6
|
*
|
|
7
7
|
* Each instance is keyed by lowercased channel name (the convention the
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* {@link ConnectionDO} into its `state.storage`. Tests inspect the call
|
|
4
4
|
* log via `runInDurableObject` after exercising the connection.
|
|
5
5
|
*
|
|
6
|
-
* PLAN §2.1 — the real RegistryDO ships
|
|
6
|
+
* PLAN §2.1 — the real RegistryDO ships separately. We
|
|
7
7
|
* only need enough surface area to assert that ConnectionDO closes
|
|
8
8
|
* release the nick and fan out QUIT correctly. The stub records the
|
|
9
9
|
* requested operations verbatim; it does not enforce uniqueness.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { DurableObject } from 'cloudflare:workers';
|
|
13
13
|
|
|
14
14
|
export interface RecordedRegistryCall {
|
|
15
|
-
method: 'reserveNick' | 'changeNick' | 'releaseNick' | 'lookupNick';
|
|
15
|
+
method: 'reserveNick' | 'changeNick' | 'releaseNick' | 'lookupNick' | 'listEntries';
|
|
16
16
|
args: unknown[];
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -21,6 +21,7 @@ interface RegistryRpc {
|
|
|
21
21
|
changeNick(conn: string, oldNick: string, newNick: string): Promise<boolean>;
|
|
22
22
|
releaseNick(nick: string): Promise<void>;
|
|
23
23
|
lookupNick(nick: string): Promise<string | null>;
|
|
24
|
+
listEntries(): Promise<Array<[nickLower: string, connId: string]>>;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const LOG_KEY = 'registry-call-log';
|
|
@@ -54,4 +55,9 @@ export class RecordingRegistryDO extends DurableObject implements RegistryRpc {
|
|
|
54
55
|
await this.append({ method: 'lookupNick', args: [nick] });
|
|
55
56
|
return null;
|
|
56
57
|
}
|
|
58
|
+
|
|
59
|
+
async listEntries(): Promise<Array<[nickLower: string, connId: string]>> {
|
|
60
|
+
await this.append({ method: 'listEntries', args: [] });
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
57
63
|
}
|
|
@@ -8,11 +8,18 @@ compatibility_flags = ["nodejs_compat"]
|
|
|
8
8
|
# integration-test harness so scenario 18 (MOTD) asserts against the same
|
|
9
9
|
# body text in every runtime factory.
|
|
10
10
|
[vars]
|
|
11
|
+
SERVER_NAME = "irc.example.com"
|
|
12
|
+
NETWORK_NAME = "ServerlessIRCd"
|
|
11
13
|
MOTD_LINES = "Welcome to the ServerlessIRCd integration-test harness.\nAll scenarios run parametrized over the IrcRuntime factory matrix."
|
|
12
14
|
# SASL PLAIN accounts seeded into the in-worker AccountStore. Newline-
|
|
13
15
|
# delimited `username:password` pairs; the connection-do parses these into
|
|
14
16
|
# an InMemoryAccountStore so AUTHENTICATE PLAIN works end-to-end in tests.
|
|
15
17
|
SASL_ACCOUNTS = "cf-sasl:s3cret"
|
|
18
|
+
# IRC operator credentials consulted by the OPER reducer. The DO must
|
|
19
|
+
# thread these from env into the reducer-facing ServerConfig so
|
|
20
|
+
# `OPER <user> <pass>` returns 381 rather than 491 ERR_NOOPERHOST.
|
|
21
|
+
OPER_USER = "cf-oper"
|
|
22
|
+
OPER_PASSWORD = "oper-secret"
|
|
16
23
|
|
|
17
24
|
# D1 database backing persistent SASL accounts. The D1 account-store suite
|
|
18
25
|
# creates the `accounts` table in a beforeEach and exercises the real
|