mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
* perf(daemon): parallelize runtime version detection during registration (MUL-5119) Registration probed each agent CLI's `--version` serially, so total latency was the sum of every probe. On an onboarding host with several coding tools installed that stacked into many seconds before runtimes registered — long enough that the desktop runtime step timed out into its empty 'no runtime found' state while the daemon was still working. Fan the probes out with a bounded errgroup so total latency tracks the slowest single probe instead of their sum. Each probe still self-heals a vanished pinned path and re-detects the live version (no cross-registration caching, so an in-place upgrade is still reported correctly); failures are logged and skipped as before. Results are sorted by provider for a deterministic payload. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): stop the runtime step flashing 'no runtime found' while the daemon probes (MUL-5119) The runtime step flipped from scanning to the empty 'no runtime found' state on a fixed 5s wall-clock, so a machine that does have coding tools installed saw a false-negative flash whenever registration outlasted the timeout (cold start, slow/wedged CLI, many CLIs). Gate the empty flip on a desktop-only `runtimesPending` signal derived from the local daemon's live status (booting, or running with agent CLIs detected on the host): while pending, keep the scanning skeleton past the soft timeout. An absolute hard-timeout ceiling still guarantees a fallback so a wedged probe can't pin the step on the skeleton forever. Web omits the signal and keeps the plain wall-clock timeout. Also drop the two dead/duplicated affordances on the step: the permanently disabled 'Start exploring' button now renders only in the found phase, and the empty state's duplicate footer 'Skip for now' is removed in favour of its own Skip card. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
146 lines
4.7 KiB
TypeScript
146 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
import { act, render, screen } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { AgentRuntime } from "@multica/core/types";
|
|
import { I18nProvider } from "@multica/core/i18n/react";
|
|
import enCommon from "../../locales/en/common.json";
|
|
import enOnboarding from "../../locales/en/onboarding.json";
|
|
|
|
const TEST_RESOURCES = { en: { common: enCommon, onboarding: enOnboarding } };
|
|
|
|
// Drive the runtime picker via a hoisted mock so the step renders without a
|
|
// live daemon. (The onboarding_runtime_detected PostHog event this file used
|
|
// to cover was removed in MUL-4127.)
|
|
const mocks = vi.hoisted(() => ({
|
|
pickerState: {
|
|
runtimes: [] as AgentRuntime[],
|
|
selected: null as AgentRuntime | null,
|
|
selectedId: null as string | null,
|
|
setSelectedId: vi.fn<(id: string) => void>(),
|
|
hasRuntimes: false,
|
|
},
|
|
}));
|
|
|
|
vi.mock("../components/use-runtime-picker", () => ({
|
|
useRuntimePicker: () => mocks.pickerState,
|
|
}));
|
|
|
|
import { StepRuntimeConnect } from "./step-runtime-connect";
|
|
|
|
function makeRuntime(overrides: Partial<AgentRuntime> = {}): AgentRuntime {
|
|
return {
|
|
id: "rt_1",
|
|
name: "Claude (dev-box)",
|
|
provider: "claude",
|
|
status: "online",
|
|
...overrides,
|
|
} as AgentRuntime;
|
|
}
|
|
|
|
function renderStep(props: { runtimesPending?: boolean } = {}) {
|
|
const onNext = vi.fn();
|
|
const onBack = vi.fn();
|
|
const qc = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
render(
|
|
<QueryClientProvider client={qc}>
|
|
<I18nProvider locale="en" resources={TEST_RESOURCES}>
|
|
<StepRuntimeConnect
|
|
wsId="ws_test"
|
|
onNext={onNext}
|
|
onBack={onBack}
|
|
runtimesPending={props.runtimesPending}
|
|
/>
|
|
</I18nProvider>
|
|
</QueryClientProvider>,
|
|
);
|
|
return { onNext, onBack };
|
|
}
|
|
|
|
describe("StepRuntimeConnect", () => {
|
|
beforeEach(() => {
|
|
mocks.pickerState.runtimes = [];
|
|
mocks.pickerState.selected = null;
|
|
mocks.pickerState.selectedId = null;
|
|
mocks.pickerState.hasRuntimes = false;
|
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("mounts and shows the scanning UI without touching framework-level globals", () => {
|
|
renderStep();
|
|
expect(
|
|
screen.getByText(/connecting this computer/i),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("does not render a permanently-disabled Start exploring while scanning", () => {
|
|
renderStep();
|
|
expect(
|
|
screen.queryByRole("button", { name: /start exploring/i }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("flips to the empty state after the idle timeout when no pending signal is given", () => {
|
|
renderStep();
|
|
act(() => vi.advanceTimersByTime(5000));
|
|
expect(
|
|
screen.getByText(/no agent runtime found on this computer yet/i),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("keeps scanning past the idle timeout while runtimes are pending, then falls back at the hard ceiling", () => {
|
|
renderStep({ runtimesPending: true });
|
|
|
|
// Past the soft budget the daemon still reports work in flight, so the
|
|
// false-negative empty state must not appear (MUL-5119).
|
|
act(() => vi.advanceTimersByTime(5000));
|
|
expect(
|
|
screen.getByText(/connecting this computer/i),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.queryByText(/no agent runtime found/i),
|
|
).not.toBeInTheDocument();
|
|
|
|
// The absolute ceiling still guarantees a fallback so a wedged probe
|
|
// cannot hang the step on the skeleton forever.
|
|
act(() => vi.advanceTimersByTime(15000));
|
|
expect(
|
|
screen.getByText(/no agent runtime found/i),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows the found list — not the empty state — once runtimes register", () => {
|
|
mocks.pickerState.runtimes = [makeRuntime()];
|
|
mocks.pickerState.selected = makeRuntime();
|
|
mocks.pickerState.selectedId = "rt_1";
|
|
mocks.pickerState.hasRuntimes = true;
|
|
|
|
renderStep();
|
|
act(() => vi.advanceTimersByTime(25000));
|
|
|
|
expect(
|
|
screen.getByText(/this computer is connected/i),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.queryByText(/no agent runtime found/i),
|
|
).not.toBeInTheDocument();
|
|
// Continue is actionable in the found phase.
|
|
expect(
|
|
screen.getByRole("button", { name: /start exploring/i }),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows a single Skip affordance in the empty state (no duplicate footer button)", () => {
|
|
renderStep();
|
|
act(() => vi.advanceTimersByTime(5000));
|
|
// The empty view owns one prominent "Skip for now" card; the footer no
|
|
// longer duplicates it.
|
|
expect(screen.getAllByText("Skip for now")).toHaveLength(1);
|
|
});
|
|
});
|