Skip to content

Commit 8a5d54d

Browse files
committed
test(desktop): cover the runtime pairing decision surface
The browser smoke drives the boot surface through the decision: both choices stay available, nothing installs before one is chosen, the native boot-failure callback cannot overwrite it, escalation never relabels it as an error, the layout holds at phone width, and "update" checks then installs while "use this App's runtime" issues the align action and retires the chooser once services connect. The unit test keeps the pairing evidence bounded and free of private detail. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
1 parent f74cd5b commit 8a5d54d

2 files changed

Lines changed: 137 additions & 2 deletions

File tree

‎examples/desktop-recovery-diagnostics-test.mjs‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,55 @@ test('installer failure is actionable and clipboard denial leaves selectable tex
4141
await elements.get('#copy-diagnostics').onclick();
4242
assert.equal(elements.get('#diagnostics').selected, true);
4343
});
44+
test('a different installed runtime asks before anything is replaced', () => {
45+
const {context, elements} = page();
46+
const decision = {
47+
phase: 'runtime_pairing_required',
48+
details: {
49+
code: 'runtime_pairing_required',
50+
installed_revision: 'a'.repeat(40),
51+
bundled_revision: 'b'.repeat(40),
52+
installed_identity_available: true,
53+
revision_matches: false,
54+
},
55+
};
56+
runInNewContext('render(packet)', Object.assign(context, {packet: decision}));
57+
assert.equal(elements.get('#pairing').hidden, false);
58+
assert.equal(elements.get('#pairing-installed').textContent, 'a'.repeat(12));
59+
assert.equal(elements.get('#pairing-bundled').textContent, 'b'.repeat(12));
60+
assert.match(elements.get('#pairing-status').textContent, /本地服务需要两者一致/);
61+
// Neither choice is a background install: both stay available and neither
62+
// runs before the operator picks one.
63+
assert.equal(elements.get('#pairing-update').disabled, false);
64+
assert.equal(elements.get('#pairing-align').disabled, false);
65+
// The chooser stays up while the chosen action runs, and retires only when
66+
// services connect.
67+
runInNewContext('render({phase:"installing_runtime",details:{}})', context);
68+
assert.equal(elements.get('#pairing').hidden, false);
69+
runInNewContext('render({phase:"connecting",details:{service:"chat"}})', context);
70+
assert.equal(elements.get('#pairing').hidden, true);
71+
});
72+
test('the pairing decision reaches diagnostics without private detail', () => {
73+
const {context, elements} = page();
74+
context.packet = {
75+
app_version: '1.0.5',
76+
state: {phase: 'runtime_pairing_required', details: {code: 'runtime_pairing_required'}},
77+
last_failure: {
78+
phase: 'runtime_pairing_required',
79+
details: {
80+
code: 'runtime_pairing_required',
81+
installed_revision: 'a'.repeat(40),
82+
bundled_revision: 'b'.repeat(40),
83+
installed_identity_available: true,
84+
revision_matches: false,
85+
path: 'PRIVATE',
86+
},
87+
},
88+
};
89+
runInNewContext('renderDiagnostics(packet)', context);
90+
const value = JSON.parse(elements.get('#diagnostics').value);
91+
assert.equal(value.error_code, 'runtime_pairing_required');
92+
assert.equal(value.failure_phase, 'runtime_pairing_required');
93+
assert.equal(value.revision_matches, false);
94+
assert.ok(!JSON.stringify(value).includes('PRIVATE'));
95+
});

‎examples/desktop-update-browser-smoke.mjs‎

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ try {
3131
let startupTiming = null;
3232
let failUpdate = false;
3333
let checkFailure = null;
34+
let checkUpToDate = false;
3435
let statusFailure = false;
3536
let environmentTelemetry = null;
3637
await page.exposeFunction("nativeInvoke", async (command, args) => {
@@ -40,9 +41,16 @@ try {
4041
}
4142
calls.push({ command, args });
4243
if (checkFailure && args.action === "check") return { phase: "error", details: { code: checkFailure } };
44+
if (checkUpToDate && args.action === "check") {
45+
nativeState = { phase: "up_to_date", details: { channel: args.channel } };
46+
return nativeState;
47+
}
4348
if (failUpdate) throw new Error("private diagnostic must not be displayed");
4449
await new Promise((done) => setTimeout(done, 150));
45-
nativeState = { phase: args.action === "check" ? "available" : "restart_required", details: { version: "0.5.5", channel: args.channel } };
50+
nativeState = {
51+
phase: args.action === "check" ? "available" : args.action === "align_runtime" ? "connecting" : "restart_required",
52+
details: { version: "0.5.5", channel: args.channel },
53+
};
4654
return nativeState;
4755
});
4856
await page.addInitScript(() => {
@@ -232,6 +240,81 @@ try {
232240
nativeState = { phase: "runtime_required", details: { code: "runtime_setup_required" } };
233241
await page.waitForFunction(() => document.querySelector("main").dataset.state === "error" && document.querySelector("#status").innerText.includes("请修复当前版本,成功后重启"));
234242
environmentTelemetry = null;
243+
// A different installed CLI runtime is the operator's decision, shown on the
244+
// first screen with both choices: update the App and runtime together, or
245+
// replace the CLI runtime with this App's bundled snapshot. Nothing is
246+
// installed until one of them is chosen.
247+
const pairingState = { phase: "runtime_pairing_required", details: {
248+
code: "runtime_pairing_required", app_version: "1.0.5",
249+
installed_revision: "a".repeat(40), bundled_revision: "b".repeat(40),
250+
installed_identity_available: true, revision_matches: false,
251+
} };
252+
nativeState = pairingState;
253+
await page.reload();
254+
const pairingPanel = page.locator("#pairing");
255+
await pairingPanel.waitFor({ state: "visible" });
256+
assert.equal(await startupPanel.getAttribute("data-state"), "decision");
257+
assert.equal(await startupPanel.getAttribute("aria-busy"), "false");
258+
assert.equal(await startupDots.isVisible(), false);
259+
assert.equal(await page.locator("#status").innerText(), "需要你选择 App 与 CLI 运行时的对齐方式");
260+
assert.equal(await page.locator("#pairing-installed").innerText(), "aaaaaaaaaaaa");
261+
assert.equal(await page.locator("#pairing-bundled").innerText(), "bbbbbbbbbbbb");
262+
assert.ok((await page.locator("#pairing-status").innerText()).includes("本地服务需要两者一致"));
263+
assert.equal(await page.getByRole("button", { name: /升级:更新 App 与运行时/ }).isEnabled(), true);
264+
assert.equal(await page.getByRole("button", { name: /回退 CLI:改用本 App 自带运行时/ }).isEnabled(), true);
265+
// A decision never escalates into the error projection, and the native boot
266+
// failure callback cannot overwrite it.
267+
await page.evaluate(() => window.loopxBootFailed("启动失败,请重试。"));
268+
await page.waitForTimeout(2600);
269+
assert.equal(await startupPanel.getAttribute("data-state"), "decision");
270+
assert.equal(await pairingPanel.isVisible(), true);
271+
assert.equal(await page.locator("#status").innerText(), "需要你选择 App 与 CLI 运行时的对齐方式");
272+
await page.screenshot({ path: resolve(output, "startup-pairing-decision.png") });
273+
await page.emulateMedia({ colorScheme: "dark" });
274+
await page.screenshot({ path: resolve(output, "startup-pairing-decision-dark.png") });
275+
await page.emulateMedia({ colorScheme: "light" });
276+
await page.setViewportSize({ width: 390, height: 844 });
277+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), "the decision must fit a phone-width window");
278+
const upgradeBox = await page.getByRole("button", { name: /升级:更新 App 与运行时/ }).boundingBox();
279+
const alignBox = await page.getByRole("button", { name: /回退 CLI:改用本 App 自带运行时/ }).boundingBox();
280+
assert.ok(upgradeBox && alignBox && upgradeBox.y + upgradeBox.height <= alignBox.y + 1, "both choices stay visible and ordered");
281+
await page.screenshot({ path: resolve(output, "startup-pairing-decision-mobile.png") });
282+
await page.setViewportSize({ width: 1280, height: 900 });
283+
284+
// "Upgrade" is one intent: check this App's channel, then continue into the
285+
// verified install instead of asking for a second click.
286+
calls.length = 0;
287+
await page.getByRole("button", { name: /升级:更新 App 与运行时/ }).click();
288+
await page.waitForFunction(() => document.querySelector("#pairing-status").textContent.includes("重启"));
289+
assert.deepEqual(calls.map((call) => call.args?.action), ["check", "apply"], "upgrade must check then install");
290+
assert.deepEqual(calls.at(-1).args.channel, "stable");
291+
assert.equal(await pairingPanel.isVisible(), true, "the install outcome stays on the decision surface");
292+
293+
// A channel with nothing newer says so and leaves the CLI choice standing.
294+
nativeState = pairingState;
295+
await page.reload();
296+
await pairingPanel.waitFor({ state: "visible" });
297+
calls.length = 0;
298+
checkUpToDate = true;
299+
await page.getByRole("button", { name: /升级:更新 App 与运行时/ }).click();
300+
await page.waitForFunction(() => document.querySelector("#pairing-status").textContent.includes("没有更新的 App 构建"));
301+
assert.deepEqual(calls.map((call) => call.args?.action), ["check"], "a channel without a newer build must not install anything");
302+
assert.equal(await pairingPanel.isVisible(), true);
303+
checkUpToDate = false;
304+
305+
// "Roll back the CLI" installs this App's snapshot and reconnects the same
306+
// window; the chooser retires as soon as services connect.
307+
nativeState = pairingState;
308+
await page.reload();
309+
await pairingPanel.waitFor({ state: "visible" });
310+
calls.length = 0;
311+
await page.getByRole("button", { name: /回退 CLI:改用本 App 自带运行时/ }).click();
312+
await page.getByText("正在连接本地服务", { exact: true }).waitFor();
313+
assert.deepEqual(calls.map((call) => call.args?.action), ["align_runtime"]);
314+
assert.equal(await pairingPanel.isVisible(), false);
315+
nativeState = { phase: "connecting", details: { service: "chat" } };
316+
await page.getByText("正在连接管家对话服务", { exact: true }).waitFor();
317+
235318
// Production boot surface must expose native installation and service
236319
// stages even while recovery is collapsed; reload keeps native elapsed time.
237320
nativeState = { phase: "installing_runtime", details: {} };
@@ -249,7 +332,7 @@ try {
249332
await page.getByText(/启动用时较长/).waitFor();
250333
nativeState = { phase: "service_error", details: { code: "service_start_failed" } };
251334
await page.getByText("本地服务连接失败,正在等待重试", { exact: true }).waitFor();
252-
console.log("desktop-update-browser-smoke: passed (confirmation, failure redaction, mobile, missing assets + reload, startup motion states, startup recovery, startup error escalation)");
335+
console.log("desktop-update-browser-smoke: passed (confirmation, failure redaction, mobile, missing assets + reload, startup motion states, startup recovery, startup error escalation, runtime pairing decision)");
253336
} finally {
254337
await browser.close();
255338
await new Promise((done) => server.close(done));

0 commit comments

Comments
 (0)