mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Improve agent runtime settings (#2026)
This commit is contained in:
@@ -113,7 +113,7 @@ pub(crate) enum RequirementPayload {
|
||||
/// One-line stderr excerpt identifying the parse error.
|
||||
diagnostic: String,
|
||||
},
|
||||
/// Git for Windows is missing; open Doctor for the installation guide.
|
||||
/// Git for Windows is missing; open Agent runtimes for the installation guide.
|
||||
GitBash,
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ impl RequirementPayload {
|
||||
.map(String::as_str)
|
||||
.unwrap_or("the agent");
|
||||
format!(
|
||||
"install the {} ACP adapter (open Doctor in Settings to diagnose)",
|
||||
"install the {} ACP adapter (open Agent runtimes in Settings to diagnose)",
|
||||
harness
|
||||
)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ impl RequirementPayload {
|
||||
.map(String::as_str)
|
||||
.unwrap_or("the agent");
|
||||
format!(
|
||||
"reinstall the {} ACP adapter — the installed version is outdated (open Doctor in Settings to diagnose)",
|
||||
"reinstall the {} ACP adapter — the installed version is outdated (open Agent runtimes in Settings to diagnose)",
|
||||
harness
|
||||
)
|
||||
}
|
||||
@@ -159,7 +159,7 @@ impl RequirementPayload {
|
||||
.map(String::as_str)
|
||||
.unwrap_or("the agent");
|
||||
format!(
|
||||
"install {} CLI (open Doctor in Settings to diagnose)",
|
||||
"install {} CLI (open Agent runtimes in Settings to diagnose)",
|
||||
harness
|
||||
)
|
||||
}
|
||||
@@ -168,7 +168,10 @@ impl RequirementPayload {
|
||||
.first()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("the agent");
|
||||
format!("install {} (open Doctor in Settings to diagnose)", harness)
|
||||
format!(
|
||||
"install {} (open Agent runtimes in Settings to diagnose)",
|
||||
harness
|
||||
)
|
||||
}
|
||||
},
|
||||
RequirementPayload::CliConfigInvalid {
|
||||
@@ -184,7 +187,7 @@ impl RequirementPayload {
|
||||
)
|
||||
}
|
||||
RequirementPayload::GitBash => {
|
||||
"install Git for Windows (open Doctor in Settings to diagnose)".to_string()
|
||||
"install Git for Windows (open Agent runtimes in Settings to diagnose)".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,7 +267,7 @@ impl SetupPayload {
|
||||
.any(|r| matches!(r, RequirementPayload::CliConfigInvalid { .. }));
|
||||
|
||||
let footer = if has_doctor_requirement {
|
||||
"Open Doctor in the Buzz app, install Git for Windows, then re-check and restart the agent.".to_string()
|
||||
"Open Agent runtimes in Settings, install Git for Windows, then re-check and restart the agent.".to_string()
|
||||
} else if all_external {
|
||||
// All requirements are external config files — Edit Agent cannot
|
||||
// help. Don't send the user there.
|
||||
@@ -743,6 +746,53 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nudge_body_runtime_install_copy_points_to_agent_runtimes() {
|
||||
for availability in [
|
||||
AcpAvailabilityStatus::AdapterMissing,
|
||||
AcpAvailabilityStatus::AdapterOutdated,
|
||||
AcpAvailabilityStatus::CliMissing,
|
||||
AcpAvailabilityStatus::NotInstalled,
|
||||
] {
|
||||
let payload = SetupPayload {
|
||||
agent_name: "Codex".to_string(),
|
||||
agent_pubkey: "test".to_string(),
|
||||
requirements: vec![RequirementPayload::CliLogin {
|
||||
probe_args: vec!["codex".to_string()],
|
||||
setup_copy: "run `codex login`".to_string(),
|
||||
availability,
|
||||
}],
|
||||
};
|
||||
let body = payload.nudge_body();
|
||||
assert!(
|
||||
body.contains("Agent runtimes in Settings"),
|
||||
"runtime install nudge must point to Agent runtimes; got: {body:?}"
|
||||
);
|
||||
assert!(
|
||||
!body.contains("Doctor"),
|
||||
"runtime install nudge must not point to the removed Doctor section; got: {body:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nudge_body_git_bash_copy_points_to_agent_runtimes() {
|
||||
let payload = SetupPayload {
|
||||
agent_name: "Buzz Agent".to_string(),
|
||||
agent_pubkey: "test".to_string(),
|
||||
requirements: vec![RequirementPayload::GitBash],
|
||||
};
|
||||
let body = payload.nudge_body();
|
||||
assert!(
|
||||
body.contains("Open Agent runtimes in Settings"),
|
||||
"Git Bash nudge must point to Agent runtimes; got: {body:?}"
|
||||
);
|
||||
assert!(
|
||||
!body.contains("Doctor"),
|
||||
"Git Bash nudge must not point to the removed Doctor section; got: {body:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nudge_body_empty_requirements_falls_back_to_generic() {
|
||||
let payload = SetupPayload {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="2048" height="2048" viewBox="0 0 2048 2048" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="200" y="200" width="1648" height="1648" rx="400" ry="400" fill="#fff"/>
|
||||
<g transform="translate(128 94) scale(3)">
|
||||
<path fill="#000" d="M455.145 485.687c18.641-4.048 38.414-15.065 38.414-15.065l-34.607-28.507c-17.108-14.081-31.67-30.999-43.054-50.007-15.731-26.263-37.467-48.425-63.413-64.663l-12.672-7.379c-4.342-3.018-7.369-7.722-7.8-13.029-.276-3.422.544-6.481 2.457-9.173 6.598-9.3 40.702-49.767 46.952-54.938 8.049-6.653 17.018-12.187 25.339-18.523 1.183-.902 2.37-1.799 3.539-2.714.041-.041.1-.073.136-.109 2.67-2.108 5.19-4.347 7.193-7.002 7.233-8.376 8.991-15.781 9.422-19.058-.979-3.154-3.898-10.206-12.033-18.341 5.099.313 11.272 4.346 16.869 9.091 3.757-6.005 7.691-12.35 11.62-18.708 2.624-4.247-1.269-7.424-1.378-7.523l-.022-.005v-.018c-.1-.109-3.277-4.002-7.514-1.378-9.06 5.606-18.115 11.217-26.168 16.315 0 0-9.545-.199-20.848 9.563-2.66 2.008-4.899 4.527-7.002 7.192-.041.041-.077.095-.109.136-.92 1.165-1.817 2.352-2.714 3.54-6.341 8.325-11.87 17.29-18.523 25.339-5.167 6.254-45.638 40.353-54.938 46.952-2.692 1.913-5.747 2.737-9.173 2.456-5.302-.426-10.011-3.458-13.03-7.799l-7.378-12.672c-16.238-25.955-38.4-47.682-64.663-63.413-19.008-11.384-35.922-25.951-50.007-43.055l-28.516-34.607s-11.022 19.769-15.065 38.414c5.624 6.839 20.295 23.812 37.372 36.774-18.36-8.634-31.933-15.31-42.638-20.893-1.65 12.209-1.006 30.673.693 44.972 11.589 5.071 31.321 12.884 51.408 17.036-16.067 3.757-33.692 4.441-47.302 4.174 2.398 8.851 5.733 17.879 10.22 26.961 1.908 4.201 3.997 8.303 6.254 12.318 7.202 1.981 35.478 6.1 50.524 2.719-14.969 5.344-40.1 14.322-40.1 14.322 19.302 24.015 40.635 43.553 40.635 43.553 32.449-17.448 39.846-19.832 64.183-35.749-39.411 32.069-49.64 45.162-60.703 58.618l-7.709 10.818c-4.006 5.62-7.482 11.589-10.387 17.847-9.721 20.916-23.481 65.484-23.481 65.484-2.456 7.768 3.236 13.461 10.777 10.778 0 0 44.56-13.76 65.484-23.481 6.255-2.91 12.233-6.386 17.848-10.388l10.818-7.709c3.63-2.986 7.238-5.914 11.199-9.367 0 0 27.233 32.263 55.227 53.487 0 0 8.979-25.13 14.322-40.1-3.386 15.051.734 43.327 2.719 50.524 4.006 2.257 8.117 4.346 12.318 6.254 9.087 4.491 18.11 7.822 26.962 10.22-.263-13.614.417-31.235 4.174-47.301 4.151 20.086 11.96 39.823 17.036 51.407 14.298 1.7 32.762 2.343 44.972.693-5.584-10.704-12.26-24.287-20.893-42.637 12.961 17.077 29.934 31.747 36.773 37.371l-.014-.013z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -49,7 +49,7 @@ type AppShellContextValue = {
|
||||
feedItemState: FeedItemState;
|
||||
// Open the Settings panel at the given section. Available on all surfaces
|
||||
// that render under AppShell (channel, home, projects, pulse, agents).
|
||||
// Used by config-nudge cards to deep-link to Settings → Doctor.
|
||||
// Used by config-nudge cards to deep-link to Settings → Agents.
|
||||
onOpenSettings: ((section: SettingsSection) => void) | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ type SettingsRouteSearch = {
|
||||
function validateSettingsSearch(
|
||||
search: Record<string, unknown>,
|
||||
): SettingsRouteSearch {
|
||||
if (search.section === "doctor") {
|
||||
return { section: "agents" };
|
||||
}
|
||||
|
||||
return {
|
||||
section: isSettingsSection(search.section) ? search.section : undefined,
|
||||
};
|
||||
|
||||
@@ -624,7 +624,7 @@ export function AgentDefinitionDialog({
|
||||
: selectedRuntime.availability === "cli_missing"
|
||||
? `${selectedRuntime.label} ACP adapter is installed but the CLI is missing.`
|
||||
: `${selectedRuntime.label} is not installed.`}{" "}
|
||||
Visit Settings > Doctor to set it up.
|
||||
Visit Settings > Agents to set it up.
|
||||
</p>
|
||||
) : null;
|
||||
const advancedFieldsTransition = shouldReduceMotion
|
||||
|
||||
@@ -815,8 +815,8 @@ function RuntimeProvidersSection({
|
||||
className="max-w-[560px] rounded-2xl bg-white/70 px-6 py-6 text-sm text-muted-foreground"
|
||||
data-testid="onboarding-acp-empty"
|
||||
>
|
||||
No compatible ACP runtimes detected yet. You can finish setup now
|
||||
and come back later in Settings > Doctor.
|
||||
No compatible agent runtimes detected yet. You can finish setup now
|
||||
and come back later in Settings > Agents.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ type ProfileAvatarProps = {
|
||||
label: string;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
imageClassName?: string;
|
||||
plain?: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
@@ -23,6 +24,7 @@ export function ProfileAvatar({
|
||||
label,
|
||||
className,
|
||||
iconClassName,
|
||||
imageClassName,
|
||||
plain = false,
|
||||
testId,
|
||||
}: ProfileAvatarProps) {
|
||||
@@ -67,7 +69,7 @@ export function ProfileAvatar({
|
||||
{src !== undefined ? (
|
||||
<AvatarImage
|
||||
alt={`${label} avatar`}
|
||||
className="object-cover"
|
||||
className={cn("object-cover", imageClassName)}
|
||||
data-testid={testId ? `${testId}-image` : undefined}
|
||||
onLoadingStatusChange={(status) => {
|
||||
if (status === "error") setFailedSrc(liveSrc);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@ import {
|
||||
ShieldAlert,
|
||||
Smartphone,
|
||||
Smile,
|
||||
Stethoscope,
|
||||
Sun,
|
||||
SunMoon,
|
||||
UserRound,
|
||||
@@ -100,8 +99,7 @@ export type SettingsSection =
|
||||
| "custom-emoji"
|
||||
| "local-archive"
|
||||
| "mobile"
|
||||
| "updates"
|
||||
| "doctor";
|
||||
| "updates";
|
||||
|
||||
export const DEFAULT_SETTINGS_SECTION: SettingsSection = "profile";
|
||||
|
||||
@@ -121,7 +119,6 @@ const SETTINGS_SECTION_VALUES: readonly SettingsSection[] = [
|
||||
"local-archive",
|
||||
"mobile",
|
||||
"updates",
|
||||
"doctor",
|
||||
];
|
||||
|
||||
export function isSettingsSection(value: unknown): value is SettingsSection {
|
||||
@@ -233,12 +230,6 @@ export const settingsSections: SettingsSectionDescriptor[] = [
|
||||
label: "Updates",
|
||||
icon: Download,
|
||||
},
|
||||
{
|
||||
value: "doctor",
|
||||
label: "Doctor",
|
||||
icon: Stethoscope,
|
||||
featureGate: "doctor",
|
||||
},
|
||||
];
|
||||
|
||||
function formatThemeLabel(name: string): string {
|
||||
@@ -822,6 +813,7 @@ export function renderSettingsSection(
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<PreventSleepSettingsCard />
|
||||
<DoctorSettingsPanel />
|
||||
<GlobalAgentConfigSettingsCard />
|
||||
</div>
|
||||
);
|
||||
@@ -849,8 +841,6 @@ export function renderSettingsSection(
|
||||
return <MobilePairingCard currentPubkey={props.currentPubkey} />;
|
||||
case "updates":
|
||||
return <UpdateChecker />;
|
||||
case "doctor":
|
||||
return <DoctorSettingsPanel />;
|
||||
default: {
|
||||
const exhaustiveCheck: never = section;
|
||||
return exhaustiveCheck;
|
||||
|
||||
@@ -66,14 +66,7 @@ const settingsNavGroups: Array<{
|
||||
},
|
||||
{
|
||||
label: "App",
|
||||
sections: [
|
||||
"agents",
|
||||
"compute",
|
||||
"experimental",
|
||||
"mobile",
|
||||
"updates",
|
||||
"doctor",
|
||||
],
|
||||
sections: ["agents", "compute", "experimental", "mobile", "updates"],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ function requirementKey(
|
||||
/**
|
||||
* Returns true when every requirement in the nudge is a `cli_login` surface.
|
||||
* Non-authOnly all-cli_login cards (at least one install-state row) route to
|
||||
* Doctor — install/login problems can't be fixed in Edit Agent. AuthOnly cards
|
||||
* Agent runtimes — install/login problems can't be fixed in Edit Agent. AuthOnly cards
|
||||
* (every row is `availability === "available"`) are purely informational and
|
||||
* do not route anywhere.
|
||||
*/
|
||||
@@ -65,8 +65,8 @@ export function shouldOpenDoctor(
|
||||
|
||||
/**
|
||||
* Returns true when the card is all-cli_login AND every requirement is in the
|
||||
* `available` state (tooling installed, just needs login). In this case Doctor
|
||||
* has no auth functionality and is a misleading dead-end — the card becomes
|
||||
* `available` state (tooling installed, just needs login). In this case Agent
|
||||
* runtimes has no auth functionality and is a misleading dead-end — the card becomes
|
||||
* purely informational (no trigger, no CTA, no pointer/hover affordance).
|
||||
*/
|
||||
function isAuthOnly(reqs: ConfigNudgePayload["requirements"]): boolean {
|
||||
@@ -140,7 +140,7 @@ function firstFocusTarget(
|
||||
* Mirrors `firstFocusTarget` but operates on one row — used so per-row
|
||||
* Edit Agent CTAs focus the field that row describes, not the first editable
|
||||
* field on the card.
|
||||
* Returns `undefined` for `cli_login` requirements (Doctor, not Edit Agent).
|
||||
* Returns `undefined` for `cli_login` requirements (Agent runtimes, not Edit Agent).
|
||||
*/
|
||||
export function focusTargetForRequirement(
|
||||
req: ConfigNudgePayload["requirements"][number],
|
||||
@@ -164,15 +164,15 @@ export function focusTargetForRequirement(
|
||||
*
|
||||
* Routing:
|
||||
* (A) Any card with a `git_bash` requirement, or one whose requirements are all
|
||||
* install-state `cli_login`, opens Settings → Doctor. A card-level
|
||||
* "Open Doctor →" label in `AttachmentActions` confirms the action at rest.
|
||||
* install-state `cli_login`, opens Settings → Agent runtimes. A card-level
|
||||
* Agent runtimes label in `AttachmentActions` confirms the action at rest.
|
||||
* (A-auth) A card whose requirements are all available `cli_login` surfaces is
|
||||
* purely informational: Doctor cannot authenticate a CLI, and `setup_copy`
|
||||
* purely informational: Agent runtimes cannot authenticate a CLI, and `setup_copy`
|
||||
* already gives the needed command.
|
||||
* (B) Other mixed cards open Edit Agent as the card-level fallback. Their rows
|
||||
* carry inline CTAs for the matching destination: install-state `cli_login`
|
||||
* opens Doctor; `env_key` and `normalized_field` open Edit Agent. A
|
||||
* `git_bash` row is covered by the card-level Doctor route, so it does not
|
||||
* opens Agent runtimes; `env_key` and `normalized_field` open Edit Agent. A
|
||||
* `git_bash` row is covered by the card-level Agent runtimes route, so it does not
|
||||
* render a redundant row action.
|
||||
*/
|
||||
export function ConfigNudgeCard({
|
||||
@@ -196,10 +196,10 @@ export function ConfigNudgeCard({
|
||||
const openDoctor = () => {
|
||||
if (!onOpenSettings) {
|
||||
console.warn(
|
||||
"[ConfigNudgeCard] onOpenSettings is null — Doctor deep-link unavailable on this surface",
|
||||
"[ConfigNudgeCard] onOpenSettings is null — Agent runtimes deep-link unavailable on this surface",
|
||||
);
|
||||
}
|
||||
onOpenSettings?.("doctor");
|
||||
onOpenSettings?.("agents");
|
||||
};
|
||||
|
||||
const openEditAgent = (focus?: EditAgentFocusTarget) => {
|
||||
@@ -209,7 +209,7 @@ export function ConfigNudgeCard({
|
||||
|
||||
const handleOpen = () => {
|
||||
if (shouldOpenDoctor(nudge.requirements)) {
|
||||
// Git Bash and install-state CLI requirements both resolve in Doctor.
|
||||
// Git Bash and install-state CLI requirements both resolve in Agent runtimes.
|
||||
// Informational-only cards never mount this trigger.
|
||||
openDoctor();
|
||||
} else {
|
||||
@@ -219,7 +219,7 @@ export function ConfigNudgeCard({
|
||||
};
|
||||
|
||||
const handleOpenDoctor = (e: React.MouseEvent) => {
|
||||
// (B) Per-row Doctor CTA — stop propagation so the card trigger doesn't
|
||||
// (B) Per-row Agent runtimes CTA — stop propagation so the card trigger doesn't
|
||||
// double-fire to Edit Agent on mixed cards.
|
||||
e.stopPropagation();
|
||||
openDoctor();
|
||||
@@ -266,11 +266,13 @@ export function ConfigNudgeCard({
|
||||
))}
|
||||
</div>
|
||||
</AttachmentContent>
|
||||
{/* (A) Doctor-routed cards have one card-level CTA. Informational-only
|
||||
{/* (A) Agent-runtime-routed cards have one card-level CTA. Informational-only
|
||||
cards have none; other mixed cards render their own row CTAs. */}
|
||||
{opensDoctor && !informationalOnly && (
|
||||
<AttachmentActions className="items-end self-end">
|
||||
<span className="text-xs text-muted-foreground">Open Doctor →</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Open Agent runtimes →
|
||||
</span>
|
||||
</AttachmentActions>
|
||||
)}
|
||||
{/* Informational-only cards are purely informational — no trigger, no routing. */}
|
||||
@@ -278,7 +280,7 @@ export function ConfigNudgeCard({
|
||||
<AttachmentTrigger
|
||||
aria-label={
|
||||
opensDoctor
|
||||
? `Open Doctor settings for ${nudge.agent_name}`
|
||||
? `Open Agent runtimes settings for ${nudge.agent_name}`
|
||||
: `Open Edit Agent for ${nudge.agent_name}`
|
||||
}
|
||||
onClick={handleOpen}
|
||||
@@ -352,11 +354,11 @@ function RequirementRow({
|
||||
<span className="flex-1 [overflow-wrap:anywhere]">
|
||||
{cliLoginMessage(requirement)}
|
||||
</span>
|
||||
{/* (B) Per-row Doctor CTA — shown only on mixed cards where the
|
||||
{/* (B) Per-row Agent runtimes CTA — shown only on mixed cards where the
|
||||
card-level trigger opens Edit Agent (not auth-only cards). When
|
||||
allCliLogin is true the card trigger already routes to Doctor; the
|
||||
allCliLogin is true the card trigger already routes to Agent runtimes; the
|
||||
per-row button is redundant and is suppressed. Also suppressed for
|
||||
`available` cli_login rows — Doctor has no auth functionality and
|
||||
`available` cli_login rows — Agent runtimes has no auth functionality and
|
||||
the setup_copy already provides the exact login command.
|
||||
stopPropagation prevents double-fire on mixed cards where both
|
||||
card and row CTAs are visible. */}
|
||||
@@ -366,7 +368,7 @@ function RequirementRow({
|
||||
onClick={onOpenDoctor}
|
||||
type="button"
|
||||
>
|
||||
Open Doctor →
|
||||
Open Agent runtimes →
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -381,7 +383,7 @@ function RequirementRow({
|
||||
);
|
||||
case "cli_config_invalid": {
|
||||
// Config-invalid rows are purely informational — the user must edit an
|
||||
// external file. No Doctor CTA (Doctor can't repair ~/.codex/config.toml)
|
||||
// external file. No Agent runtimes CTA (Buzz can't repair ~/.codex/config.toml)
|
||||
// and no Edit Agent CTA (the field isn't managed by Buzz).
|
||||
const cli = requirement.probe_args[0] ?? "the CLI";
|
||||
const configFile = `~/.${cli}/config.toml`;
|
||||
|
||||
@@ -9,7 +9,7 @@ const Switch = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-none transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -17,7 +17,7 @@ const Switch = React.forwardRef<
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-none ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
|
||||
@@ -128,11 +128,13 @@ type E2eConfig = {
|
||||
acpRuntimesCatalog?: RawAcpRuntimeCatalogEntry[];
|
||||
acpRuntimesDelayMs?: number;
|
||||
acpAuthMethods?: Record<string, RawAcpAuthMethodsResult>;
|
||||
acpAuthMethodsErrors?: Record<string, string>;
|
||||
acpAuthMethodsError?: string;
|
||||
connectAcpRuntimeResult?: RawConnectAcpRuntimeResult;
|
||||
connectAcpRuntimeDelayMs?: number;
|
||||
connectAcpRuntimeError?: string;
|
||||
activePersonaIds?: string[];
|
||||
installAcpRuntimeDelayMs?: number;
|
||||
installAcpRuntimeResult?: RawInstallRuntimeResult;
|
||||
/** Sequence of results for successive `install_acp_runtime` calls.
|
||||
* Call N returns results[N]; when exhausted the last entry repeats.
|
||||
@@ -6700,11 +6702,15 @@ async function handleDiscoverAcpAuthMethods(
|
||||
args: { runtimeId?: string },
|
||||
config: E2eConfig | undefined,
|
||||
): Promise<RawAcpAuthMethodsResult> {
|
||||
const error = config?.mock?.acpAuthMethodsError;
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
const globalError = config?.mock?.acpAuthMethodsError;
|
||||
if (globalError) {
|
||||
throw new Error(globalError);
|
||||
}
|
||||
const runtimeId = args.runtimeId ?? "";
|
||||
const perRuntimeError = config?.mock?.acpAuthMethodsErrors?.[runtimeId];
|
||||
if (perRuntimeError) {
|
||||
throw new Error(perRuntimeError);
|
||||
}
|
||||
const configured = config?.mock?.acpAuthMethods?.[runtimeId];
|
||||
if (configured) {
|
||||
return configured;
|
||||
@@ -6750,6 +6756,10 @@ async function handleInstallAcpRuntime(
|
||||
},
|
||||
config: E2eConfig | undefined,
|
||||
): Promise<RawInstallRuntimeResult> {
|
||||
const delayMs = config?.mock?.installAcpRuntimeDelayMs ?? 0;
|
||||
if (delayMs > 0) {
|
||||
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
|
||||
}
|
||||
const sequence = config?.mock?.installAcpRuntimeResults;
|
||||
if (sequence && sequence.length > 0) {
|
||||
const idx = Math.min(installCallCount, sequence.length - 1);
|
||||
|
||||
@@ -155,8 +155,8 @@ test.describe("doctor CTA nudge card screenshots", () => {
|
||||
// Wait for the nudge card to render.
|
||||
const card = page.locator("[data-config-nudge]").last();
|
||||
await expect(card).toBeVisible({ timeout: 10_000 });
|
||||
// Auth-only card is informational — no Doctor CTA anywhere.
|
||||
await expect(card.getByText("Open Doctor →")).toHaveCount(0);
|
||||
// Auth-only card is informational — no runtime settings CTA anywhere.
|
||||
await expect(card.getByText("Open Agent runtimes →")).toHaveCount(0);
|
||||
|
||||
await card.scrollIntoViewIfNeeded();
|
||||
await settleAnimations(page);
|
||||
@@ -168,7 +168,7 @@ test.describe("doctor CTA nudge card screenshots", () => {
|
||||
|
||||
/**
|
||||
* 02 — not_installed state: neither adapter nor CLI found.
|
||||
* Card shows "claude isn't installed" copy + inline "Open Doctor →" CTA.
|
||||
* Card shows "claude isn't installed" copy + an Agent runtimes CTA.
|
||||
*/
|
||||
test("02-cli-login-not-installed-state", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
@@ -210,7 +210,7 @@ test.describe("doctor CTA nudge card screenshots", () => {
|
||||
/**
|
||||
* 03 — mixed card: one cli_login (adapter_missing) + one env_key requirement.
|
||||
* Each requirement row owns its CTA, right-aligned to a shared edge:
|
||||
* the cli_login row shows "Open Doctor →" and the env_key row shows
|
||||
* the cli_login row opens Agent runtimes and the env_key row shows
|
||||
* "Edit Agent →", both at the same x (vertically aligned).
|
||||
*/
|
||||
test("03-mixed-requirements-inline-doctor-cta", async ({ page }) => {
|
||||
@@ -244,8 +244,8 @@ test.describe("doctor CTA nudge card screenshots", () => {
|
||||
|
||||
const card = page.locator("[data-config-nudge]").last();
|
||||
await expect(card).toBeVisible({ timeout: 10_000 });
|
||||
// Mixed card: cli_login row shows "Open Doctor →", env_key row shows "Edit Agent →".
|
||||
await expect(card.getByText("Open Doctor →")).toBeVisible();
|
||||
// Mixed card: cli_login opens Agent runtimes; env_key opens Edit Agent.
|
||||
await expect(card.getByText("Open Agent runtimes →")).toBeVisible();
|
||||
// Both per-row CTAs share the same right edge (vertically aligned).
|
||||
await expect(card.getByText("Edit Agent →", { exact: true })).toBeVisible();
|
||||
|
||||
|
||||
@@ -111,9 +111,116 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 01 — available runtime that passed the auth probe: green "Authenticated"
|
||||
* badge appears below the binary path.
|
||||
* 00 — the runtime catalog reads as a set of individual status cards rather
|
||||
* than one continuous table.
|
||||
*/
|
||||
test("00-runtime-card-layout", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
acpRuntimesCatalog: [
|
||||
GOOSE_AVAILABLE,
|
||||
CLAUDE_AVAILABLE_LOGGED_IN,
|
||||
CODEX_NOT_INSTALLED,
|
||||
BUZZ_AGENT_AVAILABLE,
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "agents");
|
||||
|
||||
const runtimeList = page.getByTestId("doctor-runtime-list");
|
||||
await expect(runtimeList).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId("doctor-runtime-goose")).toBeVisible();
|
||||
await expect(page.getByTestId("doctor-runtime-codex")).toBeVisible();
|
||||
await expect(
|
||||
runtimeList.locator(":scope > [data-testid^='doctor-runtime-']"),
|
||||
).toHaveCount(4);
|
||||
expect(
|
||||
await runtimeList
|
||||
.locator(":scope > [data-testid^='doctor-runtime-']")
|
||||
.evaluateAll((rows) =>
|
||||
rows.map((row) => row.getAttribute("data-testid")),
|
||||
),
|
||||
).toEqual([
|
||||
"doctor-runtime-buzz-agent",
|
||||
"doctor-runtime-goose",
|
||||
"doctor-runtime-claude",
|
||||
"doctor-runtime-codex",
|
||||
]);
|
||||
for (const runtimeId of ["goose", "claude", "codex", "buzz-agent"]) {
|
||||
await expect(
|
||||
page.getByTestId(`doctor-runtime-logo-${runtimeId}`),
|
||||
).toBeVisible();
|
||||
}
|
||||
const rowHeights = await Promise.all(
|
||||
["goose", "claude", "codex", "buzz-agent"].map((runtimeId) =>
|
||||
page
|
||||
.getByTestId(`doctor-runtime-${runtimeId}`)
|
||||
.evaluate((element) =>
|
||||
Math.round(element.getBoundingClientRect().height),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(new Set(rowHeights).size).toBe(1);
|
||||
const [gooseColors, codexColors] = await Promise.all(
|
||||
["goose", "codex"].map((runtimeId) =>
|
||||
page.getByTestId(`doctor-runtime-${runtimeId}`).evaluate((element) => {
|
||||
const styles = getComputedStyle(element);
|
||||
return {
|
||||
backgroundColor: styles.backgroundColor,
|
||||
borderColor: styles.borderColor,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(codexColors).toEqual(gooseColors);
|
||||
await expect(
|
||||
page
|
||||
.getByRole("heading", { name: "Agent runtimes" })
|
||||
.locator("..")
|
||||
.locator(".."),
|
||||
).toHaveCSS("align-items", "center");
|
||||
for (const runtimeId of ["goose", "claude", "buzz-agent"]) {
|
||||
await expect(
|
||||
page.getByTestId(`doctor-runtime-menu-${runtimeId}`),
|
||||
).toHaveCount(0);
|
||||
}
|
||||
await expect(
|
||||
page.getByTestId("doctor-runtime-toggle-codex"),
|
||||
).not.toBeChecked();
|
||||
await expect(page.getByTestId("doctor-runtime-toggle-codex")).toBeEnabled();
|
||||
for (const runtimeId of ["goose", "codex"]) {
|
||||
const toggle = page.getByTestId(`doctor-runtime-toggle-${runtimeId}`);
|
||||
await expect(toggle).toHaveClass(/shadow-none/);
|
||||
await expect(toggle.locator("span")).toHaveClass(/shadow-none/);
|
||||
}
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Instructions" }),
|
||||
).toHaveCount(0);
|
||||
await page.getByTestId("doctor-runtime-menu-codex").click();
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Instructions" }),
|
||||
).toBeVisible();
|
||||
await waitForAnimations(page);
|
||||
await page.screenshot({
|
||||
path: `${SHOTS}/00-runtime-overflow-menu.png`,
|
||||
});
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByTestId("doctor-runtime-toggle-goose")).toBeChecked();
|
||||
await expect(
|
||||
page.getByTestId("doctor-runtime-toggle-goose"),
|
||||
).toBeDisabled();
|
||||
await expect(page.getByTestId("doctor-runtime-codex")).not.toContainText(
|
||||
"Not installed",
|
||||
);
|
||||
|
||||
await runtimeList.scrollIntoViewIfNeeded();
|
||||
await waitForAnimations(page);
|
||||
await runtimeList.screenshot({
|
||||
path: `${SHOTS}/00-runtime-card-layout.png`,
|
||||
});
|
||||
});
|
||||
|
||||
/** 01 — a ready runtime stays compact without redundant status copy. */
|
||||
test("01-auth-logged-in", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
acpRuntimesCatalog: [
|
||||
@@ -125,11 +232,18 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "doctor");
|
||||
await openSettings(page, "agents");
|
||||
|
||||
const row = page.getByTestId("doctor-runtime-claude");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
await expect(row).toContainText("Authenticated");
|
||||
await expect(
|
||||
page.getByTestId("doctor-runtime-toggle-claude"),
|
||||
).toBeChecked();
|
||||
await expect(row).not.toContainText("Authenticated");
|
||||
await expect(row).not.toContainText("Available");
|
||||
await expect(row).not.toContainText("claude-agent-acp");
|
||||
await expect(row).not.toContainText("/usr/local/bin");
|
||||
await expect(page.getByTestId("doctor-runtime-menu-claude")).toHaveCount(0);
|
||||
|
||||
await row.scrollIntoViewIfNeeded();
|
||||
await waitForAnimations(page);
|
||||
@@ -137,8 +251,8 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 02 — available runtime that failed the auth probe: amber "Not
|
||||
* authenticated" badge + login hint shown below the binary path.
|
||||
* 02 — an available runtime that needs authentication stays the same height
|
||||
* as the others and moves setup instructions into its overflow menu.
|
||||
*/
|
||||
test("02-auth-logged-out", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
@@ -159,12 +273,23 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "doctor");
|
||||
await openSettings(page, "agents");
|
||||
|
||||
const row = page.getByTestId("doctor-runtime-codex");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
await expect(row).toContainText("Not authenticated");
|
||||
await expect(row).toContainText("Run `codex login` to authenticate.");
|
||||
await expect(row).not.toContainText("Not authenticated");
|
||||
await expect(row).not.toContainText("Run `codex login` to authenticate.");
|
||||
await expect(row).toHaveCSS(
|
||||
"height",
|
||||
await page
|
||||
.getByTestId("doctor-runtime-goose")
|
||||
.evaluate((element) => getComputedStyle(element).height),
|
||||
);
|
||||
await page.getByTestId("doctor-runtime-menu-codex").click();
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Instructions" }),
|
||||
).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await row.scrollIntoViewIfNeeded();
|
||||
await waitForAnimations(page);
|
||||
@@ -172,8 +297,8 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 03 — available runtime whose CLI has a config-parse error: red "Config
|
||||
* error" badge + diagnostic excerpt shown below the binary path.
|
||||
* 03 — a runtime with invalid configuration exposes its diagnostic and keeps
|
||||
* setup instructions in overflow.
|
||||
*/
|
||||
test("03-auth-config-error", async ({ page }) => {
|
||||
const diagnostic =
|
||||
@@ -192,12 +317,23 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "doctor");
|
||||
await openSettings(page, "agents");
|
||||
|
||||
const row = page.getByTestId("doctor-runtime-claude");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
await expect(row).toContainText("Config error");
|
||||
await expect(row).toContainText("error loading configuration");
|
||||
await expect(page.getByTestId("doctor-runtime-status-claude")).toHaveText(
|
||||
"Config error",
|
||||
);
|
||||
await expect(
|
||||
page.getByTestId("doctor-runtime-config-error-claude"),
|
||||
).toContainText(
|
||||
"Config error: error loading configuration: ~/.claude/settings.json: unknown key foo",
|
||||
);
|
||||
await page.getByTestId("doctor-runtime-menu-claude").click();
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Instructions" }),
|
||||
).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await row.scrollIntoViewIfNeeded();
|
||||
await waitForAnimations(page);
|
||||
@@ -205,9 +341,8 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 04 — adapter_missing runtime with node_required: true: the amber "Node.js
|
||||
* is required…" callout replaces the Install button so the user cannot
|
||||
* inadvertently trigger a doomed npm install.
|
||||
* 04 — adapter_missing runtime with node_required: true: the off toggle is
|
||||
* disabled, and the Node.js action moves into the overflow menu.
|
||||
*/
|
||||
test("04-node-required", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
@@ -227,15 +362,28 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "doctor");
|
||||
await openSettings(page, "agents");
|
||||
|
||||
const row = page.getByTestId("doctor-runtime-codex");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
await expect(row).toContainText("Node.js is required");
|
||||
// Exact-name match so "Install Node.js" (inside the callout) is not counted.
|
||||
const toggle = page.getByTestId("doctor-runtime-toggle-codex");
|
||||
await expect(toggle).not.toBeChecked();
|
||||
await expect(toggle).toBeDisabled();
|
||||
await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText(
|
||||
"Adapter needed",
|
||||
);
|
||||
await expect(row).not.toContainText("Node.js is required");
|
||||
await expect(row).toHaveCSS(
|
||||
"height",
|
||||
await page
|
||||
.getByTestId("doctor-runtime-goose")
|
||||
.evaluate((element) => getComputedStyle(element).height),
|
||||
);
|
||||
await page.getByTestId("doctor-runtime-menu-codex").click();
|
||||
await expect(
|
||||
row.getByRole("button", { name: "Install", exact: true }),
|
||||
).toHaveCount(0);
|
||||
page.getByRole("menuitem", { name: "Install Node.js" }),
|
||||
).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await row.scrollIntoViewIfNeeded();
|
||||
await waitForAnimations(page);
|
||||
@@ -243,13 +391,13 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 05 — a failed install renders a "Retry" button; clicking Retry succeeds.
|
||||
* 05 — a failed toggle install returns to off; toggling again retries.
|
||||
*
|
||||
* The mock is configured with a two-call sequence:
|
||||
* call 1 → failure (E404)
|
||||
* call 2 → success
|
||||
* This exercises the full Retry UX path: fail state → click Retry →
|
||||
* spinner → success banner.
|
||||
* This exercises the full retry path: fail state → toggle on again →
|
||||
* success banner.
|
||||
*/
|
||||
test("05-retry-after-failure", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
@@ -263,6 +411,7 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
},
|
||||
BUZZ_AGENT_AVAILABLE,
|
||||
],
|
||||
installAcpRuntimeDelayMs: 250,
|
||||
installAcpRuntimeResults: [
|
||||
{
|
||||
success: false,
|
||||
@@ -294,19 +443,26 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "doctor");
|
||||
await openSettings(page, "agents");
|
||||
|
||||
const row = page.getByTestId("doctor-runtime-codex");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
await expect(row).not.toContainText("Not installed");
|
||||
|
||||
// Trigger the first install — the mock returns a failure.
|
||||
const installBtn = row.getByRole("button", { name: "Install Codex" });
|
||||
await expect(installBtn).toBeVisible({ timeout: 5_000 });
|
||||
await installBtn.click();
|
||||
const toggle = page.getByTestId("doctor-runtime-toggle-codex");
|
||||
await expect(toggle).not.toBeChecked();
|
||||
await expect(toggle).toBeEnabled();
|
||||
await toggle.click();
|
||||
const loading = page.getByTestId("doctor-runtime-loading-codex");
|
||||
await expect(loading).toBeVisible();
|
||||
await expect(loading).toContainText("Codex installing");
|
||||
await expect(toggle).toHaveCount(0);
|
||||
|
||||
// After failure: Retry button appears and the error message is visible.
|
||||
const retryBtn = row.getByRole("button", { name: "Retry Install Codex" });
|
||||
await expect(retryBtn).toBeVisible({ timeout: 5_000 });
|
||||
// After failure: the toggle returns to off and the error is visible.
|
||||
await expect(loading).toHaveCount(0, { timeout: 5_000 });
|
||||
await expect(toggle).not.toBeChecked({ timeout: 5_000 });
|
||||
await expect(toggle).toBeEnabled();
|
||||
await expect(row).toContainText("Step");
|
||||
await expect(row).toContainText("failed");
|
||||
|
||||
@@ -314,17 +470,21 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
await waitForAnimations(page);
|
||||
await row.screenshot({ path: `${SHOTS}/05-retry-after-failure.png` });
|
||||
|
||||
// Click Retry — the mock returns success on the second call.
|
||||
await retryBtn.click();
|
||||
// Toggle on again — the mock returns success on the second call.
|
||||
await toggle.click();
|
||||
await expect(loading).toBeVisible();
|
||||
await expect(toggle).toHaveCount(0);
|
||||
|
||||
// Error paragraph must disappear and per-runtime spinner must appear,
|
||||
// then the success banner must render.
|
||||
// The error disappears, then the success banner and on state render.
|
||||
await expect(loading).toHaveCount(0, { timeout: 5_000 });
|
||||
await expect(row).not.toContainText("failed", { timeout: 5_000 });
|
||||
await expect(
|
||||
row.getByText("Codex installed. Checking for sign-in options..."),
|
||||
).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(toggle).toBeChecked();
|
||||
await expect(toggle).toBeDisabled();
|
||||
|
||||
await row.scrollIntoViewIfNeeded();
|
||||
await waitForAnimations(page);
|
||||
@@ -332,9 +492,8 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 06 — logged-out runtime with adapter-advertised auth methods: Doctor shows
|
||||
* adapter-provided labels/descriptions and clicking one launches the
|
||||
* vendor-owned flow through the mocked connect command.
|
||||
* 06 — adapter-provided account methods appear in the overflow menu and
|
||||
* launch the vendor-owned flow without expanding the runtime row.
|
||||
*/
|
||||
test("06-connect-account-methods", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
@@ -368,28 +527,37 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "doctor");
|
||||
await openSettings(page, "agents");
|
||||
|
||||
const row = page.getByTestId("doctor-runtime-codex");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
await expect(row).toContainText("Not authenticated");
|
||||
await expect(row).toContainText("Sign in with ChatGPT");
|
||||
await expect(row).toContainText(
|
||||
"Use your Codex subscription in the browser.",
|
||||
await expect(row).not.toContainText("Not authenticated");
|
||||
await expect(row).toHaveCSS(
|
||||
"height",
|
||||
await page
|
||||
.getByTestId("doctor-runtime-goose")
|
||||
.evaluate((element) => getComputedStyle(element).height),
|
||||
);
|
||||
await expect(row).toContainText("Credentials stay with Codex.");
|
||||
|
||||
await row.getByRole("button", { name: "Sign in with ChatGPT" }).click();
|
||||
await page.getByTestId("doctor-runtime-menu-codex").click();
|
||||
await expect(
|
||||
row.getByRole("button", { name: "Connecting..." }),
|
||||
page.getByRole("menuitem", { name: "Sign in with ChatGPT" }),
|
||||
).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
await page.getByRole("menuitem", { name: "Sign in with ChatGPT" }).click();
|
||||
const loading = page.getByTestId("doctor-runtime-loading-codex");
|
||||
await expect(loading).toBeVisible();
|
||||
await expect(loading).toContainText("Codex connecting");
|
||||
await expect(page.getByTestId("doctor-runtime-toggle-codex")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
await expect(loading).toHaveCount(0, { timeout: 5_000 });
|
||||
await expect(page.getByTestId("doctor-runtime-toggle-codex")).toBeChecked();
|
||||
});
|
||||
|
||||
/**
|
||||
* 07 — old or constrained adapter with no advertised auth methods: Doctor
|
||||
* falls back to manual instructions instead of inventing a login command.
|
||||
* 07 — an adapter with no advertised auth methods shows only its manual
|
||||
* instructions in overflow and keeps the row compact.
|
||||
*/
|
||||
test("07-connect-account-no-methods", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
@@ -409,14 +577,162 @@ test.describe("Doctor panel state screenshots", () => {
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "doctor");
|
||||
await openSettings(page, "agents");
|
||||
|
||||
const row = page.getByTestId("doctor-runtime-claude");
|
||||
await expect(row).toBeVisible({ timeout: 10_000 });
|
||||
await expect(row).toContainText("Not authenticated");
|
||||
await expect(row).toContainText(
|
||||
"This adapter did not advertise a built-in login flow.",
|
||||
await expect(row).not.toContainText("Not authenticated");
|
||||
await expect(row).toHaveCSS(
|
||||
"height",
|
||||
await page
|
||||
.getByTestId("doctor-runtime-goose")
|
||||
.evaluate((element) => getComputedStyle(element).height),
|
||||
);
|
||||
await expect(row).not.toContainText("Connect account");
|
||||
await page.getByTestId("doctor-runtime-menu-claude").click();
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Instructions" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Sign in with ChatGPT" }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("08-auth-method-discovery-error", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
acpRuntimesCatalog: [
|
||||
GOOSE_AVAILABLE,
|
||||
CLAUDE_AVAILABLE_LOGGED_IN,
|
||||
{
|
||||
...CODEX_NOT_INSTALLED,
|
||||
availability: "available",
|
||||
auth_status: { status: "logged_out" },
|
||||
},
|
||||
BUZZ_AGENT_AVAILABLE,
|
||||
],
|
||||
acpAuthMethodsErrors: {
|
||||
codex: "Could not inspect the Codex adapter.",
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "agents");
|
||||
|
||||
await expect(page.getByTestId("doctor-runtime-error-codex")).toContainText(
|
||||
"Couldn't load sign-in options: Could not inspect the Codex adapter.",
|
||||
);
|
||||
});
|
||||
|
||||
test("09-connect-account-error", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
acpRuntimesCatalog: [
|
||||
GOOSE_AVAILABLE,
|
||||
CLAUDE_AVAILABLE_LOGGED_IN,
|
||||
{
|
||||
...CODEX_NOT_INSTALLED,
|
||||
availability: "available",
|
||||
auth_status: { status: "logged_out" },
|
||||
},
|
||||
BUZZ_AGENT_AVAILABLE,
|
||||
],
|
||||
acpAuthMethods: {
|
||||
codex: {
|
||||
methods: [
|
||||
{
|
||||
id: "chat-gpt",
|
||||
name: "Sign in with ChatGPT",
|
||||
type: "browser",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
connectAcpRuntimeError: "The browser could not be opened.",
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "agents");
|
||||
|
||||
await page.getByTestId("doctor-runtime-menu-codex").click();
|
||||
await page.getByRole("menuitem", { name: "Sign in with ChatGPT" }).click();
|
||||
await expect(page.getByTestId("doctor-runtime-error-codex")).toContainText(
|
||||
"Couldn't connect Codex: The browser could not be opened.",
|
||||
);
|
||||
});
|
||||
|
||||
test("10-terminal-auth-completion-guidance", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
acpRuntimesCatalog: [
|
||||
GOOSE_AVAILABLE,
|
||||
CLAUDE_AVAILABLE_LOGGED_IN,
|
||||
{
|
||||
...CODEX_NOT_INSTALLED,
|
||||
availability: "available",
|
||||
auth_status: { status: "logged_out" },
|
||||
},
|
||||
BUZZ_AGENT_AVAILABLE,
|
||||
],
|
||||
acpAuthMethods: {
|
||||
codex: {
|
||||
methods: [
|
||||
{
|
||||
id: "terminal-login",
|
||||
name: "Sign in from Terminal",
|
||||
type: "terminal",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
connectAcpRuntimeResult: { launched: true },
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "agents");
|
||||
|
||||
await page.getByTestId("doctor-runtime-menu-codex").click();
|
||||
await page.getByRole("menuitem", { name: "Sign in from Terminal" }).click();
|
||||
await expect(
|
||||
page.getByTestId("doctor-runtime-terminal-guidance-codex"),
|
||||
).toContainText(
|
||||
"Finish signing in from the Terminal window, then click Check again to re-check Codex.",
|
||||
);
|
||||
});
|
||||
|
||||
test("11-outdated-adapter-warning", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
acpRuntimesCatalog: [
|
||||
GOOSE_AVAILABLE,
|
||||
CLAUDE_AVAILABLE_LOGGED_IN,
|
||||
{
|
||||
...CODEX_NOT_INSTALLED,
|
||||
availability: "adapter_outdated",
|
||||
binary_path: "/usr/local/bin/codex-acp",
|
||||
underlying_cli_path: "/usr/local/bin/codex",
|
||||
can_auto_install: true,
|
||||
},
|
||||
BUZZ_AGENT_AVAILABLE,
|
||||
],
|
||||
installAcpRuntimeDelayMs: 250,
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await openSettings(page, "agents");
|
||||
|
||||
await expect(page.getByTestId("doctor-runtime-status-codex")).toHaveText(
|
||||
"Update needed",
|
||||
);
|
||||
await page.getByTestId("doctor-runtime-toggle-codex").click();
|
||||
|
||||
const dialog = page.getByRole("alertdialog");
|
||||
await expect(dialog).toContainText("Update Codex adapter?");
|
||||
await expect(dialog).toContainText(
|
||||
"Older Buzz releases using the legacy adapter may lose community access",
|
||||
);
|
||||
await expect(page.getByTestId("doctor-runtime-loading-codex")).toHaveCount(
|
||||
0,
|
||||
);
|
||||
|
||||
await page.getByTestId("doctor-runtime-confirm-update-codex").click();
|
||||
const loading = page.getByTestId("doctor-runtime-loading-codex");
|
||||
await expect(loading).toBeVisible();
|
||||
await expect(loading).toContainText("Codex installing");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1537,11 +1537,11 @@ test("supports webview zoom keyboard shortcuts", async ({ page }) => {
|
||||
});
|
||||
});
|
||||
|
||||
test("shows doctor checks for local CLI tooling", async ({ page }) => {
|
||||
test("shows agent runtimes in agent settings", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await openSettings(page, "doctor");
|
||||
await openSettings(page, "agents");
|
||||
|
||||
await expect(page.getByTestId("settings-doctor")).toBeVisible();
|
||||
await expect(page.getByTestId("settings-agent-runtimes")).toBeVisible();
|
||||
await expect(page.getByTestId("doctor-runtime-goose")).toContainText("Goose");
|
||||
});
|
||||
|
||||
@@ -134,6 +134,7 @@ type MockBridgeOptions = {
|
||||
connectAcpRuntimeResult?: { launched: boolean };
|
||||
connectAcpRuntimeDelayMs?: number;
|
||||
connectAcpRuntimeError?: string;
|
||||
installAcpRuntimeDelayMs?: number;
|
||||
/** Override the result returned by the `install_acp_runtime` mock command.
|
||||
* Pass `{ success: false, steps: [...] }` to exercise error/Retry states. */
|
||||
installAcpRuntimeResult?: {
|
||||
|
||||
@@ -11,8 +11,7 @@ type SettingsSection =
|
||||
| "tokens"
|
||||
| "community-members"
|
||||
| "mobile"
|
||||
| "updates"
|
||||
| "doctor";
|
||||
| "updates";
|
||||
|
||||
export async function openProfileMenu(page: Page) {
|
||||
await page.getByTestId("open-settings").click();
|
||||
|
||||
Reference in New Issue
Block a user