tests: pin ink engine in _make_tui_argv npm-bootstrap tests (post-merge semantic fix)

Main's rewritten test_tui_npm_install.py tests call _make_tui_argv expecting
the Ink/npm flow unconditionally; with the dual-engine dispatch merged in,
_resolve_tui_engine() auto-selects opentui whenever ui-opentui/dist is built
in the repo, routing the call away from the path under test (first subprocess
became 'node --version' instead of 'npm run build'). Pin the engine to ink
via an autouse fixture, mirroring the existing pinning precedent in
test_tui_resume_flow.py.
This commit is contained in:
alt-glitch 2026-06-12 10:32:40 +05:30
parent ab37440ce6
commit e1067dbbe5
756 changed files with 79874 additions and 19585 deletions

View file

@ -0,0 +1,115 @@
import {
useCallback,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import { useLocation, useSearchParams } from "react-router-dom";
import { api, setManagementProfile } from "@/lib/api";
import { ProfileContext } from "@/contexts/profile-context";
/**
* Machine-level management-profile scope.
*
* One switcher (rendered in the sidebar) decides which profile every
* management page reads/writes. React STATE is the source of truth; the
* URL (`?profile=<name>`) is a synchronized projection of it so deep links
* land scoped and refresh survives. The selection is mirrored into the api
* module so `fetchJSON` transparently appends it to the profile-scoped
* endpoint families. "" = the dashboard's own profile.
*
* Why state-first instead of URL-first: sidebar nav links are bare paths
* (`/config`, `/skills`). A URL-derived scope would silently reset to the
* dashboard's own profile on every nav click the switcher would LOOK
* global while normal navigation dropped the write target. With state as
* truth, the effect below re-asserts `?profile=` onto the new location
* after each navigation, so the scope survives nav and stays deep-linkable.
*
* This exists because "Set as active" on the Profiles page only flips the
* sticky active_profile file (future CLI/gateway runs) it cannot retarget
* the running dashboard. The switcher is the dashboard's own, visible,
* write-target selector.
*/
export function ProfileProvider({ children }: { children: ReactNode }) {
const [searchParams, setSearchParams] = useSearchParams();
const { pathname } = useLocation();
const [profiles, setProfiles] = useState<string[]>([]);
const [currentProfile, setCurrentProfile] = useState("default");
// Initial value comes from the URL (deep link / refresh / unified-launch
// preselect); afterwards state leads and the URL follows.
const [profile, setProfileState] = useState(
() => searchParams.get("profile") ?? "",
);
// Mirror into the api module synchronously on every render where it
// changed, so fetches fired by child effects in the same commit see it.
setManagementProfile(profile);
// A profile param arriving via in-app navigation (e.g. the Profiles
// page's "Manage skills & tools" linking to /skills?profile=X) must win
// over current state — it's an explicit scope request.
const urlProfile = searchParams.get("profile");
useEffect(() => {
if (urlProfile !== null && urlProfile !== profile) {
setManagementProfile(urlProfile);
setProfileState(urlProfile);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [urlProfile]);
// Re-assert ?profile= after navigations that dropped it (bare nav links).
// Runs on every pathname/profile change; no-ops when already in sync.
useEffect(() => {
const inUrl = searchParams.get("profile") ?? "";
if ((profile || "") === inUrl) return;
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
if (profile) next.set("profile", profile);
else next.delete("profile");
return next;
},
{ replace: true },
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname, profile]);
useEffect(() => {
api
.getProfiles()
.then((res) => setProfiles(res.profiles.map((p) => p.name)))
.catch(() => {});
api
.getActiveProfile()
.then((info) => setCurrentProfile(info.current || "default"))
.catch(() => {});
}, []);
const setProfile = useCallback(
(name: string) => {
setManagementProfile(name);
setProfileState(name);
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
if (name) next.set("profile", name);
else next.delete("profile");
return next;
},
{ replace: true },
);
},
[setSearchParams],
);
const value = useMemo(
() => ({ profile, currentProfile, profiles, setProfile }),
[profile, currentProfile, profiles, setProfile],
);
return (
<ProfileContext.Provider value={value}>{children}</ProfileContext.Provider>
);
}

View file

@ -0,0 +1,19 @@
import { createContext } from "react";
export interface ProfileContextValue {
/** Profile every management surface reads/writes ("" = the dashboard
* process's own profile). */
profile: string;
/** The profile the dashboard process itself runs under. */
currentProfile: string;
/** Known profile names (includes "default"). */
profiles: string[];
setProfile: (name: string) => void;
}
export const ProfileContext = createContext<ProfileContextValue>({
profile: "",
currentProfile: "default",
profiles: [],
setProfile: () => {},
});

View file

@ -0,0 +1,6 @@
import { useContext } from "react";
import { ProfileContext } from "@/contexts/profile-context";
export function useProfileScope() {
return useContext(ProfileContext);
}