@antasphere/chassis-contract 1.0.0 → 1.0.1
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/dist/entitlements.d.ts +9 -230
- package/dist/entitlements.d.ts.map +1 -1
- package/dist/entitlements.js +20 -209
- package/dist/entitlements.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/entitlements.ts +46 -233
- package/src/index.ts +0 -1
- package/dist/wire.d.ts +0 -101
- package/dist/wire.d.ts.map +0 -1
- package/dist/wire.js +0 -161
- package/dist/wire.js.map +0 -1
- package/src/wire.ts +0 -246
- package/test/hub-wire.test.ts +0 -114
package/src/wire.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
BILLING_PLANS,
|
|
4
|
-
USAGE_CHECK_REASONS,
|
|
5
|
-
USAGE_EVENT_MAX_FUTURE_MS,
|
|
6
|
-
USAGE_EVENT_MAX_PAST_MS,
|
|
7
|
-
USAGE_EVENTS_BATCH_MAX,
|
|
8
|
-
USAGE_REJECT_REASONS,
|
|
9
|
-
USAGE_ROUTE_ERRORS,
|
|
10
|
-
USAGE_WRITE_SCOPE,
|
|
11
|
-
entitlementProfileSchema,
|
|
12
|
-
usageCheckRequestSchema,
|
|
13
|
-
usageCheckSchema,
|
|
14
|
-
usageEventOccurrenceIssue,
|
|
15
|
-
usageEventSchema,
|
|
16
|
-
usageEventsBatchSchema,
|
|
17
|
-
usageIngestResultSchema
|
|
18
|
-
} from './entitlements.js';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The chassis's side of the hub–tool wire check (PRDCT-2677). The hub owns
|
|
22
|
-
* the messages the tools exchange with it and publishes them as ONE
|
|
23
|
-
* generated snapshot, `packages/contract/wire/hub-tool-messages.json` of the
|
|
24
|
-
* hub, built by its `packages/contract/src/wire.ts`. The block below, from
|
|
25
|
-
* `WIRE_SCHEMA_KEYS` to `buildWireSnapshot`, is the chassis's copy of that
|
|
26
|
-
* builder, kept VERBATIM so both sides compute the same picture: the same
|
|
27
|
-
* JSON Schema export, the same probe verdicts, the same occurrence table.
|
|
28
|
-
* The hub's inputs (its offsets and probe payloads) and its definitions are
|
|
29
|
-
* NOT copied: the inputs are read back from the hub's file
|
|
30
|
-
* (`wireInputsOf`), the definitions are this package's own copies
|
|
31
|
-
* (`CHASSIS_WIRE_DEFINITIONS`, from `entitlements.ts`).
|
|
32
|
-
*
|
|
33
|
-
* `compareWireSnapshots` names every difference by its dotted path;
|
|
34
|
-
* `scripts/hub-wire-check.mjs` (`pnpm --filter @antasphere/chassis-contract
|
|
35
|
-
* wire:check`, the `hub-wire` CI job against the hub's `dev`) fails on any.
|
|
36
|
-
* The check runs one way: the hub changes first, the chassis follows.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
// ── Copied VERBATIM from the hub's packages/contract/src/wire.ts ──────────
|
|
40
|
-
|
|
41
|
-
/** The keys of the six shapes, the same on both sides. */
|
|
42
|
-
export const WIRE_SCHEMA_KEYS = [
|
|
43
|
-
'usageEvent',
|
|
44
|
-
'usageEventsBatch',
|
|
45
|
-
'usageIngestResult',
|
|
46
|
-
'usageEntitlements',
|
|
47
|
-
'usageCheckRequest',
|
|
48
|
-
'usageCheck'
|
|
49
|
-
] as const;
|
|
50
|
-
export type WireSchemaKey = (typeof WIRE_SCHEMA_KEYS)[number];
|
|
51
|
-
|
|
52
|
-
export interface WireConstants {
|
|
53
|
-
USAGE_WRITE_SCOPE: string;
|
|
54
|
-
BILLING_PLANS: readonly string[];
|
|
55
|
-
USAGE_EVENTS_BATCH_MAX: number;
|
|
56
|
-
USAGE_EVENT_MAX_PAST_MS: number;
|
|
57
|
-
USAGE_EVENT_MAX_FUTURE_MS: number;
|
|
58
|
-
USAGE_CHECK_REASONS: readonly string[];
|
|
59
|
-
USAGE_REJECT_REASONS: readonly string[];
|
|
60
|
-
USAGE_ROUTE_ERRORS: Readonly<Record<string, number>>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface WireProbe {
|
|
64
|
-
name: string;
|
|
65
|
-
input: unknown;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface WireProbeResult extends WireProbe {
|
|
69
|
-
/** Whether the schema accepts the input. */
|
|
70
|
-
ok: boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface WireOccurrence {
|
|
74
|
-
/** The event's `occurredAt` minus the receipt time, in milliseconds. */
|
|
75
|
-
offsetMs: number;
|
|
76
|
-
issue: { path: 'occurredAt'; message: string } | null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** What one side feeds the builder: its own definitions. */
|
|
80
|
-
export interface WireDefinitions {
|
|
81
|
-
constants: WireConstants;
|
|
82
|
-
schemas: Record<WireSchemaKey, z.ZodType>;
|
|
83
|
-
occurrenceIssue: (occurredAt: Date, receivedAt: Date) => { path: 'occurredAt'; message: string } | null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** The inputs the snapshot is computed on: the hub's, read back by the chassis from the hub's file. */
|
|
87
|
-
export interface WireInputs {
|
|
88
|
-
occurrenceOffsetsMs: readonly number[];
|
|
89
|
-
probes: Record<WireSchemaKey, readonly WireProbe[]>;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface HubToolWireSnapshot {
|
|
93
|
-
v: 1;
|
|
94
|
-
constants: WireConstants;
|
|
95
|
-
schemas: Record<WireSchemaKey, unknown>;
|
|
96
|
-
occurrence: WireOccurrence[];
|
|
97
|
-
probes: Record<WireSchemaKey, WireProbeResult[]>;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** A fixed receipt time, so the occurrence table is the same on every run. */
|
|
101
|
-
const RECEIVED_AT = new Date('2026-09-23T12:00:00.000Z');
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Build the snapshot from one side's definitions on the given inputs. The
|
|
105
|
-
* SAME function runs on both sides (the chassis carries a verbatim copy):
|
|
106
|
-
* the hub with its own inputs, the chassis with the inputs read from the
|
|
107
|
-
* hub's file. Deterministic: no clock, no randomness, no environment.
|
|
108
|
-
*/
|
|
109
|
-
export function buildWireSnapshot(defs: WireDefinitions, inputs: WireInputs): HubToolWireSnapshot {
|
|
110
|
-
const schemas = {} as Record<WireSchemaKey, unknown>;
|
|
111
|
-
const probes = {} as Record<WireSchemaKey, WireProbeResult[]>;
|
|
112
|
-
for (const key of WIRE_SCHEMA_KEYS) {
|
|
113
|
-
const schema = defs.schemas[key];
|
|
114
|
-
// A refinement has no JSON Schema form and is dropped on both sides
|
|
115
|
-
// alike; the probes below carry what it decides.
|
|
116
|
-
schemas[key] = z.toJSONSchema(schema, { unrepresentable: 'any' });
|
|
117
|
-
probes[key] = inputs.probes[key].map((probe) => ({
|
|
118
|
-
name: probe.name,
|
|
119
|
-
input: probe.input,
|
|
120
|
-
ok: schema.safeParse(probe.input).success
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
v: 1,
|
|
125
|
-
constants: {
|
|
126
|
-
USAGE_WRITE_SCOPE: defs.constants.USAGE_WRITE_SCOPE,
|
|
127
|
-
BILLING_PLANS: [...defs.constants.BILLING_PLANS],
|
|
128
|
-
USAGE_EVENTS_BATCH_MAX: defs.constants.USAGE_EVENTS_BATCH_MAX,
|
|
129
|
-
USAGE_EVENT_MAX_PAST_MS: defs.constants.USAGE_EVENT_MAX_PAST_MS,
|
|
130
|
-
USAGE_EVENT_MAX_FUTURE_MS: defs.constants.USAGE_EVENT_MAX_FUTURE_MS,
|
|
131
|
-
USAGE_CHECK_REASONS: [...defs.constants.USAGE_CHECK_REASONS],
|
|
132
|
-
USAGE_REJECT_REASONS: [...defs.constants.USAGE_REJECT_REASONS],
|
|
133
|
-
USAGE_ROUTE_ERRORS: { ...defs.constants.USAGE_ROUTE_ERRORS }
|
|
134
|
-
},
|
|
135
|
-
schemas,
|
|
136
|
-
occurrence: inputs.occurrenceOffsetsMs.map((offsetMs) => ({
|
|
137
|
-
offsetMs,
|
|
138
|
-
issue: defs.occurrenceIssue(new Date(RECEIVED_AT.getTime() + offsetMs), RECEIVED_AT)
|
|
139
|
-
})),
|
|
140
|
-
probes
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ── The chassis's own half ─────────────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
/** The chassis's definitions: this package's copies of the hub's shapes, under the hub's keys. */
|
|
147
|
-
export const CHASSIS_WIRE_DEFINITIONS: WireDefinitions = {
|
|
148
|
-
constants: {
|
|
149
|
-
USAGE_WRITE_SCOPE,
|
|
150
|
-
BILLING_PLANS,
|
|
151
|
-
USAGE_EVENTS_BATCH_MAX,
|
|
152
|
-
USAGE_EVENT_MAX_PAST_MS,
|
|
153
|
-
USAGE_EVENT_MAX_FUTURE_MS,
|
|
154
|
-
USAGE_CHECK_REASONS,
|
|
155
|
-
USAGE_REJECT_REASONS,
|
|
156
|
-
USAGE_ROUTE_ERRORS
|
|
157
|
-
},
|
|
158
|
-
schemas: {
|
|
159
|
-
usageEvent: usageEventSchema,
|
|
160
|
-
usageEventsBatch: usageEventsBatchSchema,
|
|
161
|
-
usageIngestResult: usageIngestResultSchema,
|
|
162
|
-
usageEntitlements: entitlementProfileSchema,
|
|
163
|
-
usageCheckRequest: usageCheckRequestSchema,
|
|
164
|
-
usageCheck: usageCheckSchema
|
|
165
|
-
},
|
|
166
|
-
occurrenceIssue: usageEventOccurrenceIssue
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
/** The inputs the hub computed its snapshot on, read back from its file: the offsets and the probe payloads. */
|
|
170
|
-
export function wireInputsOf(reference: HubToolWireSnapshot): WireInputs {
|
|
171
|
-
const probes = {} as Record<WireSchemaKey, readonly WireProbe[]>;
|
|
172
|
-
for (const key of WIRE_SCHEMA_KEYS) {
|
|
173
|
-
probes[key] = (reference.probes[key] ?? []).map((probe) => ({ name: probe.name, input: probe.input }));
|
|
174
|
-
}
|
|
175
|
-
return {
|
|
176
|
-
occurrenceOffsetsMs: reference.occurrence.map((row) => row.offsetMs),
|
|
177
|
-
probes
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** The snapshot the chassis's copies give on the hub's own inputs: what must equal the hub's file. */
|
|
182
|
-
export function chassisWireSnapshot(reference: HubToolWireSnapshot): HubToolWireSnapshot {
|
|
183
|
-
return buildWireSnapshot(CHASSIS_WIRE_DEFINITIONS, wireInputsOf(reference));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** One place where the hub's snapshot and the chassis's differ: its dotted path and the two values. */
|
|
187
|
-
export interface WireDifference {
|
|
188
|
-
path: string;
|
|
189
|
-
hub: unknown;
|
|
190
|
-
chassis: unknown;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
194
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function childPath(parent: string, key: string): string {
|
|
198
|
-
return /^[A-Za-z_$][\w$]*$/.test(key)
|
|
199
|
-
? parent === ''
|
|
200
|
-
? key
|
|
201
|
-
: `${parent}.${key}`
|
|
202
|
-
: `${parent}[${JSON.stringify(key)}]`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function compareAt(path: string, hub: unknown, chassis: unknown, out: WireDifference[]): void {
|
|
206
|
-
if (Array.isArray(hub) && Array.isArray(chassis)) {
|
|
207
|
-
if (hub.length !== chassis.length) {
|
|
208
|
-
out.push({ path, hub, chassis });
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
for (let i = 0; i < hub.length; i += 1) compareAt(`${path}[${i}]`, hub[i], chassis[i], out);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (isPlainObject(hub) && isPlainObject(chassis)) {
|
|
215
|
-
const keys = [...new Set([...Object.keys(hub), ...Object.keys(chassis)])].sort();
|
|
216
|
-
for (const key of keys) compareAt(childPath(path, key), hub[key], chassis[key], out);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (!Object.is(hub, chassis)) out.push({ path, hub, chassis });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Every difference between the hub's snapshot and the chassis's, by dotted
|
|
224
|
-
* path (`constants.USAGE_CHECK_REASONS[2]`,
|
|
225
|
-
* `schemas.usageCheck.properties.reason.anyOf[0].enum`,
|
|
226
|
-
* `probes.usageCheck[3].ok`). Objects compare by the union of their keys,
|
|
227
|
-
* order ignored, an absent key and `undefined` being the same; arrays by
|
|
228
|
-
* index, a length difference being one difference at the array's path.
|
|
229
|
-
* Deterministic: keys sorted, arrays in order. Empty when they agree.
|
|
230
|
-
*/
|
|
231
|
-
export function compareWireSnapshots(
|
|
232
|
-
hub: HubToolWireSnapshot,
|
|
233
|
-
chassis: HubToolWireSnapshot
|
|
234
|
-
): WireDifference[] {
|
|
235
|
-
const out: WireDifference[] = [];
|
|
236
|
-
compareAt('', hub, chassis, out);
|
|
237
|
-
return out;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** One line of figures for a snapshot: `6 shapes, N constants, M probes, K occurrence offsets`. */
|
|
241
|
-
export function describeWireSnapshot(snapshot: HubToolWireSnapshot): string {
|
|
242
|
-
const shapes = Object.keys(snapshot.schemas).length;
|
|
243
|
-
const constants = Object.keys(snapshot.constants).length;
|
|
244
|
-
const probes = Object.values(snapshot.probes).reduce((n, list) => n + list.length, 0);
|
|
245
|
-
return `${shapes} shapes, ${constants} constants, ${probes} probes, ${snapshot.occurrence.length} occurrence offsets`;
|
|
246
|
-
}
|
package/test/hub-wire.test.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import {
|
|
6
|
-
buildWireSnapshot,
|
|
7
|
-
CHASSIS_WIRE_DEFINITIONS,
|
|
8
|
-
chassisWireSnapshot,
|
|
9
|
-
compareWireSnapshots,
|
|
10
|
-
type HubToolWireSnapshot,
|
|
11
|
-
type WireInputs
|
|
12
|
-
} from '../src/index.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The hub–tool wire check (PRDCT-2677). First the compare itself, on a
|
|
16
|
-
* snapshot the chassis builds from its own copies, so it is proven without
|
|
17
|
-
* any hub file; then the cross-check against the hub's snapshot, found the
|
|
18
|
-
* way `scripts/hub-wire-check.mjs` finds it (`HUB_WIRE_SNAPSHOT`, else the
|
|
19
|
-
* hub checkout beside this repository), skipped when neither is there. CI
|
|
20
|
-
* runs the script against the hub's `dev` (the `hub-wire` job).
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const ACCOUNT = '0b6c7c1e-4f4e-4a7b-9c1d-2e3f4a5b6c7d';
|
|
24
|
-
const INPUTS: WireInputs = {
|
|
25
|
-
occurrenceOffsetsMs: [0, 5 * 60 * 1000 + 1],
|
|
26
|
-
probes: {
|
|
27
|
-
usageEvent: [{ name: 'empty', input: {} }],
|
|
28
|
-
usageEventsBatch: [{ name: 'empty batch', input: { events: [] } }],
|
|
29
|
-
usageIngestResult: [
|
|
30
|
-
{ name: 'no results', input: { results: [], accepted: 0, duplicate: 0, rejected: 0 } }
|
|
31
|
-
],
|
|
32
|
-
usageEntitlements: [{ name: 'missing everything', input: {} }],
|
|
33
|
-
usageCheckRequest: [
|
|
34
|
-
{ name: 'plain', input: { accountRef: ACCOUNT, actionKey: 'things.publish', quantity: 1 } }
|
|
35
|
-
],
|
|
36
|
-
usageCheck: [
|
|
37
|
-
{
|
|
38
|
-
name: 'unpriceable',
|
|
39
|
-
input: {
|
|
40
|
-
accountRef: ACCOUNT,
|
|
41
|
-
actionKey: 'things.publish',
|
|
42
|
-
quantity: 1,
|
|
43
|
-
allowed: false,
|
|
44
|
-
credits: Number.MAX_SAFE_INTEGER,
|
|
45
|
-
balance: 0,
|
|
46
|
-
unit: 'call',
|
|
47
|
-
priced: true,
|
|
48
|
-
plan: 'free',
|
|
49
|
-
reason: 'unpriceable',
|
|
50
|
-
topUpUrl: 'https://account.example/billing/top-up'
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
]
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const clone = (s: HubToolWireSnapshot): HubToolWireSnapshot => JSON.parse(JSON.stringify(s));
|
|
58
|
-
|
|
59
|
-
describe('compareWireSnapshots', () => {
|
|
60
|
-
const base = clone(buildWireSnapshot(CHASSIS_WIRE_DEFINITIONS, INPUTS));
|
|
61
|
-
|
|
62
|
-
it('finds nothing between a snapshot and itself, and the chassis rebuilds it on its own inputs', () => {
|
|
63
|
-
expect(compareWireSnapshots(base, clone(base))).toEqual([]);
|
|
64
|
-
expect(compareWireSnapshots(base, clone(chassisWireSnapshot(base)))).toEqual([]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('names a constant, a probe verdict and a schema leaf by their paths', () => {
|
|
68
|
-
const other = clone(base);
|
|
69
|
-
(other.constants.USAGE_CHECK_REASONS as string[])[2] = 'over_quota';
|
|
70
|
-
other.probes.usageCheck[0]!.ok = !other.probes.usageCheck[0]!.ok;
|
|
71
|
-
const reason = (
|
|
72
|
-
other.schemas.usageCheck as { properties: { reason: { anyOf: Array<{ enum?: string[] }> } } }
|
|
73
|
-
).properties.reason.anyOf[0]!;
|
|
74
|
-
reason.enum = ['insufficient_credits', 'account_suspended'];
|
|
75
|
-
const differences = compareWireSnapshots(base, other);
|
|
76
|
-
expect(differences.map((d) => d.path)).toEqual([
|
|
77
|
-
'constants.USAGE_CHECK_REASONS[2]',
|
|
78
|
-
'probes.usageCheck[0].ok',
|
|
79
|
-
'schemas.usageCheck.properties.reason.anyOf[0].enum'
|
|
80
|
-
]);
|
|
81
|
-
expect(differences[0]).toEqual({
|
|
82
|
-
path: 'constants.USAGE_CHECK_REASONS[2]',
|
|
83
|
-
hub: 'unpriceable',
|
|
84
|
-
chassis: 'over_quota'
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('treats an absent key as undefined and names an added one', () => {
|
|
89
|
-
const other = clone(base);
|
|
90
|
-
(other.constants.USAGE_ROUTE_ERRORS as Record<string, number | undefined>).payment_required = undefined;
|
|
91
|
-
expect(compareWireSnapshots(base, other)).toEqual([]);
|
|
92
|
-
(other.constants.USAGE_ROUTE_ERRORS as Record<string, number>).payment_required = 402;
|
|
93
|
-
expect(compareWireSnapshots(base, other)).toEqual([
|
|
94
|
-
{ path: 'constants.USAGE_ROUTE_ERRORS.payment_required', hub: undefined, chassis: 402 }
|
|
95
|
-
]);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// The hub's file, found as the script finds it: from this package, four levels up is labs/products/antasphere/.
|
|
100
|
-
const PACKAGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
101
|
-
const SIBLING = path.resolve(PACKAGE_DIR, '../../../../hub/packages/contract/wire/hub-tool-messages.json');
|
|
102
|
-
const HUB_FILE = process.env.HUB_WIRE_SNAPSHOT ? path.resolve(process.env.HUB_WIRE_SNAPSHOT) : SIBLING;
|
|
103
|
-
|
|
104
|
-
describe('the chassis’s copies against the hub’s wire snapshot', () => {
|
|
105
|
-
if (!existsSync(HUB_FILE)) {
|
|
106
|
-
it.skip(`skipped: no hub snapshot at ${HUB_FILE} (set HUB_WIRE_SNAPSHOT; CI runs the hub-wire job)`, () => {});
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
it(`agree with ${HUB_FILE}`, () => {
|
|
110
|
-
const hub = JSON.parse(readFileSync(HUB_FILE, 'utf8')) as HubToolWireSnapshot;
|
|
111
|
-
const differences = compareWireSnapshots(hub, clone(chassisWireSnapshot(hub)));
|
|
112
|
-
expect(differences, JSON.stringify(differences, null, 2)).toEqual([]);
|
|
113
|
-
});
|
|
114
|
-
});
|