Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions desktop/src-tauri/src/commands/desktop_stop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,14 @@ pub async fn receive_desktop_stop(
if managed_agents::placement::desired(&conn, &target.agent)?
.is_some_and(|(host, _)| host == desktop)
{
return StopResult {
let result = StopResult {
target,
request: event.id.to_hex(),
outcome: StopOutcome::Unknown,
}
.sign(&scope.owner_keys)
.map(Some);
.sign(&scope.owner_keys)?;
remote_stop::save_result(&mut conn, &event.id.to_hex(), &result.as_json())?;
return Ok(Some(result));
}
let owned = owned_local(&app, &state, &owner, &target.agent)?;
remote_stop::receive(
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/features/agents/desktopList.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ test("rendered list distinguishes current, partial, unavailable and empty withou
});

test("mounted cache clears both scopes, fences late reads and retains rows on failure", async (t) => {
const originalRaf = globalThis.requestAnimationFrame;
globalThis.requestAnimationFrame = (fn) => setTimeout(fn, 0);
const { JSDOM } = await import("jsdom");
const dom = new JSDOM("<div id='root'></div>", {
url: "https://desktop.test",
Expand Down Expand Up @@ -375,6 +377,7 @@ test("mounted cache clears both scopes, fences late reads and retains rows on fa
"reconnect producer unsubscribed on unmount",
);
t.mock.timers.reset();
globalThis.requestAnimationFrame = originalRaf;
dom.window.close();
}
});
239 changes: 239 additions & 0 deletions desktop/src/features/agents/ui/DesktopLifecycleControl.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import assert from "node:assert/strict";
import test from "node:test";
import React from "react";
import { JSDOM } from "jsdom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
DesktopLifecycleControl,
DesktopLifecycleReceiver,
} from "./DesktopLifecycleControl.tsx";
import { toast } from "sonner";
import { relayClient } from "../../../shared/api/relayClient.ts";

test("mounted Start exposes unavailable provisioning and exact retry; Restart resolves source", async () => {
const dom = new JSDOM("<div id='root'></div>", {
url: "https://desktop.test",
});
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
localStorage: dom.window.localStorage,
IS_REACT_ACT_ENVIRONMENT: true,
});
const { createRoot } = await import("react-dom/client");
const client = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
});
client.setQueryData(
["relay-agents"],
[
{ pubkey: "agent", name: "Owned agent", ownerPubkey: "owner" },
{ pubkey: "foreign", name: "Foreign agent", ownerPubkey: "other" },
],
);
const scope = { owner: "owner", community: "wss://one.example" };
const originals = {
fetch: relayClient.fetchEvents,
publish: relayClient.publishEvent,
};
const prepared = [],
sent = [];
let stop = "failed";
window.__TAURI_INTERNALS__ = {
invoke: async (command, args) => {
assert.equal(args.owner, scope.owner);
assert.equal(args.community, scope.community);
if (command === "observe_desktop_placement") return;
if (command === "read_desktop_placement") return ["source", "selection"];
if (
command === "prepare_desktop_lifecycle" ||
command === "prepare_desktop_stop"
) {
const request = {
id: `request-${prepared.length}`,
kind: command.endsWith("_stop") ? 50180 : 50182,
...args,
};
prepared.push(request);
return request;
}
if (command === "read_desktop_lifecycle_results")
return args.request.action === "status"
? "running"
: "provisioning_unavailable";
if (command === "read_desktop_stop_results") return stop;
throw Error(command);
},
};
relayClient.fetchEvents = async () => [];
relayClient.publishEvent = async (event, _timeout, _failure, check) => {
check();
sent.push(event);
};
const root = createRoot(document.getElementById("root"));
const click = (text) =>
React.act(async () =>
[...document.querySelectorAll("button")]
.find((b) => b.textContent === text)
.click(),
);
const select = (label, value) =>
React.act(async () => {
const element = document.querySelector(`select[aria-label="${label}"]`);
element.value = value;
element.dispatchEvent(new dom.window.Event("change", { bubbles: true }));
});
try {
await React.act(async () =>
root.render(
React.createElement(
QueryClientProvider,
{ client },
React.createElement(DesktopLifecycleControl, {
scope,
desktops: [
{ id: "source", name: "Source" },
{ id: "destination", name: "Destination" },
],
}),
),
),
);
assert.doesNotMatch(document.body.textContent, /Foreign agent/);
await select("Agent to place", "agent");
await select("Destination Desktop", "destination");
await click("Start on destination");
assert.match(
document.body.textContent,
/keyless launch provisioning is unavailable/,
);
assert.equal(prepared[0].desktop, "destination");
await click("Retry same request");
assert.equal(prepared.length, 1);
assert.equal(sent[0], sent[1]);
await click("Restart on current Desktop");
const restart = prepared.at(-1);
assert.equal(
restart.desktop,
"source",
"destination picker must not redirect Restart",
);
assert.equal(restart.action, "restart");
assert.equal(restart.observed, prepared.at(-2).id);
await click("Move to destination");
assert.match(document.body.textContent, /destination was not started/);
const count = prepared.length;
stop = "stopped";
await React.act(async () => {});
assert.equal(prepared.length, count, "late Stop cannot resume failed Move");
assert.doesNotMatch(document.body.textContent, /Retry same request/);
} finally {
await React.act(async () => root.unmount());
client.clear();
relayClient.fetchEvents = originals.fetch;
relayClient.publishEvent = originals.publish;
dom.window.close();
}
});

test("receiver failure is a scope-owned notification, not pre-shell layout", async () => {
const originalRaf = globalThis.requestAnimationFrame;
globalThis.requestAnimationFrame = (fn) => setTimeout(fn, 0);
const dom = new JSDOM("<div id='root'></div>", {
url: "https://desktop.test",
});
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
localStorage: dom.window.localStorage,
IS_REACT_ACT_ENVIRONMENT: true,
});
const { createRoot } = await import("react-dom/client");
const originals = {
fetch: relayClient.fetchEvents,
subscribe: relayClient.subscribeLive,
};
let readiness;
let closed = 0;
let rejectLate;
let delayed = false;
relayClient.fetchEvents = async () => {
if (delayed)
return new Promise((_, reject) => {
rejectLate = reject;
});
return [];
};
relayClient.subscribeLive = async (_filter, _event, onReadiness) => {
readiness = onReadiness;
return () => {
closed++;
};
};
window.__TAURI_INTERNALS__ = {
invoke: async () => {
throw new Error("fixture: storage unavailable");
},
};
const root = createRoot(document.getElementById("root"));
const scope = { owner: "owner", community: "wss://one.example" };
const warnings = () =>
toast
.getToasts()
.filter((t) => String(t.title).startsWith("Desktop lifecycle"));
try {
await React.act(async () =>
root.render(React.createElement(DesktopLifecycleReceiver, { scope })),
);
assert.equal(
document.getElementById("root").childElementCount,
0,
"startup must not render in-flow failure UI",
);
assert.equal(warnings().length, 1);
assert.equal(
warnings()[0].title,
"Desktop lifecycle receiver is unavailable.",
);
assert.equal(warnings()[0].duration, Infinity);
assert.equal(warnings()[0].closeButton, true);
readiness("closed");
assert.equal(
warnings().length,
1,
"repeated failures update one notification",
);
await React.act(async () =>
root.render(
React.createElement(DesktopLifecycleReceiver, { scope: null }),
),
);
assert.equal(warnings().length, 0, "leaving the scope removes its warning");
readiness("closed");
assert.equal(
warnings().length,
0,
"retired receiver cannot notify another scope",
);
delayed = true;
await React.act(async () =>
root.render(React.createElement(DesktopLifecycleReceiver, { scope })),
);
assert.equal(typeof rejectLate, "function");
await React.act(async () => root.unmount());
await React.act(async () => rejectLate(new Error("late failure")));
assert.equal(
warnings().length,
0,
"late startup rejection must not recreate the warning",
);
assert.equal(closed, 2, "both failed subscriptions are released");
} finally {
await React.act(async () => root.unmount());
relayClient.fetchEvents = originals.fetch;
relayClient.subscribeLive = originals.subscribe;
for (const warning of warnings()) toast.dismiss(warning.id);
globalThis.requestAnimationFrame = originalRaf;
dom.window.close();
}
});
Loading
Loading