fix: preserve gateway presence instanceId
This commit is contained in:
@@ -220,4 +220,36 @@ describe("subscribeEmbeddedPiSession", () => {
|
|||||||
expect(payloads).toHaveLength(1);
|
expect(payloads).toHaveLength(1);
|
||||||
expect(payloads[0]?.text).toBe("MEDIA:");
|
expect(payloads[0]?.text).toBe("MEDIA:");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits agent events when media arrives without text", () => {
|
||||||
|
let handler: ((evt: unknown) => void) | undefined;
|
||||||
|
const session: StubSession = {
|
||||||
|
subscribe: (fn) => {
|
||||||
|
handler = fn;
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAgentEvent = vi.fn();
|
||||||
|
|
||||||
|
subscribeEmbeddedPiSession({
|
||||||
|
session: session as unknown as Parameters<typeof subscribeEmbeddedPiSession>[0]["session"],
|
||||||
|
runId: "run",
|
||||||
|
onAgentEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
handler?.({ type: "message_start", message: { role: "assistant" } });
|
||||||
|
handler?.({
|
||||||
|
type: "message_update",
|
||||||
|
message: { role: "assistant" },
|
||||||
|
assistantMessageEvent: { type: "text_delta", delta: "MEDIA: https://example.com/a.png" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const payloads = onAgentEvent.mock.calls
|
||||||
|
.map((call) => call[0]?.data as Record<string, unknown> | undefined)
|
||||||
|
.filter((value): value is Record<string, unknown> => Boolean(value));
|
||||||
|
expect(payloads).toHaveLength(1);
|
||||||
|
expect(payloads[0]?.text).toBe("");
|
||||||
|
expect(payloads[0]?.mediaUrls).toEqual(["https://example.com/a.png"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -156,10 +156,12 @@ export class GatewayClient {
|
|||||||
const signedAtMs = Date.now();
|
const signedAtMs = Date.now();
|
||||||
const role = this.opts.role ?? "operator";
|
const role = this.opts.role ?? "operator";
|
||||||
const scopes = this.opts.scopes ?? ["operator.admin"];
|
const scopes = this.opts.scopes ?? ["operator.admin"];
|
||||||
const device = (() => {
|
const identity = this.opts.deviceIdentity;
|
||||||
if (!this.opts.deviceIdentity) return undefined;
|
if (!identity) {
|
||||||
|
throw new Error("gateway client requires a device identity");
|
||||||
|
}
|
||||||
const payload = buildDeviceAuthPayload({
|
const payload = buildDeviceAuthPayload({
|
||||||
deviceId: this.opts.deviceIdentity.deviceId,
|
deviceId: identity.deviceId,
|
||||||
clientId: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
clientId: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||||
clientMode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND,
|
clientMode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND,
|
||||||
role,
|
role,
|
||||||
@@ -167,14 +169,13 @@ export class GatewayClient {
|
|||||||
signedAtMs,
|
signedAtMs,
|
||||||
token: this.opts.token ?? null,
|
token: this.opts.token ?? null,
|
||||||
});
|
});
|
||||||
const signature = signDevicePayload(this.opts.deviceIdentity.privateKeyPem, payload);
|
const signature = signDevicePayload(identity.privateKeyPem, payload);
|
||||||
return {
|
const device = {
|
||||||
id: this.opts.deviceIdentity.deviceId,
|
id: identity.deviceId,
|
||||||
publicKey: publicKeyRawBase64UrlFromPem(this.opts.deviceIdentity.publicKeyPem),
|
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
|
||||||
signature,
|
signature,
|
||||||
signedAt: signedAtMs,
|
signedAt: signedAtMs,
|
||||||
};
|
};
|
||||||
})();
|
|
||||||
const params: ConnectParams = {
|
const params: ConnectParams = {
|
||||||
minProtocol: this.opts.minProtocol ?? PROTOCOL_VERSION,
|
minProtocol: this.opts.minProtocol ?? PROTOCOL_VERSION,
|
||||||
maxProtocol: this.opts.maxProtocol ?? PROTOCOL_VERSION,
|
maxProtocol: this.opts.maxProtocol ?? PROTOCOL_VERSION,
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ import {
|
|||||||
type SkillsBinsParams,
|
type SkillsBinsParams,
|
||||||
SkillsBinsParamsSchema,
|
SkillsBinsParamsSchema,
|
||||||
type SkillsBinsResult,
|
type SkillsBinsResult,
|
||||||
SkillsBinsResultSchema,
|
|
||||||
type SkillsInstallParams,
|
type SkillsInstallParams,
|
||||||
SkillsInstallParamsSchema,
|
SkillsInstallParamsSchema,
|
||||||
type SkillsStatusParams,
|
type SkillsStatusParams,
|
||||||
|
|||||||
@@ -57,10 +57,11 @@ describe("gateway node command allowlist", () => {
|
|||||||
await connectOk(nodeWs, {
|
await connectOk(nodeWs, {
|
||||||
role: "node",
|
role: "node",
|
||||||
client: {
|
client: {
|
||||||
id: "node-empty",
|
id: GATEWAY_CLIENT_NAMES.NODE_HOST,
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
platform: "ios",
|
platform: "ios",
|
||||||
mode: GATEWAY_CLIENT_MODES.NODE,
|
mode: GATEWAY_CLIENT_MODES.NODE,
|
||||||
|
instanceId: "node-empty",
|
||||||
},
|
},
|
||||||
commands: [],
|
commands: [],
|
||||||
});
|
});
|
||||||
@@ -92,10 +93,11 @@ describe("gateway node command allowlist", () => {
|
|||||||
await connectOk(nodeWs, {
|
await connectOk(nodeWs, {
|
||||||
role: "node",
|
role: "node",
|
||||||
client: {
|
client: {
|
||||||
id: "node-allowed",
|
id: GATEWAY_CLIENT_NAMES.NODE_HOST,
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
platform: "ios",
|
platform: "ios",
|
||||||
mode: GATEWAY_CLIENT_MODES.NODE,
|
mode: GATEWAY_CLIENT_MODES.NODE,
|
||||||
|
instanceId: "node-allowed",
|
||||||
},
|
},
|
||||||
commands: ["canvas.snapshot"],
|
commands: ["canvas.snapshot"],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (device && devicePublicKey) {
|
if (device && devicePublicKey) {
|
||||||
const requirePairing = async (reason: string, paired?: { deviceId: string }) => {
|
const requirePairing = async (reason: string, _paired?: { deviceId: string }) => {
|
||||||
const pairing = await requestDevicePairing({
|
const pairing = await requestDevicePairing({
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
publicKey: devicePublicKey,
|
publicKey: devicePublicKey,
|
||||||
@@ -445,11 +445,7 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
} else {
|
} else {
|
||||||
const allowedRoles = new Set(
|
const allowedRoles = new Set(
|
||||||
Array.isArray(paired.roles)
|
Array.isArray(paired.roles) ? paired.roles : paired.role ? [paired.role] : [],
|
||||||
? paired.roles
|
|
||||||
: paired.role
|
|
||||||
? [paired.role]
|
|
||||||
: [],
|
|
||||||
);
|
);
|
||||||
if (allowedRoles.size === 0) {
|
if (allowedRoles.size === 0) {
|
||||||
const ok = await requirePairing("role-upgrade", paired);
|
const ok = await requirePairing("role-upgrade", paired);
|
||||||
@@ -532,7 +528,7 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
deviceFamily: connectParams.client.deviceFamily,
|
deviceFamily: connectParams.client.deviceFamily,
|
||||||
modelIdentifier: connectParams.client.modelIdentifier,
|
modelIdentifier: connectParams.client.modelIdentifier,
|
||||||
mode: connectParams.client.mode,
|
mode: connectParams.client.mode,
|
||||||
instanceId: connectParams.device?.id ?? instanceId,
|
instanceId: instanceId ?? connectParams.device?.id,
|
||||||
reason: "connect",
|
reason: "connect",
|
||||||
});
|
});
|
||||||
incrementPresenceVersion();
|
incrementPresenceVersion();
|
||||||
|
|||||||
@@ -35,4 +35,24 @@ describe("agent-events sequencing", () => {
|
|||||||
expect(seen["run-1"]).toEqual([1, 2, 3]);
|
expect(seen["run-1"]).toEqual([1, 2, 3]);
|
||||||
expect(seen["run-2"]).toEqual([1]);
|
expect(seen["run-2"]).toEqual([1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("preserves compaction ordering on the event bus", async () => {
|
||||||
|
const phases: Array<string> = [];
|
||||||
|
const stop = onAgentEvent((evt) => {
|
||||||
|
if (evt.runId !== "run-1") return;
|
||||||
|
if (evt.stream !== "compaction") return;
|
||||||
|
if (typeof evt.data?.phase === "string") phases.push(evt.data.phase);
|
||||||
|
});
|
||||||
|
|
||||||
|
emitAgentEvent({ runId: "run-1", stream: "compaction", data: { phase: "start" } });
|
||||||
|
emitAgentEvent({
|
||||||
|
runId: "run-1",
|
||||||
|
stream: "compaction",
|
||||||
|
data: { phase: "end", willRetry: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
stop();
|
||||||
|
|
||||||
|
expect(phases).toEqual(["start", "end"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user