Skip to content
Merged
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
2 changes: 1 addition & 1 deletion desktop/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"test:bench": "PLAYWRIGHT_BROWSERS_PATH=.pw-browsers node bench/run.mjs",
"test:typecheck": "tsc --noEmit -p tsconfig.test.json",
"test:isolated-worktree": "tsx src/__tests__/isolated-worktree.test.ts",
"test:updater": "tsx src/__tests__/updater-shared-state.test.tsx",
"test:updater": "tsx src/__tests__/updater-scheduling.test.tsx && tsx src/__tests__/updater-shared-state.test.tsx && node --import ./scripts/css-stub-register.mjs --import tsx src/__tests__/about-settings-updater-capability.test.tsx",
"test:window-state": "tsx src/__tests__/window-state-ordering.test.ts",
"test:all": "pnpm test:typecheck && pnpm test:updater && pnpm test:window-state && pnpm test && pnpm test:remote && pnpm test:performance",
"test:app-lifecycle": "tsx src/__tests__/app-lifecycle.test.tsx && tsx src/__tests__/committed-command-lifecycle.test.tsx && tsx src/__tests__/committed-command-execution.test.tsx && tsx src/__tests__/navigation-surface-lifecycle.test.tsx && tsx src/__tests__/app-lifecycle-probe.test.ts && tsx src/__tests__/subscription-scope.test.ts && tsx src/__tests__/composer-source-operations.test.tsx && tsx src/__tests__/session-prompt-lifecycle.test.tsx && tsx src/__tests__/desktop-preferences-lifecycle.test.tsx && tsx src/__tests__/onboarding-commands.test.tsx && tsx src/__tests__/topicbar-actions-lifecycle.test.tsx && tsx src/__tests__/decision-slots-lifecycle.test.tsx && tsx src/__tests__/session-experience-settings.test.tsx && tsx src/__tests__/project-topic-lifecycle.test.tsx && tsx src/__tests__/conversation-projection.test.ts && tsx src/__tests__/remote-composer-presentation.test.tsx && tsx src/__tests__/remote-composer-commands.test.tsx && tsx src/__tests__/terminal-panel-commands.test.tsx && tsx src/__tests__/workspace-panel-commands.test.tsx && tsx src/__tests__/desktop-navigation-lifecycle.test.tsx && tsx src/__tests__/runtime-status-lifecycle.test.tsx && tsx src/__tests__/session-control-commands.test.ts && tsx src/__tests__/automation-navigation-lifecycle.test.tsx && tsx src/__tests__/mock-remote-catalog.test.ts && node --import ./scripts/svg-stub-register.mjs --import tsx src/__tests__/topicbar-region.test.tsx && tsx src/__tests__/controller-profile-lifecycle.test.tsx && tsx src/__tests__/session-submission-lifecycle.test.tsx && tsx src/__tests__/pending-plan-revision-lifecycle.test.tsx && tsx src/__tests__/session-undo-lifecycle.test.tsx && tsx src/__tests__/session-clear-commands.test.tsx && tsx src/__tests__/turn-verification-commands.test.tsx && tsx src/__tests__/delivery-continue-commands.test.tsx && tsx src/__tests__/active-tab-mirror.test.tsx && tsx src/__tests__/windows-maximised-sync.test.tsx && tsx src/__tests__/topic-summary-commands.test.tsx && tsx src/__tests__/worktree-merge-commands.test.tsx && tsx src/__tests__/composer-insert-commands.test.tsx && node --test bench/app-memory-evidence.test.mjs && node --test bench/app-memory-shards.test.mjs bench/app-memory-paths.test.mjs && node --test scripts/check-app-layers.test.mjs && node --test scripts/check-desktop-host-boundary.test.mjs && node --test scripts/shell-css.test.mjs && node --test scripts/check-css-syntax.test.mjs",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { SettingsPanel } from "../components/SettingsPanel";
import { LocaleProvider } from "../lib/i18n";
import { UpdaterProvider } from "../lib/useUpdater";
import { baseSettings, flushPromises } from "../test-support/settingsTestFixtures";
import { installDesktopHostStub } from "./desktopHostStub";

const dom = new JSDOM("<!doctype html><div id='root'></div>", { url: "http://localhost", pretendToBeVisual: true });
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
HTMLElement: dom.window.HTMLElement,
Node: dom.window.Node,
Event: dom.window.Event,
CustomEvent: dom.window.CustomEvent,
localStorage: dom.window.localStorage,
sessionStorage: dom.window.sessionStorage,
IS_REACT_ACT_ENVIRONMENT: true,
});
Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
window.matchMedia = () => ({ matches: true, addEventListener() {}, removeEventListener() {} }) as unknown as MediaQueryList;
window.scrollTo = () => {};

let settings = baseSettings("standard");
let applyCalls = 0;
const desktopStub = installDesktopHostStub({
Settings: async () => settings,
Version: async () => "v1.2.3",
CheckUpdate: async () => ({ available: false, current: "v1.2.3", latest: "v1.2.3", channel: "stable" }),
ApplyUpdateRequest: async () => { applyCalls += 1; },
FetchAllProviderModels: async () => ({}),
});

async function renderAbout(root: ReturnType<typeof createRoot>) {
await act(async () => {
root.render(
<LocaleProvider>
<UpdaterProvider>
<SettingsPanel initialTab="updates" desktopPlatform="windows" onClose={() => {}} onChanged={() => {}} />
</UpdaterProvider>
</LocaleProvider>,
);
await flushPromises();
});
await act(async () => { await flushPromises(); });
}

try {
const stableRoot = createRoot(document.getElementById("root")!);
await renderAbout(stableRoot);
assert.ok(document.querySelector('.settings-page--updates[aria-label="About"]'), "legacy updates route opens the About page");
assert.ok(document.body.textContent?.includes("Current version: v1.2.3"));
assert.ok(document.querySelector('[aria-label="Check for updates"]'), "stable build shows manual update check");
assert.ok(document.body.textContent?.includes("Download from official site"), "stable build shows official download entry");
assert.ok(document.body.textContent?.includes("Update preferences"), "stable build shows update preferences");
assert.equal(applyCalls, 0, "available update actions never run before a user click");
await act(async () => stableRoot.unmount());

settings = { ...baseSettings("standard"), updaterEnabled: false };
const testRoot = createRoot(document.getElementById("root")!);
await renderAbout(testRoot);
assert.ok(document.querySelector('.settings-page--updates[aria-label="About"]'), "test build keeps the legacy route functional");
assert.equal(document.querySelector('[aria-label="Check for updates"]'), null, "test build hides manual update check");
assert.equal(document.body.textContent?.includes("Download from official site"), false, "test build hides production download entry");
assert.equal(document.body.textContent?.includes("Update preferences"), false, "test build hides updater preferences");
assert.ok(document.body.textContent?.includes("Build identity"), "test build shows build identity");
assert.ok(document.body.textContent?.includes("Privacy & configuration"), "test build keeps privacy and configuration");
assert.ok(document.body.textContent?.includes("Release notes"), "test build keeps changelog access");
assert.ok(document.body.textContent?.includes("Help & feedback"), "test build keeps feedback access");
await act(async () => testRoot.unmount());

console.log("PASS About route and stable/test updater capability controls");
} finally {
desktopStub.uninstall();
dom.window.close();
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,25 @@ let current!: ReturnType<typeof useDesktopPreferences>;
function Probe() { current = useDesktopPreferences(); return <div>{current.configLoadWarnings.join("|")}</div>; }
const root = createRoot(document.getElementById("root")!);
const snapshot = { sessionExperience: "deep", desktopTheme: "light", desktopThemeStyle: "graphite",
desktopLanguage: "en", checkUpdates: true, configWarnings: ["warning"], configWarningsRevision: 3 } as DesktopStartupSettingsView;
desktopLanguage: "en", checkUpdates: true, updaterEnabled: true, configWarnings: ["warning"], configWarningsRevision: 3 } as DesktopStartupSettingsView;
try {
localStorage.setItem("reasonix-process-fold", "auto");
await act(async () => root.render(<LocaleProvider><Probe /></LocaleProvider>));
assert.equal(requests, 1);
await act(async () => { resolveStartup(snapshot); await import("../lib/themeExperience"); });
assert.equal(getSessionExperience(), "deep", "backend wins over an old localStorage mirror");
assert.equal(current.startupUpdateChecksEnabled, true, "stable build and enabled preference allow automatic checks");
assert.deepEqual(current.configLoadWarnings, ["warning"]);
await act(async () => { desktopStub.emit("config:load-warnings", ["stale"], 2); });
assert.deepEqual(current.configLoadWarnings, ["warning"], "stale runtime warning cannot replace startup snapshot");
await act(async () => { desktopStub.emit("config:load-warnings", ["current"], 4); });
assert.deepEqual(current.configLoadWarnings, ["current"]);
await act(async () => { await current.reload({ ...snapshot, sessionExperience: undefined }); });
assert.equal(getSessionExperience(), "standard", "old backend missing field resolves standard");
await act(async () => { await current.reload({ ...snapshot, sessionExperience: undefined, updaterEnabled: undefined }); });
assert.equal(current.startupUpdateChecksEnabled, false, "old backend missing updater capability fails closed");
await act(async () => { await current.reload({ ...snapshot, sessionExperience: undefined, updaterEnabled: false }); });
assert.equal(current.startupUpdateChecksEnabled, false, "disabled build capability overrides the user preference");
assert.equal(fullSettings, 0, "preferences and IM projection never request full Settings");
const oldReload = current.reload;
await act(async () => root.unmount());
Expand All @@ -57,7 +62,7 @@ try {
await act(async () => failedRoot.render(<LocaleProvider><Probe /></LocaleProvider>));
await act(async () => { await current.reload(); });
assert.equal(getSessionExperience(), "standard", "failed first snapshot uses canonical standard, not a legacy local preference");
assert.equal(current.startupUpdateChecksEnabled, true);
assert.equal(current.startupUpdateChecksEnabled, false, "startup RPC failure disables automatic checks");
} finally { await act(async () => failedRoot.unmount()); console.warn = originalWarn; }
console.log("desktop preferences: lightweight snapshot, legacy mirror, warning revision and disposal passed");
} finally { dom.window.close(); }
149 changes: 149 additions & 0 deletions desktop/frontend/src/__tests__/updater-scheduling.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import React, { act, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { UpdateBanner } from "../components/UpdateBanner";
import { LocaleProvider } from "../lib/i18n";
import {
__resetUpdaterCheckScheduleForTests,
UPDATE_CHECK_INTERVAL_MS,
UPDATE_CHECK_STORAGE_KEY,
UpdaterProvider,
useUpdater,
} from "../lib/useUpdater";
import { installDesktopHostStub } from "./desktopHostStub";

const dom = new JSDOM("<!doctype html><div id='root'></div>", { url: "http://localhost", pretendToBeVisual: true });
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
Node: dom.window.Node,
Element: dom.window.Element,
HTMLElement: dom.window.HTMLElement,
Event: dom.window.Event,
MouseEvent: dom.window.MouseEvent,
IS_REACT_ACT_ENVIRONMENT: true,
});

let now = 1_800_000_000_000;
const originalDateNow = Date.now;
Date.now = () => now;

let checkCalls = 0;
const desktopStub = installDesktopHostStub({
CheckUpdate: async () => {
checkCalls += 1;
return {
available: false,
current: "v1.2.3",
latest: "v1.2.3",
notes: "",
channel: "stable",
canSelfUpdate: true,
manualOnly: false,
installMode: "portable",
requiresElevation: false,
downloaded: false,
downloadUrl: "https://example.invalid/download",
assetSize: 0,
};
},
});

function ManualCheck() {
const updater = useUpdater();
return <button id="manual-check" onClick={() => void updater.check()}>Check</button>;
}

function AutomaticCheck() {
const updater = useUpdater();
useEffect(() => { void updater.refresh(); }, [updater.refresh]);
return null;
}

async function flush() {
await new Promise((resolve) => setTimeout(resolve, 0));
}

const root = createRoot(document.getElementById("root")!);
try {
__resetUpdaterCheckScheduleForTests();
await act(async () => {
root.render(<LocaleProvider><UpdaterProvider><UpdateBanner enabled /><ManualCheck /></UpdaterProvider></LocaleProvider>);
await flush();
});
assert.equal(checkCalls, 1, "first enabled mount performs one automatic check");

now += UPDATE_CHECK_INTERVAL_MS - 1;
await act(async () => {
window.dispatchEvent(new Event("focus"));
document.dispatchEvent(new Event("visibilitychange"));
await flush();
});
assert.equal(checkCalls, 1, "focus and visibility events stay throttled before six hours");

now += 1;
await act(async () => {
window.dispatchEvent(new Event("focus"));
document.dispatchEvent(new Event("visibilitychange"));
await flush();
});
assert.equal(checkCalls, 2, "simultaneous due events produce one check after six hours");

await act(async () => root.render(<LocaleProvider><UpdaterProvider><AutomaticCheck /><ManualCheck /></UpdaterProvider></LocaleProvider>));
await act(async () => { await flush(); });
assert.equal(checkCalls, 2, "stored attempt survives provider remounts");

await act(async () => {
(document.getElementById("manual-check") as HTMLButtonElement).click();
await flush();
});
assert.equal(checkCalls, 3, "manual checks bypass the automatic throttle");
assert.equal(window.localStorage.getItem(UPDATE_CHECK_STORAGE_KEY), String(now), "manual check resets the automatic schedule");

now += UPDATE_CHECK_INTERVAL_MS - 1;
await act(async () => {
window.dispatchEvent(new Event("focus"));
await flush();
});
assert.equal(checkCalls, 3, "manual check suppresses automatic checks for the next six hours");

await act(async () => root.render(<LocaleProvider><UpdaterProvider><UpdateBanner enabled={false} /></UpdaterProvider></LocaleProvider>));
now += UPDATE_CHECK_INTERVAL_MS;
await act(async () => {
window.dispatchEvent(new Event("focus"));
document.dispatchEvent(new Event("visibilitychange"));
await flush();
});
assert.equal(checkCalls, 3, "disabled automatic checks install no startup, focus, or visibility work");

__resetUpdaterCheckScheduleForTests();
window.localStorage.setItem(UPDATE_CHECK_STORAGE_KEY, String(now + UPDATE_CHECK_INTERVAL_MS));
await act(async () => root.render(<LocaleProvider><UpdaterProvider><AutomaticCheck /></UpdaterProvider></LocaleProvider>));
await act(async () => { await flush(); });
assert.equal(checkCalls, 4, "a future timestamp is discarded and rebuilt by an immediate check");

const localStorageDescriptor = Object.getOwnPropertyDescriptor(window, "localStorage");
Object.defineProperty(window, "localStorage", {
configurable: true,
get() { throw new Error("storage denied"); },
});
__resetUpdaterCheckScheduleForTests();
await act(async () => root.render(<LocaleProvider><UpdaterProvider><UpdateBanner enabled /></UpdaterProvider></LocaleProvider>));
await act(async () => { await flush(); });
assert.equal(checkCalls, 5, "storage denial still allows the first process-local check");
await act(async () => {
window.dispatchEvent(new Event("focus"));
document.dispatchEvent(new Event("visibilitychange"));
await flush();
});
assert.equal(checkCalls, 5, "process-local fallback throttles duplicate checks when storage is unavailable");
if (localStorageDescriptor) Object.defineProperty(window, "localStorage", localStorageDescriptor);

console.log("PASS updater six-hour scheduling, persistence, manual bypass, disable, and storage recovery");
} finally {
await act(async () => root.unmount());
desktopStub.uninstall();
Date.now = originalDateNow;
dom.window.close();
}
4 changes: 3 additions & 1 deletion desktop/frontend/src/app-runtime/useDesktopPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export function useDesktopPreferences() {
const sidebarImConnections = useMemo(() => snapshot ? sidebarImConnectionsFromBot(snapshot.bot, t, botRuntime, nativeRuntime) : [], [snapshot, t, botRuntime, nativeRuntime]);
const imTopicSources = useMemo(() => snapshot ? sidebarImTopicSourcesFromBot(snapshot.bot, t) : {}, [snapshot, t]);
return {
startupUpdateChecksEnabled: snapshot ? snapshot.checkUpdates !== false : startupFailed ? true : null,
startupUpdateChecksEnabled: snapshot
? snapshot.updaterEnabled === true && snapshot.checkUpdates !== false
: startupFailed ? false : null,
statusBarStyle: snapshot?.statusBarStyle === "text" ? "text" as const : "icon" as const,
statusBarItems: snapshot ? normalizeStatusBarItems(snapshot.statusBarItems) : DEFAULT_STATUS_BAR_ITEMS,
sidebarImConnections, imTopicSources,
Expand Down
4 changes: 2 additions & 2 deletions desktop/frontend/src/components/SettingsNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {
Database,
Globe,
HardDrive,
Info,
Keyboard,
LockKeyhole,
Network,
Package,
Palette,
Plug,
RefreshCw,
Search,
Server,
Settings2,
Expand Down Expand Up @@ -150,6 +150,6 @@ function settingsTabIcon(id: SettingsTab): ReactNode {
case "appearance": return <Palette {...props} />;
case "storage": return <HardDrive {...props} />;
case "browser": return <Globe {...props} />;
case "updates": return <RefreshCw {...props} />;
case "updates": return <Info {...props} />;
}
}
Loading
Loading