serverless-ircd 0.1.0 → 0.3.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 +96 -2
- package/.github/workflows/deploy-aws.yml +129 -0
- package/.github/workflows/deploy-cf.yml +0 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +436 -0
- package/README.md +127 -84
- package/apps/aws-stack/README.md +73 -0
- package/apps/aws-stack/bin/aws.ts +49 -0
- package/apps/aws-stack/cdk.json +10 -0
- package/apps/aws-stack/package.json +41 -0
- package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
- package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
- package/apps/aws-stack/scripts/smoke.mjs +142 -0
- package/apps/aws-stack/src/aws-stack.ts +263 -0
- package/apps/aws-stack/src/tables.ts +10 -0
- package/apps/aws-stack/tests/localstack.test.ts +46 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
- package/apps/aws-stack/tests/stack.test.ts +464 -0
- package/apps/aws-stack/tsconfig.build.json +11 -0
- package/apps/aws-stack/tsconfig.test.json +8 -0
- package/apps/aws-stack/vitest.config.ts +20 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +5 -1
- package/apps/cf-worker/wrangler.toml +66 -17
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +57 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +267 -32
- package/apps/local-cli/tests/config-loader.test.ts +107 -0
- package/apps/local-cli/tests/e2e.test.ts +126 -0
- package/apps/local-cli/tests/security-e2e.test.ts +239 -0
- package/biome.json +3 -1
- 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 +93 -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 +86 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1186 -0
- package/docs/Cloudflare-Deployment-Guide.md +660 -0
- package/docs/Home.md +11 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanIRCv3Websocket.md +489 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +19 -14
- package/packages/aws-adapter/README.md +35 -0
- package/packages/aws-adapter/package.json +52 -0
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/aws-runtime.ts +871 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +73 -0
- package/packages/aws-adapter/src/config-loader.ts +126 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +156 -0
- package/packages/aws-adapter/src/dynamo.ts +61 -0
- package/packages/aws-adapter/src/handlers/connect.ts +44 -0
- package/packages/aws-adapter/src/handlers/default.ts +330 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +48 -0
- package/packages/aws-adapter/src/handlers/index.ts +293 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
- package/packages/aws-adapter/src/handlers/state.ts +13 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
- package/packages/aws-adapter/src/index.ts +74 -0
- package/packages/aws-adapter/src/message-store.ts +34 -0
- package/packages/aws-adapter/src/serialize.ts +283 -0
- package/packages/aws-adapter/src/tables.ts +53 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +171 -0
- package/packages/aws-adapter/tests/account-store.test.ts +280 -0
- package/packages/aws-adapter/tests/aws-harness.ts +408 -0
- package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +213 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +336 -0
- package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
- package/packages/aws-adapter/tests/global-setup.ts +301 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
- package/packages/aws-adapter/tests/handlers.test.ts +473 -0
- package/packages/aws-adapter/tests/message-store.test.ts +55 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
- package/packages/aws-adapter/tests/serialize.test.ts +249 -0
- package/packages/aws-adapter/tests/smoke.test.ts +48 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
- package/packages/aws-adapter/tests/tables.test.ts +74 -0
- package/packages/aws-adapter/tests/transactions.test.ts +446 -0
- package/packages/aws-adapter/tsconfig.build.json +16 -0
- package/packages/aws-adapter/tsconfig.test.json +14 -0
- package/packages/aws-adapter/vitest.config.ts +23 -0
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/cf-runtime.ts +42 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +135 -0
- package/packages/cf-adapter/src/connection-do.ts +119 -0
- package/packages/cf-adapter/src/env.ts +27 -1
- package/packages/cf-adapter/src/index.ts +2 -1
- package/packages/cf-adapter/tests/cf-harness.ts +2 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +82 -0
- package/packages/cf-adapter/tests/worker/main.ts +2 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- package/packages/cf-adapter/wrangler.test.toml +10 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +107 -16
- package/packages/in-memory-runtime/src/index.ts +1 -1
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +134 -0
- package/packages/irc-core/package.json +13 -2
- package/packages/irc-core/src/admission.ts +216 -0
- package/packages/irc-core/src/caps/capabilities.ts +1 -0
- package/packages/irc-core/src/case-fold.ts +64 -0
- package/packages/irc-core/src/cloak.ts +81 -0
- package/packages/irc-core/src/commands/cap.ts +1 -2
- package/packages/irc-core/src/commands/chathistory.ts +305 -0
- package/packages/irc-core/src/commands/index.ts +9 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +63 -10
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/privmsg.ts +24 -0
- package/packages/irc-core/src/commands/registration.ts +79 -21
- package/packages/irc-core/src/commands/sasl.ts +251 -0
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/config.ts +270 -0
- package/packages/irc-core/src/flood-control.ts +175 -0
- package/packages/irc-core/src/index.ts +7 -0
- package/packages/irc-core/src/ports.ts +719 -0
- package/packages/irc-core/src/protocol/base64.ts +16 -0
- package/packages/irc-core/src/protocol/index.ts +2 -1
- package/packages/irc-core/src/protocol/numerics.ts +11 -0
- package/packages/irc-core/src/protocol/parser.ts +27 -2
- package/packages/irc-core/src/state/connection.ts +41 -0
- package/packages/irc-core/src/types.ts +88 -2
- package/packages/irc-core/stryker.commands.conf.json +41 -0
- package/packages/irc-core/stryker.protocol.conf.json +26 -0
- package/packages/irc-core/tests/account-store.test.ts +88 -0
- package/packages/irc-core/tests/admission.test.ts +229 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- package/packages/irc-core/tests/cloak.test.ts +78 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
- package/packages/irc-core/tests/commands/join.test.ts +246 -2
- package/packages/irc-core/tests/commands/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
- package/packages/irc-core/tests/commands/registration.test.ts +380 -20
- package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +688 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/config.test.ts +721 -0
- package/packages/irc-core/tests/flood-control.test.ts +394 -0
- package/packages/irc-core/tests/message-store.test.ts +530 -0
- package/packages/irc-core/tests/parser.test.ts +44 -1
- package/packages/irc-core/tests/ports.test.ts +263 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +186 -44
- package/packages/irc-server/src/dispatch.ts +89 -5
- package/packages/irc-server/src/index.ts +2 -0
- package/packages/irc-server/src/routing.ts +160 -0
- package/packages/irc-server/tests/actor.test.ts +690 -15
- package/packages/irc-server/tests/dispatch.test.ts +84 -0
- package/packages/irc-server/tests/routing.test.ts +204 -0
- package/packages/irc-test-support/README.md +32 -0
- package/packages/irc-test-support/package.json +37 -0
- package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +6 -5
- package/packages/irc-test-support/src/index.ts +24 -0
- package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
- package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
- package/packages/irc-test-support/tests/smoke.test.ts +11 -0
- package/packages/irc-test-support/tsconfig.build.json +16 -0
- package/packages/irc-test-support/tsconfig.test.json +14 -0
- package/packages/irc-test-support/vitest.config.ts +20 -0
- package/pnpm-workspace.yaml +1 -0
- package/tools/ci-hardening/package.json +30 -0
- package/tools/ci-hardening/src/index.ts +6 -0
- package/tools/ci-hardening/src/validate.ts +103 -0
- package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
- package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
- package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
- package/tools/ci-hardening/tests/validate.test.ts +177 -0
- package/tools/ci-hardening/tsconfig.build.json +12 -0
- package/tools/ci-hardening/tsconfig.test.json +10 -0
- package/tools/ci-hardening/vitest.config.ts +21 -0
- package/tools/seed-aws-accounts.ts +80 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
- package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
- package/tools/tcp-ws-forwarder/src/main.ts +33 -14
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
- package/packages/cf-adapter/tests/integration/index.ts +0 -17
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Effect } from '@serverless-ircd/irc-core';
|
|
2
|
+
import { CapturingLogger, LogLevel } from '@serverless-ircd/irc-core';
|
|
2
3
|
import type { ChannelDelta, ConnId, RawLine } from '@serverless-ircd/irc-core';
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
5
|
import { dispatch } from '../src/dispatch';
|
|
@@ -510,3 +511,86 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
510
511
|
expect(sends.map((s) => s.to)).toEqual(['c1']);
|
|
511
512
|
});
|
|
512
513
|
});
|
|
514
|
+
|
|
515
|
+
// ===========================================================================
|
|
516
|
+
// dispatch observability
|
|
517
|
+
// ===========================================================================
|
|
518
|
+
describe('dispatch — observability', () => {
|
|
519
|
+
it('emits a `dispatch` debug record with traceId/connectionId and effect tags', async () => {
|
|
520
|
+
const calls: RecordedCall[] = [];
|
|
521
|
+
const runtime = fakeRuntime(calls);
|
|
522
|
+
const logger = new CapturingLogger();
|
|
523
|
+
|
|
524
|
+
await dispatch(
|
|
525
|
+
[Effect.send('c1', [L('PING :1')]), Effect.send('c1', [L('PONG :1')])],
|
|
526
|
+
runtime,
|
|
527
|
+
{ logger, traceId: 't-1', connectionId: 'c1' },
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const records = logger.records.filter((r) => r.msg === 'dispatch');
|
|
531
|
+
expect(records).toHaveLength(1);
|
|
532
|
+
const rec = records[0];
|
|
533
|
+
expect(rec?.level).toBe(LogLevel.Debug);
|
|
534
|
+
expect(rec?.traceId).toBe('t-1');
|
|
535
|
+
expect(rec?.connectionId).toBe('c1');
|
|
536
|
+
expect(rec?.fields?.effects).toEqual(['Send', 'Send']);
|
|
537
|
+
expect(rec?.fields?.histogram).toEqual({ Send: 2 });
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('is silent when no logger is supplied (backwards-compatible default)', async () => {
|
|
541
|
+
const calls: RecordedCall[] = [];
|
|
542
|
+
const runtime = fakeRuntime(calls);
|
|
543
|
+
// Default silent call signature still works.
|
|
544
|
+
await expect(dispatch([Effect.send('c1', [L('x')])], runtime)).resolves.toBeUndefined();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('emits a dispatch.error warn record and rethrows when a handler rejects', async () => {
|
|
548
|
+
const runtime: IrcRuntime = {
|
|
549
|
+
...fakeRuntime([]),
|
|
550
|
+
async send() {
|
|
551
|
+
throw new Error('boom');
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
const logger = new CapturingLogger();
|
|
555
|
+
|
|
556
|
+
await expect(
|
|
557
|
+
dispatch([Effect.send('c1', [L('x')])], runtime, {
|
|
558
|
+
logger,
|
|
559
|
+
traceId: 't-2',
|
|
560
|
+
connectionId: 'c1',
|
|
561
|
+
}),
|
|
562
|
+
).rejects.toThrow('boom');
|
|
563
|
+
|
|
564
|
+
const errs = logger.records.filter((r) => r.msg === 'dispatch.error');
|
|
565
|
+
expect(errs).toHaveLength(1);
|
|
566
|
+
expect(errs[0]?.level).toBe(LogLevel.Warn);
|
|
567
|
+
expect(errs[0]?.traceId).toBe('t-2');
|
|
568
|
+
expect(errs[0]?.connectionId).toBe('c1');
|
|
569
|
+
expect(errs[0]?.fields?.effect).toBe('Send');
|
|
570
|
+
const err = errs[0]?.fields?.err as { name: string; message: string };
|
|
571
|
+
expect(err.name).toBe('Error');
|
|
572
|
+
expect(err.message).toBe('boom');
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('stringifies non-Error rejections in the dispatch.error record', async () => {
|
|
576
|
+
const runtime: IrcRuntime = {
|
|
577
|
+
...fakeRuntime([]),
|
|
578
|
+
async send() {
|
|
579
|
+
throw 'string-error';
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
const logger = new CapturingLogger();
|
|
583
|
+
|
|
584
|
+
await expect(
|
|
585
|
+
dispatch([Effect.send('c1', [L('x')])], runtime, {
|
|
586
|
+
logger,
|
|
587
|
+
traceId: 't-3',
|
|
588
|
+
connectionId: 'c1',
|
|
589
|
+
}),
|
|
590
|
+
).rejects.toBe('string-error');
|
|
591
|
+
|
|
592
|
+
const errs = logger.records.filter((r) => r.msg === 'dispatch.error');
|
|
593
|
+
expect(errs).toHaveLength(1);
|
|
594
|
+
expect(errs[0]?.fields?.err).toBe('string-error');
|
|
595
|
+
});
|
|
596
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConnectionState,
|
|
3
|
+
type Effect as EffectType,
|
|
4
|
+
EmptyMotdProvider,
|
|
5
|
+
FakeClock,
|
|
6
|
+
type FloodControlConfig,
|
|
7
|
+
SequentialIdFactory,
|
|
8
|
+
type ServerConfig,
|
|
9
|
+
buildCtx,
|
|
10
|
+
createConnection,
|
|
11
|
+
} from '@serverless-ircd/irc-core';
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { type RoutedReducers, buildRoutedReducers, resolveFloodControl } from '../src/routing';
|
|
14
|
+
|
|
15
|
+
const baseServerConfig: ServerConfig = {
|
|
16
|
+
serverName: 'irc.example.com',
|
|
17
|
+
networkName: 'ExampleNet',
|
|
18
|
+
maxChannelsPerUser: 30,
|
|
19
|
+
maxTargetsPerCommand: 10,
|
|
20
|
+
maxListEntries: 50,
|
|
21
|
+
nickLen: 30,
|
|
22
|
+
channelLen: 50,
|
|
23
|
+
topicLen: 390,
|
|
24
|
+
quitMessage: 'Client Quit',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function makeConn(id = 'c1'): ConnectionState {
|
|
28
|
+
const conn = createConnection({ id, connectedSince: 0 });
|
|
29
|
+
conn.nick = 'alice';
|
|
30
|
+
conn.user = 'alice';
|
|
31
|
+
conn.host = 'example.com';
|
|
32
|
+
conn.realname = 'Alice';
|
|
33
|
+
conn.registration = 'registered';
|
|
34
|
+
return conn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ctxFor(conn: ConnectionState, clock = new FakeClock(1_000)) {
|
|
38
|
+
return buildCtx({
|
|
39
|
+
serverConfig: baseServerConfig,
|
|
40
|
+
clock,
|
|
41
|
+
ids: new SequentialIdFactory(),
|
|
42
|
+
motd: EmptyMotdProvider,
|
|
43
|
+
connection: conn,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const motd = (): { command: string; params: string[]; tags: Record<string, string> } => ({
|
|
48
|
+
command: 'MOTD',
|
|
49
|
+
params: [],
|
|
50
|
+
tags: {},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const ping = (): { command: string; params: string[]; tags: Record<string, string> } => ({
|
|
54
|
+
command: 'PING',
|
|
55
|
+
params: ['tok'],
|
|
56
|
+
tags: {},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Counts Disconnect effects across one reducer invocation. */
|
|
60
|
+
function isDisconnect(out: { effects: EffectType[] }): boolean {
|
|
61
|
+
return out.effects.some((e) => e.tag === 'Disconnect');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('resolveFloodControl', () => {
|
|
65
|
+
it('returns null when flood control is explicitly disabled', () => {
|
|
66
|
+
expect(resolveFloodControl({ ...baseServerConfig, floodControl: null })).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('applies the documented default bucket (with control-plane exemption) when the field is omitted', () => {
|
|
70
|
+
const fc = resolveFloodControl(baseServerConfig);
|
|
71
|
+
expect(fc).not.toBeNull();
|
|
72
|
+
expect(fc?.capacity).toBe(10);
|
|
73
|
+
expect(fc?.refillRatePerSecond).toBe(1);
|
|
74
|
+
expect(fc?.disconnectThreshold).toBe(0);
|
|
75
|
+
expect(typeof fc?.commandCost).toBe('function');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('honours caller-supplied thresholds and attaches the default cost when none is supplied', () => {
|
|
79
|
+
const fc = resolveFloodControl({
|
|
80
|
+
...baseServerConfig,
|
|
81
|
+
floodControl: { capacity: 7, refillRatePerSecond: 2, disconnectThreshold: 1 },
|
|
82
|
+
});
|
|
83
|
+
expect(fc?.capacity).toBe(7);
|
|
84
|
+
expect(fc?.refillRatePerSecond).toBe(2);
|
|
85
|
+
expect(fc?.disconnectThreshold).toBe(1);
|
|
86
|
+
expect(typeof fc?.commandCost).toBe('function');
|
|
87
|
+
// PING is in the exempt set under the default cost.
|
|
88
|
+
expect(fc?.commandCost?.('PING')).toBe(0);
|
|
89
|
+
expect(fc?.commandCost?.('PRIVMSG')).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('preserves a caller-supplied commandCost verbatim', () => {
|
|
93
|
+
const custom: FloodControlConfig['commandCost'] = () => 5;
|
|
94
|
+
const fc = resolveFloodControl({
|
|
95
|
+
...baseServerConfig,
|
|
96
|
+
floodControl: {
|
|
97
|
+
capacity: 7,
|
|
98
|
+
refillRatePerSecond: 2,
|
|
99
|
+
disconnectThreshold: 1,
|
|
100
|
+
commandCost: custom,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
expect(fc?.commandCost).toBe(custom);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('buildRoutedReducers', () => {
|
|
108
|
+
it('exposes every routed command in the map', () => {
|
|
109
|
+
const reducers = buildRoutedReducers(baseServerConfig);
|
|
110
|
+
expect(Object.keys(reducers).sort()).toEqual(
|
|
111
|
+
[
|
|
112
|
+
'authenticate',
|
|
113
|
+
'away',
|
|
114
|
+
'cap',
|
|
115
|
+
'channelMode',
|
|
116
|
+
'invite',
|
|
117
|
+
'join',
|
|
118
|
+
'kick',
|
|
119
|
+
'motd',
|
|
120
|
+
'names',
|
|
121
|
+
'nick',
|
|
122
|
+
'noticeChannel',
|
|
123
|
+
'noticeUser',
|
|
124
|
+
'oper',
|
|
125
|
+
'part',
|
|
126
|
+
'pass',
|
|
127
|
+
'ping',
|
|
128
|
+
'pong',
|
|
129
|
+
'privmsgChannel',
|
|
130
|
+
'privmsgUser',
|
|
131
|
+
'quit',
|
|
132
|
+
'tagmsgChannel',
|
|
133
|
+
'tagmsgUser',
|
|
134
|
+
'topic',
|
|
135
|
+
'user',
|
|
136
|
+
'userMode',
|
|
137
|
+
].sort(),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('wraps reducers so a non-exempt burst past capacity trips Disconnect (default config)', () => {
|
|
142
|
+
const reducers = buildRoutedReducers(baseServerConfig);
|
|
143
|
+
const conn = makeConn();
|
|
144
|
+
const ctx = ctxFor(conn);
|
|
145
|
+
|
|
146
|
+
let disconnects = 0;
|
|
147
|
+
// MOTD is a charged command (cost 1); capacity 10 / threshold 0 ⇒ 10
|
|
148
|
+
// pass, the 11th onward disconnect.
|
|
149
|
+
for (let i = 0; i < 20; i++) {
|
|
150
|
+
const out = reducers.motd(conn, motd(), ctx);
|
|
151
|
+
if (isDisconnect(out)) disconnects += 1;
|
|
152
|
+
}
|
|
153
|
+
expect(disconnects).toBe(10);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('exempts control-plane traffic (PING) so a pure PING burst never disconnects', () => {
|
|
157
|
+
const reducers = buildRoutedReducers(baseServerConfig);
|
|
158
|
+
const conn = makeConn();
|
|
159
|
+
const ctx = ctxFor(conn);
|
|
160
|
+
|
|
161
|
+
let disconnects = 0;
|
|
162
|
+
for (let i = 0; i < 50; i++) {
|
|
163
|
+
const out = reducers.ping(conn, ping(), ctx);
|
|
164
|
+
if (isDisconnect(out)) disconnects += 1;
|
|
165
|
+
}
|
|
166
|
+
expect(disconnects).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('returns raw (unwrapped) reducers when flood control is disabled', () => {
|
|
170
|
+
const reducers = buildRoutedReducers({ ...baseServerConfig, floodControl: null });
|
|
171
|
+
const conn = makeConn();
|
|
172
|
+
const ctx = ctxFor(conn);
|
|
173
|
+
|
|
174
|
+
let disconnects = 0;
|
|
175
|
+
for (let i = 0; i < 20; i++) {
|
|
176
|
+
const out = reducers.motd(conn, motd(), ctx);
|
|
177
|
+
if (isDisconnect(out)) disconnects += 1;
|
|
178
|
+
}
|
|
179
|
+
// Disabled → the bucket is never consulted; every MOTD reaches the reducer.
|
|
180
|
+
expect(disconnects).toBe(0);
|
|
181
|
+
// And the connection never even acquired a bucket.
|
|
182
|
+
expect(conn.floodBucket).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('uses the same Clock source the ctx carries (no ambient time)', () => {
|
|
186
|
+
const reducers = buildRoutedReducers(baseServerConfig);
|
|
187
|
+
const conn = makeConn();
|
|
188
|
+
const clock = new FakeClock(42_000);
|
|
189
|
+
const ctx = ctxFor(conn, clock);
|
|
190
|
+
|
|
191
|
+
reducers.motd(conn, motd(), ctx);
|
|
192
|
+
// The wrapper stamps the bucket's lastRefill with the injected clock.
|
|
193
|
+
expect(conn.floodBucket?.lastRefill).toBe(42_000);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('buildRoutedReducers — type shape', () => {
|
|
198
|
+
it('returns a readonly map typed as RoutedReducers', () => {
|
|
199
|
+
const reducers: RoutedReducers = buildRoutedReducers(baseServerConfig);
|
|
200
|
+
// Touching each field confirms the public type is usable as documented.
|
|
201
|
+
expect(reducers.ping).toBeTypeOf('function');
|
|
202
|
+
expect(reducers.privmsgChannel).toBeTypeOf('function');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @serverless-ircd/irc-test-support
|
|
2
|
+
|
|
3
|
+
Shared IRC scenario suite + harness seam used by every adapter's integration tests.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
PLAN §8 calls for the same scenario suite to run against every runtime
|
|
8
|
+
implementation (in-memory, Cloudflare Workers, AWS Lambda + DynamoDB).
|
|
9
|
+
This package owns:
|
|
10
|
+
|
|
11
|
+
- `IrcHarnessFactory` / `IrcHarness` / `ClientHarness` ports — the seam
|
|
12
|
+
each adapter implements.
|
|
13
|
+
- `runIrcScenarios(factories)` — registers the 31 Phase 2 scenarios as a
|
|
14
|
+
`describe` block per factory.
|
|
15
|
+
- `inMemoryHarnessFactory` — the reference factory used to keep the suite
|
|
16
|
+
honest; also useful as a template when implementing a new adapter.
|
|
17
|
+
- `HARNESS_SERVER_CONFIG` / `HARNESS_MOTD_LINES` — the constants scenario
|
|
18
|
+
bodies assert against.
|
|
19
|
+
|
|
20
|
+
## Adding a runtime
|
|
21
|
+
|
|
22
|
+
1. Implement `IrcHarnessFactory` in your adapter package.
|
|
23
|
+
2. From a vitest test file, call `runIrcScenarios([yourFactory])`.
|
|
24
|
+
|
|
25
|
+
See `packages/cf-adapter/tests/cf-harness.ts` and
|
|
26
|
+
`packages/aws-adapter/tests/aws-harness.ts` for examples.
|
|
27
|
+
|
|
28
|
+
## Scripts
|
|
29
|
+
|
|
30
|
+
- `pnpm --filter @serverless-ircd/irc-test-support test` — runs the smoke
|
|
31
|
+
test plus the in-memory scenario matrix.
|
|
32
|
+
- `pnpm --filter @serverless-ircd/irc-test-support build` — emits `dist/`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@serverless-ircd/irc-test-support",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Shared IRC scenario runner + harness seam used by every adapter's integration suite",
|
|
6
|
+
"license": "BSD-3-Clause",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.build.json",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.test.json --noEmit",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"coverage": "vitest run --coverage",
|
|
22
|
+
"clean": "rimraf dist coverage .tsbuildinfo .turbo"
|
|
23
|
+
},
|
|
24
|
+
"files": ["dist", "src", "README.md"],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@serverless-ircd/irc-core": "workspace:*",
|
|
27
|
+
"@serverless-ircd/irc-server": "workspace:*"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@serverless-ircd/in-memory-runtime": "workspace:*",
|
|
31
|
+
"@types/node": "^26.1.1",
|
|
32
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
33
|
+
"rimraf": "^6.0.0",
|
|
34
|
+
"typescript": "^5.6.0",
|
|
35
|
+
"vitest": "^4.1.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -52,12 +52,13 @@ export const HARNESS_SERVER_CONFIG = {
|
|
|
52
52
|
} as const;
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Default MOTD lines exposed to every spawned client. The
|
|
56
|
-
*
|
|
57
|
-
* command after registration reads
|
|
58
|
-
* port. Tests assert against this content
|
|
55
|
+
* Default MOTD lines exposed to every spawned client. The registration
|
|
56
|
+
* welcome block renders these lines as `375`/`372`×N/`376` numerics, and
|
|
57
|
+
* an explicit `MOTD` command after registration reads them again via the
|
|
58
|
+
* {@link MotdProvider} port. Tests assert against this content
|
|
59
|
+
* (scenario: MOTD).
|
|
59
60
|
*/
|
|
60
|
-
const HARNESS_MOTD_LINES = [
|
|
61
|
+
export const HARNESS_MOTD_LINES = [
|
|
61
62
|
'Welcome to the ServerlessIRCd integration-test harness.',
|
|
62
63
|
'All scenarios run parametrized over the IrcRuntime factory matrix.',
|
|
63
64
|
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @serverless-ircd/irc-test-support
|
|
3
|
+
*
|
|
4
|
+
* Shared IRC scenario suite + harness seam. Every adapter package
|
|
5
|
+
* (`cf-adapter`, `aws-adapter`, …) registers its own
|
|
6
|
+
* {@link IrcHarnessFactory} and re-runs the same scenarios under its
|
|
7
|
+
* native runtime, keeping the runtime contract (PLAN §8) honest: the
|
|
8
|
+
* same tests pass in-memory, against workerd, and against
|
|
9
|
+
* dynamodb-local.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
ClientHarness,
|
|
14
|
+
IrcHarness,
|
|
15
|
+
IrcHarnessFactory,
|
|
16
|
+
SpawnClientOptions,
|
|
17
|
+
} from './harness.js';
|
|
18
|
+
export {
|
|
19
|
+
HARNESS_MOTD_LINES,
|
|
20
|
+
HARNESS_SERVER_CONFIG,
|
|
21
|
+
InMemoryHarness,
|
|
22
|
+
inMemoryHarnessFactory,
|
|
23
|
+
} from './in-memory-harness.js';
|
|
24
|
+
export { runIrcScenarios } from './scenarios.js';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it } from 'vitest';
|
|
11
|
-
import { InMemoryHarness, inMemoryHarnessFactory } from '
|
|
11
|
+
import { InMemoryHarness, inMemoryHarnessFactory } from '../src/index.js';
|
|
12
12
|
|
|
13
13
|
describe('inMemoryHarnessFactory', () => {
|
|
14
14
|
it('exposes a stable name for the describe.each matrix', () => {
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
* requires only registering a factory").
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { inMemoryHarnessFactory } from '
|
|
17
|
-
import { runIrcScenarios } from './scenarios.js';
|
|
16
|
+
import { inMemoryHarnessFactory, runIrcScenarios } from '../src/index.js';
|
|
18
17
|
|
|
19
18
|
/** The runtime matrix. Adding a runtime = appending one entry here. */
|
|
20
19
|
runIrcScenarios([inMemoryHarnessFactory]);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test for the extracted `@serverless-ircd/irc-test-support` package.
|
|
3
|
+
*
|
|
4
|
+
* Confirms the in-memory factory still runs the full scenario suite after
|
|
5
|
+
* the move out of `cf-adapter/tests/integration`. Adapter packages import
|
|
6
|
+
* the same surface to register their own factories.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { inMemoryHarnessFactory, runIrcScenarios } from '../src/index.js';
|
|
10
|
+
|
|
11
|
+
runIrcScenarios([inMemoryHarnessFactory]);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"tsBuildInfoFile": "./.tsbuildinfo",
|
|
8
|
+
"types": ["node"]
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*.ts"],
|
|
11
|
+
"exclude": ["dist", "tests", "**/*.test.ts"],
|
|
12
|
+
"references": [
|
|
13
|
+
{ "path": "../irc-core/tsconfig.build.json" },
|
|
14
|
+
{ "path": "../irc-server/tsconfig.build.json" }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"types": ["node"]
|
|
6
|
+
},
|
|
7
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
|
8
|
+
"exclude": ["dist"],
|
|
9
|
+
"references": [
|
|
10
|
+
{ "path": "../irc-core/tsconfig.build.json" },
|
|
11
|
+
{ "path": "../irc-server/tsconfig.build.json" },
|
|
12
|
+
{ "path": "../in-memory-runtime/tsconfig.build.json" }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
include: ['tests/**/*.test.ts'],
|
|
6
|
+
coverage: {
|
|
7
|
+
provider: 'v8',
|
|
8
|
+
all: false,
|
|
9
|
+
include: ['src/**/*.ts'],
|
|
10
|
+
exclude: ['src/**/index.ts'],
|
|
11
|
+
reporter: ['text', 'html', 'json-summary'],
|
|
12
|
+
thresholds: {
|
|
13
|
+
lines: 90,
|
|
14
|
+
functions: 90,
|
|
15
|
+
branches: 90,
|
|
16
|
+
statements: 90,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@serverless-ircd/ci-hardening",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "CI hardening helpers: coverage-gate config validator + mutation-testing driver for irc-core",
|
|
6
|
+
"license": "BSD-3-Clause",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.build.json",
|
|
12
|
+
"typecheck": "tsc -p tsconfig.test.json --noEmit",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"coverage": "vitest run --coverage",
|
|
16
|
+
"start": "node --enable-source-maps ./dist/main.js",
|
|
17
|
+
"clean": "rimraf dist coverage .tsbuildinfo .turbo"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"js-yaml": "^4.1.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/js-yaml": "^4.0.9",
|
|
24
|
+
"@types/node": "^26.1.1",
|
|
25
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
26
|
+
"rimraf": "^6.0.0",
|
|
27
|
+
"typescript": "^5.6.0",
|
|
28
|
+
"vitest": "^4.1.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { load } from 'js-yaml';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimum mutation score (in percent) required for the Stryker
|
|
6
|
+
* spot-checks on `irc-core/protocol` and `irc-core/commands`.
|
|
7
|
+
*
|
|
8
|
+
* Exported so the test-suite and any future dashboards can
|
|
9
|
+
* import the canonical value instead of re-hard-coding it.
|
|
10
|
+
*/
|
|
11
|
+
export const MUTATION_SCORE_THRESHOLD = 80;
|
|
12
|
+
|
|
13
|
+
export interface CoverageThresholds {
|
|
14
|
+
lines: number;
|
|
15
|
+
functions: number;
|
|
16
|
+
branches: number;
|
|
17
|
+
statements: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface StrykerThresholds {
|
|
21
|
+
high?: number;
|
|
22
|
+
low?: number;
|
|
23
|
+
break?: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface StrykerConfig {
|
|
27
|
+
mutate?: unknown;
|
|
28
|
+
thresholds?: StrykerThresholds;
|
|
29
|
+
coverageAnalysis?: string;
|
|
30
|
+
testRunner?: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface WorkflowJob {
|
|
35
|
+
steps: Array<{ name?: string; run?: string; [key: string]: unknown }>;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ParsedWorkflow {
|
|
40
|
+
jobs?: Record<string, WorkflowJob>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract the `coverage.thresholds` block from a vitest config file.
|
|
45
|
+
*
|
|
46
|
+
* Vitest configs are plain TypeScript modules; rather than pulling in
|
|
47
|
+
* the full TS toolchain to evaluate them, we parse the source as text
|
|
48
|
+
* and pull out the `thresholds: { ... }` object. This deliberately
|
|
49
|
+
* fails loud (returns NaN-bearing values) if the config shape drifts,
|
|
50
|
+
* so the test-suite notices immediately.
|
|
51
|
+
*
|
|
52
|
+
* @param configPath absolute path to a vitest.config.ts file.
|
|
53
|
+
*/
|
|
54
|
+
export function readCoverageThresholds(configPath: string): CoverageThresholds {
|
|
55
|
+
const src = readFileSync(configPath, 'utf8');
|
|
56
|
+
const match = src.match(/thresholds:\s*\{([^}]*)\}/);
|
|
57
|
+
if (match === null || match[1] === undefined) {
|
|
58
|
+
throw new Error(`No coverage.thresholds block found in ${configPath}`);
|
|
59
|
+
}
|
|
60
|
+
const block = match[1];
|
|
61
|
+
const grab = (key: string): number => {
|
|
62
|
+
const m = block.match(new RegExp(`${key}:\\s*(\\d+)`));
|
|
63
|
+
if (m === null || m[1] === undefined) {
|
|
64
|
+
throw new Error(`No ${key} threshold found in ${configPath}`);
|
|
65
|
+
}
|
|
66
|
+
return Number.parseInt(m[1], 10);
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
lines: grab('lines'),
|
|
70
|
+
functions: grab('functions'),
|
|
71
|
+
branches: grab('branches'),
|
|
72
|
+
statements: grab('statements'),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Load and validate a Stryker JSON config file.
|
|
78
|
+
*
|
|
79
|
+
* @param configPath absolute path to a `stryker.*.conf.json` file.
|
|
80
|
+
*/
|
|
81
|
+
export function readStrykerConfig(configPath: string): StrykerConfig {
|
|
82
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
83
|
+
const parsed = JSON.parse(raw) as StrykerConfig;
|
|
84
|
+
if (!Array.isArray(parsed.mutate)) {
|
|
85
|
+
throw new Error(`Stryker config at ${configPath} is missing mutate[]`);
|
|
86
|
+
}
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse a GitHub Actions YAML file and return the named job.
|
|
92
|
+
*
|
|
93
|
+
* @param yamlText raw YAML source of a workflow file.
|
|
94
|
+
* @param jobName key under `jobs:`.
|
|
95
|
+
*/
|
|
96
|
+
export function readWorkflowJob(yamlText: string, jobName: string): WorkflowJob {
|
|
97
|
+
const parsed = load(yamlText) as ParsedWorkflow;
|
|
98
|
+
const job = parsed.jobs?.[jobName];
|
|
99
|
+
if (job === undefined) {
|
|
100
|
+
throw new Error(`Workflow has no job named "${jobName}"`);
|
|
101
|
+
}
|
|
102
|
+
return job;
|
|
103
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Fixture: vitest config with only some threshold keys present.
|
|
2
|
+
import { defineConfig } from 'vitest/config';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
environment: 'node',
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: 'v8',
|
|
9
|
+
thresholds: {
|
|
10
|
+
functions: 100,
|
|
11
|
+
branches: 100,
|
|
12
|
+
statements: 100,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|