Files
story/device-simulator-windows/electron/hardware-agent-client.test.cjs
T

120 lines
6.2 KiB
JavaScript
Raw Normal View History

2026-09-06 17:04:11 +08:00
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const test = require("node:test");
const {
HardwareAgentClient,
buildSessionUpdate,
websocketSignature,
} = require("./hardware-agent-client.cjs");
test("WebSocket signatures use the documented canonical field order", () => {
const connection = websocketSignature({ authType: 1, deviceName: "device-1", randomNumber: 42, productKey: "product-1", timestamp: 1700000000, instanceID: "instance-1" }, "device-secret");
assert.equal(connection, "6OFE1DUZzRgNy0o5IzZSmqKw3SUP4s3GxK2YHe8phfo=");
});
test("LLM session update uses the official Config/AgentConfig schema", () => {
const update = buildSessionUpdate({ dialog_id: "conversation-1", system_prompt: "你是陪伴助手", persona_tone: "亲切", authorized_context: "只可使用已授权背景" }, "event-1");
assert.equal(update.type, "session.update");
assert.equal(update.session.object, "realtime.session");
assert.equal(update.session.input_audio_format, "pcm16");
assert.equal(update.session.output_audio_format, "pcm16");
assert.equal(update.session.config.ASRConfig.TurnDetectionMode, 0);
assert.match(update.session.config.LLMConfig.SystemMessages[0], /只可使用已授权背景/);
assert.match(update.session.config.LLMConfig.SystemMessages[0], /你是陪伴助手/);
assert.match(update.session.config.LLMConfig.SystemMessages[0], /亲切/);
assert.match(update.session.config.LLMConfig.SystemMessages[0], /每轮最多问一个问题/);
assert.match(update.session.config.LLMConfig.SystemMessages[0], /先回应一个具体细节/);
assert.doesNotMatch(update.session.config.LLMConfig.SystemMessages[0], /本次对话标识/);
assert.doesNotMatch(update.session.config.LLMConfig.SystemMessages[0], /conversation-1/);
2026-09-06 17:04:11 +08:00
assert.equal(update.session.agent_config.EnableConversationStateCallback, false);
assert.equal("system_prompt" in update.session, false);
});
test("S2S session update maps persona and dialog id to provider dialog config", () => {
const update = buildSessionUpdate({ dialog_id: "conversation-2", system_prompt: "你是陪伴助手", persona_tone: "亲切自然", authorized_context: "只使用授权信息" }, "event-2", "s2s");
const dialog = update.session.config.S2SConfig.ProviderParams.dialog;
assert.equal(dialog.dialog_id, "conversation-2");
assert.equal(dialog.speaking_style, "亲切自然");
assert.match(dialog.system_role, /只使用授权信息/);
assert.match(dialog.system_role, /你是陪伴助手/);
2026-09-06 17:27:11 +08:00
assert.match(update.session.config.LLMConfig.SystemMessages[0], /你是陪伴助手/);
});
test("session update maps the bound user to native Viking memory without credentials", () => {
const update = buildSessionUpdate({
dialog_id: "conversation-memory", model_mode: "s2s", authorized_context: "authorized",
memory: {
collection_name: "memory", user_id: "u_subject_hash", assistant_id: "platform_companion_v1",
memory_types: ["event_v1", "profile_v1"], limit: 5, transition_words: "自然承接历史记忆",
},
}, "event-memory");
const memory = update.session.config.MemoryConfig;
assert.equal(memory.Enable, true);
assert.equal(memory.Provider, "volc");
assert.equal(memory.ProviderParams.collection_name, "memory");
assert.deepEqual(memory.ProviderParams.filter.user_id, ["u_subject_hash"]);
assert.deepEqual(memory.ProviderParams.filter.assistant_id, ["platform_companion_v1"]);
assert.deepEqual(memory.ProviderParams.filter.memory_type, ["event_v1", "profile_v1"]);
assert.equal("token" in memory.ProviderParams, false);
2026-09-06 17:04:11 +08:00
});
class FakeWebSocket extends EventEmitter {
static OPEN = 1;
constructor(url, options) {
super();
this.url = url;
this.options = options;
this.readyState = 0;
this.sent = [];
FakeWebSocket.last = this;
setImmediate(() => {
this.readyState = FakeWebSocket.OPEN;
this.emit("open");
this.emit("message", Buffer.from(JSON.stringify({ type: "session.created", event_id: "created-event", session: { id: "provider-session-1" } })));
});
}
send(body) {
const value = JSON.parse(body);
this.sent.push(value);
if (value.type === "session.update") {
setImmediate(() => this.emit("message", Buffer.from(JSON.stringify({ type: "session.updated", event_id: "updated-event", session: { id: "provider-session-1" } }))));
}
}
ping() {}
close(code, reason) {
this.readyState = 3;
this.emit("close", code, Buffer.from(reason));
}
}
test("hardware client waits for dynamic config acknowledgement before becoming ready", async () => {
const events = [];
const client = new HardwareAgentClient({ WebSocketImpl: FakeWebSocket, gatewayURL: "ws://127.0.0.1:19090/v1/realtime", emit: (event) => events.push(event) });
const connected = await client.connect({
instanceID: "instance-1", productKey: "product-1", deviceName: "device-1",
deviceSecret: "device-secret-with-at-least-32-characters", hardwareID: "windows-test", botID: "bot-1",
sessionConfig: { dialog_id: "conversation-1", authorized_context: "authorized" },
});
assert.equal(connected.providerConnectionRef, "provider-session-1");
assert.match(FakeWebSocket.last.url, /bot=bot-1/);
assert.equal(FakeWebSocket.last.options.headers["X-Product-Key"], "product-1");
assert.equal(FakeWebSocket.last.sent[0].type, "session.update");
client.sendAudio(Buffer.from([1, 2, 3, 4]));
client.sendText("你好");
assert.equal(FakeWebSocket.last.sent[1].type, "input_audio_buffer.append");
assert.equal(FakeWebSocket.last.sent[2].type, "conversation.item.create");
assert.ok(events.some((event) => event.type === "session.created"));
client.close();
});
test("hardware client sends session.update immediately when the WebSocket opens", async () => {
const client = new HardwareAgentClient({ WebSocketImpl: FakeWebSocket, gatewayURL: "ws://127.0.0.1:19090/v1/realtime" });
await client.connect({
instanceID: "instance-1", productKey: "product-1", deviceName: "device-1",
deviceSecret: "device-secret-with-at-least-32-characters", hardwareID: "windows-test", botID: "bot-1",
sessionConfig: { dialog_id: "conversation-1", authorized_context: "authorized" },
});
assert.equal(FakeWebSocket.last.sent.filter((event) => event.type === "session.update").length, 1);
client.close();
});