Skip to content

Commit 3b65344

Browse files
Apollon77claude
andauthored
fix(types): populate reverse mapping in cluster enum runtime objects (#3666)
* fix(types): populate reverse mapping in cluster enum runtime objects Cluster `.d.ts` files declare `NodeOperationalCertStatus` etc. as TypeScript numeric enums, which would carry value→name reverse mapping at runtime. The matter.js runtime equivalent built by `EnumForValueModel` only populated forward (name→value) entries, so `Cluster.SomeEnum[code]` returned `undefined`. This produced useless logs like Commission error for "updateFabricLabel": undefined (10), undefined (reported as matterjs-server#581). Reverse mapping now matches the declared TS shape so generic diagnostic call sites can resolve a member name from a numeric code. Also: - `StatusResponseError.is()` becomes a `error is StatusResponseError` type guard. Body still uses `of()` so cause-chain matching is preserved. - ControllerCommissioningFlow: drop the unreachable `LabelConflict` branch inside the `addNoc` block (AddNoc has no Label argument and cannot return this status per spec). Add a matching branch for `updateFabricLabel`, throwing `FabricLabelConflictError` with a descriptive message that names the conflicting label. - CommissioningController.validateAndUpdateFabricLabel: detect a `LabelConflict` `clusterCode` via `StatusResponseError.of()` and surface `FabricLabelConflictError` with the same wording so post-commission label updates produce the same actionable error as the commissioning-time path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(types): tighten EnumForValueModel return type, drop is() type guard Address PR review feedback: - Replace `Record<string, string | number>` with `EnumLikeObject` = `{ [k: string]: number } & { [k: number]: string }`. Forward lookups by name remain `number`; reverse lookups by id are `string`. Restores the numeric-enum typing that external callers relied on. - Revert `StatusResponseError.is()` to `boolean` return. Type-guarding to `error is StatusResponseError` was a partial lie because the body uses `of()` which matches the cause chain — for wrappers the original `error` is not actually an SRE. Callers needing the matched instance should use `StatusResponseError.of(error)` directly. - Add unit tests covering forward, reverse, and unknown-key lookups against `OperationalCredentials.NodeOperationalCertStatus` to lock in the runtime enum shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 07a8a29 commit 3b65344

4 files changed

Lines changed: 61 additions & 16 deletions

File tree

packages/matter.js/src/CommissioningController.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
DiscoveryData,
4545
Fabric,
4646
FabricGroups,
47+
FabricLabelConflictError,
4748
NodeSession,
4849
PeerSet,
4950
SecureSession,
@@ -56,10 +57,12 @@ import {
5657
FabricId,
5758
FabricIndex,
5859
NodeId,
60+
StatusResponseError,
5961
TypeFromPartialBitSchema,
6062
VendorId,
6163
} from "@matter/types";
6264
import { BasicInformation } from "@matter/types/clusters/basic-information";
65+
import { OperationalCredentials } from "@matter/types/clusters/operational-credentials";
6366
import { CommissioningControllerNodeOptions, NodeStates, PairedNode } from "./device/PairedNode.js";
6467
import { MatterController, PairedNodeDetails } from "./MatterController.js";
6568

@@ -908,10 +911,20 @@ export class CommissioningController {
908911
this.fabric.addressOf(nodeId),
909912
`Fabric label "${fabric.label}" does not match requested admin fabric Label "${label}". Updating...`,
910913
);
911-
await node.node.commandsOf(OperationalCredentialsClient).updateFabricLabel({
912-
label,
913-
fabricIndex: fabric.fabricIndex,
914-
});
914+
try {
915+
await node.node.commandsOf(OperationalCredentialsClient).updateFabricLabel({
916+
label,
917+
fabricIndex: fabric.fabricIndex,
918+
});
919+
} catch (error) {
920+
const sre = StatusResponseError.of(error);
921+
if (sre?.clusterCode === OperationalCredentials.NodeOperationalCertStatus.LabelConflict) {
922+
throw new FabricLabelConflictError(
923+
`Cannot set fabric label to "${label}" because another fabric on this device already uses this name. Please adjust fabric labels to be unique.`,
924+
);
925+
}
926+
throw error;
927+
}
915928
}
916929
}
917930

packages/protocol/src/peer/ControllerCommissioningFlow.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -593,11 +593,14 @@ export class ControllerCommissioningFlow {
593593
throw new MaximumCommissionedFabricsReachedError(
594594
`Commission error: This device reached the maximum number of fabrics it can be part of. Please remove a fabric before trying to add another one.`,
595595
);
596-
} else if (statusCode === OperationalCredentials.NodeOperationalCertStatus.LabelConflict) {
597-
throw new FabricLabelConflictError(
598-
`Commission error: This device is already commissioned with a fabric with the same label. Please choose a different label.`,
599-
);
600596
}
597+
} else if (
598+
context === "updateFabricLabel" &&
599+
statusCode === OperationalCredentials.NodeOperationalCertStatus.LabelConflict
600+
) {
601+
throw new FabricLabelConflictError(
602+
`Cannot set fabric label to "${this.fabric.label}" because another fabric on this device already uses this name. Please adjust fabric labels to be unique.`,
603+
);
601604
}
602605
throw new CommissioningError(
603606
`Commission error for "${context}": ${OperationalCredentials.NodeOperationalCertStatus[statusCode]} (${statusCode}), ${debugText}${
@@ -1254,7 +1257,6 @@ export class ControllerCommissioningFlow {
12541257
}),
12551258
);
12561259
} catch (error) {
1257-
// convert error
12581260
throw repackErrorAs(error, RecoverableCommissioningError);
12591261
}
12601262

packages/types/src/cluster/EnumForValueModel.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,35 @@
66

77
import { Schema, ValueModel } from "@matter/model";
88

9-
const cache = new WeakMap<ValueModel, Record<string, number>>();
9+
/**
10+
* Runtime shape of a TypeScript numeric enum: name→number forward lookup, number→name reverse lookup.
11+
*/
12+
export type EnumLikeObject = { [k: string]: number } & { [k: number]: string };
13+
14+
const cache = new WeakMap<ValueModel, EnumLikeObject>();
1015

1116
/**
12-
* Create a frozen enum object for an enum value model.
17+
* Create a frozen enum object with the runtime shape of a TypeScript numeric enum (forward and reverse mapping).
1318
*
14-
* The returned object maps enum member names to their numeric IDs. Schema is associated via {@link Schema.set} so it
15-
* can be resolved by `@field` decorators.
19+
* Schema is associated via {@link Schema.set} so it can be resolved by `@field` decorators.
1620
*
1721
* Results are cached per model instance.
1822
*/
19-
export function EnumForValueModel(model: ValueModel): Record<string, number> {
23+
export function EnumForValueModel(model: ValueModel): EnumLikeObject {
2024
let result = cache.get(model);
2125
if (result !== undefined) {
2226
return result;
2327
}
2428

25-
const values: Record<string, number> = {};
29+
const values: Record<string | number, string | number> = {};
2630
for (const child of model.children) {
2731
if (typeof child.id === "number") {
2832
values[child.name] = child.id;
33+
values[child.id] = child.name;
2934
}
3035
}
3136

32-
result = Object.freeze(values);
37+
result = Object.freeze(values) as EnumLikeObject;
3338
Schema.set(result, model);
3439

3540
cache.set(model, result);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright 2022-2026 Matter.js Authors
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { OperationalCredentials } from "#clusters/operational-credentials.js";
8+
9+
describe("EnumForValueModel runtime shape", () => {
10+
it("exposes forward name→value mapping like a TS numeric enum", () => {
11+
expect(OperationalCredentials.NodeOperationalCertStatus.Ok).equal(0);
12+
expect(OperationalCredentials.NodeOperationalCertStatus.LabelConflict).equal(10);
13+
expect(OperationalCredentials.NodeOperationalCertStatus.InvalidFabricIndex).equal(11);
14+
});
15+
16+
it("exposes reverse value→name mapping like a TS numeric enum", () => {
17+
expect(OperationalCredentials.NodeOperationalCertStatus[0]).equal("Ok");
18+
expect(OperationalCredentials.NodeOperationalCertStatus[10]).equal("LabelConflict");
19+
expect(OperationalCredentials.NodeOperationalCertStatus[11]).equal("InvalidFabricIndex");
20+
});
21+
22+
it("returns undefined for unknown values", () => {
23+
expect(OperationalCredentials.NodeOperationalCertStatus[999]).equal(undefined);
24+
});
25+
});

0 commit comments

Comments
 (0)