Skip to content

Commit 6cd5e23

Browse files
vanceingallsclaude
andcommitted
fix(studio/sdkShadow): catch dispatch errors, return dispatch_error mismatch
Wrap the dispatch loop in try/catch so a throwing SDK dispatch never propagates to Studio UX. Returns dispatched:false with kind="dispatch_error" and the error message for telemetry. One new TDD test (RED→GREEN verified). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5f6e376 commit 6cd5e23

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

packages/studio/src/utils/sdkShadow.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,27 @@ describe("sdkShadowDispatch (integration)", () => {
120120

121121
expect(session.getElement("hf-box")?.attributes["data-name"]).toBe("hero");
122122
});
123+
124+
it("returns dispatch_error when dispatch throws — does not propagate", async () => {
125+
const { sdkShadowDispatch } = await import("./sdkShadow");
126+
const session = await openComposition(BASE_HTML);
127+
// Poison dispatch so it throws on any call
128+
session.dispatch = () => {
129+
throw new Error("sdk internal error");
130+
};
131+
132+
const ops: PatchOperation[] = [{ type: "inline-style", property: "color", value: "red" }];
133+
let result: ReturnType<typeof sdkShadowDispatch> | undefined;
134+
expect(() => {
135+
result = sdkShadowDispatch(session, "hf-box", ops);
136+
}).not.toThrow();
137+
138+
expect(result!.dispatched).toBe(false);
139+
expect(result!.mismatches).toHaveLength(1);
140+
expect(result!.mismatches[0]).toMatchObject<SdkShadowMismatch>({
141+
kind: "dispatch_error",
142+
hfId: "hf-box",
143+
error: expect.stringContaining("sdk internal error"),
144+
});
145+
});
123146
});

packages/studio/src/utils/sdkShadow.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ export function patchOpsToSdkEditOps(hfId: string, ops: PatchOperation[]): EditO
5757
// ─── Shadow result types ──────────────────────────────────────────────────────
5858

5959
export interface SdkShadowMismatch {
60-
kind: "element_not_found" | "value_mismatch";
60+
kind: "element_not_found" | "value_mismatch" | "dispatch_error";
6161
hfId: string;
6262
property?: string;
6363
expected?: string | null;
6464
actual?: string | null | undefined;
65+
error?: string;
6566
}
6667

6768
export interface SdkShadowResult {
@@ -149,8 +150,15 @@ export function sdkShadowDispatch(
149150
if (!session.getElement(hfId)) {
150151
return { dispatched: false, mismatches: [{ kind: "element_not_found", hfId }] };
151152
}
152-
for (const op of patchOpsToSdkEditOps(hfId, ops)) {
153-
session.dispatch(op);
153+
try {
154+
for (const op of patchOpsToSdkEditOps(hfId, ops)) {
155+
session.dispatch(op);
156+
}
157+
} catch (err) {
158+
return {
159+
dispatched: false,
160+
mismatches: [{ kind: "dispatch_error", hfId, error: String(err) }],
161+
};
154162
}
155163
const flat = flattenSnapshot(session.getElement(hfId));
156164
const mismatches = ops

0 commit comments

Comments
 (0)