





















@@ -75,6 +75,42 @@ type TestNodeSession = {
7575platform?: string;
7676};
777778+function isRecord(value: unknown): value is Record<string, unknown> {
79+return typeof value === "object" && value !== null && !Array.isArray(value);
80+}
81+82+function requireRecord(value: unknown, label: string): Record<string, unknown> {
83+expect(isRecord(value), `${label} must be an object`).toBe(true);
84+return value as Record<string, unknown>;
85+}
86+87+function expectRecordFields(
88+value: unknown,
89+label: string,
90+expected: Record<string, unknown>,
91+): Record<string, unknown> {
92+const record = requireRecord(value, label);
93+for (const [key, expectedValue] of Object.entries(expected)) {
94+expect(record[key], `${label}.${key}`).toEqual(expectedValue);
95+}
96+return record;
97+}
98+99+function requireRespondPayload(call: RespondCall | undefined, label: string) {
100+expect(call?.[0], `${label} success`).toBe(true);
101+return requireRecord(call?.[1], `${label} payload`);
102+}
103+104+function expectQueuedAction(
105+payload: Record<string, unknown>,
106+expected: Record<string, unknown>,
107+): Record<string, unknown> {
108+expect(Array.isArray(payload.actions), "payload.actions must be an array").toBe(true);
109+const actions = payload.actions as unknown[];
110+expect(actions).toHaveLength(1);
111+return expectRecordFields(actions[0], "queued action", expected);
112+}
113+78114const WAKE_WAIT_TIMEOUT_MS = 3_001;
79115const DEFAULT_RELAY_CONFIG = {
80116baseUrl: "https://relay.example.com",
@@ -352,13 +388,13 @@ describe("node.invoke APNs wake path", () => {
352388const first = await maybeWakeNodeWithApns("ios-node-relay-no-auth");
353389const second = await maybeWakeNodeWithApns("ios-node-relay-no-auth");
354390355-expect(first).toMatchObject({
391+expectRecordFields(first, "first wake result", {
356392available: false,
357393throttled: false,
358394path: "no-auth",
359395apnsReason: "relay config missing",
360396});
361-expect(second).toMatchObject({
397+expectRecordFields(second, "second wake result", {
362398available: false,
363399throttled: false,
364400path: "no-auth",
@@ -379,30 +415,30 @@ describe("node.invoke APNs wake path", () => {
379415transport: "direct",
380416});
381417382-await expect(maybeWakeNodeWithApns("ios-node-clear-wake")).resolves.toMatchObject({
418+expectRecordFields(await maybeWakeNodeWithApns("ios-node-clear-wake"), "wake result", {
383419path: "sent",
384420throttled: false,
385421});
386-await expect(maybeSendNodeWakeNudge("ios-node-clear-wake")).resolves.toMatchObject({
422+expectRecordFields(await maybeSendNodeWakeNudge("ios-node-clear-wake"), "nudge result", {
387423sent: true,
388424throttled: false,
389425});
390-await expect(maybeWakeNodeWithApns("ios-node-clear-wake")).resolves.toMatchObject({
426+expectRecordFields(await maybeWakeNodeWithApns("ios-node-clear-wake"), "wake result", {
391427path: "throttled",
392428throttled: true,
393429});
394-await expect(maybeSendNodeWakeNudge("ios-node-clear-wake")).resolves.toMatchObject({
430+expectRecordFields(await maybeSendNodeWakeNudge("ios-node-clear-wake"), "nudge result", {
395431sent: false,
396432throttled: true,
397433});
398434399435clearNodeWakeState("ios-node-clear-wake");
400436401-await expect(maybeWakeNodeWithApns("ios-node-clear-wake")).resolves.toMatchObject({
437+expectRecordFields(await maybeWakeNodeWithApns("ios-node-clear-wake"), "wake result", {
402438path: "sent",
403439throttled: false,
404440});
405-await expect(maybeSendNodeWakeNudge("ios-node-clear-wake")).resolves.toMatchObject({
441+expectRecordFields(await maybeSendNodeWakeNudge("ios-node-clear-wake"), "nudge result", {
406442sent: true,
407443throttled: false,
408444});
@@ -443,15 +479,13 @@ describe("node.invoke APNs wake path", () => {
443479444480expect(mocks.sendApnsBackgroundWake).toHaveBeenCalledTimes(1);
445481expect(nodeRegistry.invoke).toHaveBeenCalledTimes(1);
446-expect(nodeRegistry.invoke).toHaveBeenCalledWith(
447-expect.objectContaining({
448-nodeId: "ios-node-reconnect",
449-command: "camera.capture",
450-}),
451-);
482+expectRecordFields(nodeRegistry.invoke.mock.calls[0]?.[0], "node invoke payload", {
483+nodeId: "ios-node-reconnect",
484+command: "camera.capture",
485+});
452486const call = respond.mock.calls[0] as RespondCall | undefined;
453487expect(call?.[0]).toBe(true);
454-expect(call?.[1]).toMatchObject({ ok: true, nodeId: "ios-node-reconnect" });
488+expectRecordFields(call?.[1], "respond payload", { ok: true, nodeId: "ios-node-reconnect" });
455489});
456490457491it("broadcasts canonical Talk capture events for successful PTT node commands", async () => {
@@ -490,28 +524,26 @@ describe("node.invoke APNs wake path", () => {
490524});
491525492526expect(respond.mock.calls[0]?.[0]).toBe(true);
493-expect(broadcast).toHaveBeenCalledWith(
494-"talk.event",
495-expect.objectContaining({
496-nodeId: "android-talk-node",
497-command: "talk.ptt.start",
498-talkEvent: expect.objectContaining({
499-type: "capture.started",
500-sessionId: "node:android-talk-node:talk:capture-1",
501-captureId: "capture-1",
502-seq: expect.any(Number),
503-mode: "stt-tts",
504-transport: "managed-room",
505-brain: "agent-consult",
506-final: false,
507-payload: expect.objectContaining({
508-nodeId: "android-talk-node",
509-command: "talk.ptt.start",
510-}),
511-}),
512-}),
513-{ dropIfSlow: true },
514-);
527+expect(broadcast.mock.calls[0]?.[0]).toBe("talk.event");
528+const broadcastPayload = expectRecordFields(broadcast.mock.calls[0]?.[1], "broadcast payload", {
529+nodeId: "android-talk-node",
530+command: "talk.ptt.start",
531+});
532+const talkEvent = expectRecordFields(broadcastPayload.talkEvent, "talk event", {
533+type: "capture.started",
534+sessionId: "node:android-talk-node:talk:capture-1",
535+captureId: "capture-1",
536+mode: "stt-tts",
537+transport: "managed-room",
538+brain: "agent-consult",
539+final: false,
540+});
541+expect(talkEvent.seq).toBeTypeOf("number");
542+expectRecordFields(talkEvent.payload, "talk event payload", {
543+nodeId: "android-talk-node",
544+command: "talk.ptt.start",
545+});
546+expect(broadcast.mock.calls[0]?.[2]).toEqual({ dropIfSlow: true });
515547});
516548517549it("clears stale registrations after an invalid device token wake failure", async () => {
@@ -525,7 +557,7 @@ describe("node.invoke APNs wake path", () => {
525557mocks.shouldClearStoredApnsRegistration.mockReturnValue(true);
526558const wake = await maybeWakeNodeWithApns("ios-node-stale", { force: true });
527559528-expect(wake).toMatchObject({
560+expectRecordFields(wake, "wake result", {
529561available: true,
530562throttled: false,
531563path: "send-error",
@@ -548,7 +580,7 @@ describe("node.invoke APNs wake path", () => {
548580mocks.shouldClearStoredApnsRegistration.mockReturnValue(false);
549581const wake = await maybeWakeNodeWithApns("ios-node-relay", { force: true });
550582551-expect(wake).toMatchObject({
583+expectRecordFields(wake, "wake result", {
552584available: true,
553585throttled: false,
554586path: "send-error",
@@ -632,32 +664,27 @@ describe("node.invoke APNs wake path", () => {
632664633665const pullRespond = await pullPending("ios-node-queued", ["canvas.navigate"]);
634666const pullCall = pullRespond.mock.calls[0] as RespondCall | undefined;
635-expect(pullCall?.[0]).toBe(true);
636-expect(pullCall?.[1]).toMatchObject({
667+const pullPayload = requireRespondPayload(pullCall, "pull response");
668+expectRecordFields(pullPayload, "pull payload", {
637669nodeId: "ios-node-queued",
638-actions: [
639-expect.objectContaining({
640-command: "canvas.navigate",
641-paramsJSON: JSON.stringify({ url: "http://example.com/" }),
642-}),
643-],
670+});
671+expectQueuedAction(pullPayload, {
672+command: "canvas.navigate",
673+paramsJSON: JSON.stringify({ url: "http://example.com/" }),
644674});
645675646676const repeatedPullRespond = await pullPending("ios-node-queued", ["canvas.navigate"]);
647677const repeatedPullCall = repeatedPullRespond.mock.calls[0] as RespondCall | undefined;
648-expect(repeatedPullCall?.[0]).toBe(true);
649-expect(repeatedPullCall?.[1]).toMatchObject({
678+const repeatedPullPayload = requireRespondPayload(repeatedPullCall, "repeated pull response");
679+expectRecordFields(repeatedPullPayload, "repeated pull payload", {
650680nodeId: "ios-node-queued",
651-actions: [
652-expect.objectContaining({
653-command: "canvas.navigate",
654-paramsJSON: JSON.stringify({ url: "http://example.com/" }),
655-}),
656-],
681+});
682+expectQueuedAction(repeatedPullPayload, {
683+command: "canvas.navigate",
684+paramsJSON: JSON.stringify({ url: "http://example.com/" }),
657685});
658686659-const queuedActionId = (pullCall?.[1] as { actions?: Array<{ id?: string }> } | undefined)
660-?.actions?.[0]?.id;
687+const queuedActionId = (pullPayload.actions as Array<{ id?: string }> | undefined)?.[0]?.id;
661688expect(queuedActionId).toEqual(
662689expect.stringMatching(
663690/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,
@@ -669,20 +696,22 @@ describe("node.invoke APNs wake path", () => {
669696670697const ackRespond = await ackPending("ios-node-queued", [queuedActionId], ["canvas.navigate"]);
671698const ackCall = ackRespond.mock.calls[0] as RespondCall | undefined;
672-expect(ackCall?.[0]).toBe(true);
673-expect(ackCall?.[1]).toMatchObject({
699+expectRecordFields(requireRespondPayload(ackCall, "ack response"), "ack payload", {
674700nodeId: "ios-node-queued",
675701ackedIds: [queuedActionId],
676702remainingCount: 0,
677703});
678704679705const emptyPullRespond = await pullPending("ios-node-queued", ["canvas.navigate"]);
680706const emptyPullCall = emptyPullRespond.mock.calls[0] as RespondCall | undefined;
681-expect(emptyPullCall?.[0]).toBe(true);
682-expect(emptyPullCall?.[1]).toMatchObject({
683-nodeId: "ios-node-queued",
684-actions: [],
685-});
707+expectRecordFields(
708+requireRespondPayload(emptyPullCall, "empty pull response"),
709+"empty pull payload",
710+{
711+nodeId: "ios-node-queued",
712+actions: [],
713+},
714+);
686715});
687716688717it("drops queued actions that are no longer allowed at pull time", async () => {
@@ -731,23 +760,20 @@ describe("node.invoke APNs wake path", () => {
731760"canvas.navigate",
732761]);
733762const preChangePullCall = preChangePullRespond.mock.calls[0] as RespondCall | undefined;
734-expect(preChangePullCall?.[0]).toBe(true);
735-expect(preChangePullCall?.[1]).toMatchObject({
763+const preChangePayload = requireRespondPayload(preChangePullCall, "pre-change pull response");
764+expectRecordFields(preChangePayload, "pre-change pull payload", {
736765nodeId: "ios-node-policy",
737-actions: [
738-expect.objectContaining({
739-command: "camera.snap",
740-paramsJSON: JSON.stringify({ facing: "front" }),
741-}),
742-],
766+});
767+expectQueuedAction(preChangePayload, {
768+command: "camera.snap",
769+paramsJSON: JSON.stringify({ facing: "front" }),
743770});
744771745772allowlistedCommands.delete("camera.snap");
746773747774const pullRespond = await pullPending("ios-node-policy", ["camera.snap", "canvas.navigate"]);
748775const pullCall = pullRespond.mock.calls[0] as RespondCall | undefined;
749-expect(pullCall?.[0]).toBe(true);
750-expect(pullCall?.[1]).toMatchObject({
776+expectRecordFields(requireRespondPayload(pullCall, "pull response"), "pull payload", {
751777nodeId: "ios-node-policy",
752778actions: [],
753779});
@@ -792,17 +818,13 @@ describe("node.invoke APNs wake path", () => {
792818793819const pullRespond = await pullPending("ios-node-dedupe", ["canvas.navigate"]);
794820const pullCall = pullRespond.mock.calls[0] as RespondCall | undefined;
795-expect(pullCall?.[0]).toBe(true);
796-expect(pullCall?.[1]).toMatchObject({
821+const pullPayload = requireRespondPayload(pullCall, "pull response");
822+expectRecordFields(pullPayload, "pull payload", {
797823nodeId: "ios-node-dedupe",
798-actions: [
799-expect.objectContaining({
800-command: "canvas.navigate",
801-paramsJSON: JSON.stringify({ url: "http://example.com/first" }),
802-}),
803-],
804-});
805-const actions = (pullCall?.[1] as { actions?: unknown[] } | undefined)?.actions ?? [];
806-expect(actions).toHaveLength(1);
824+});
825+expectQueuedAction(pullPayload, {
826+command: "canvas.navigate",
827+paramsJSON: JSON.stringify({ url: "http://example.com/first" }),
828+});
807829});
808830});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。