chore: reconcile contribution rules and onboarding fixtures

Extract the guided setup test fixture without dropping cases. Supply current settings/discovery contracts and Opus 5.5 in the isolated Storybook boundary.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 20:34:26 -05:00
1 parent 21c4f8afd5
commit 48d4ab54d6
19 files changed
+277 -232

No files matched your search

-1
View File
@@ -3,7 +3,6 @@ import { Field } from "./agent-config-primitives";
import { SelectPopover } from "./ui/select";
import { Button } from "./ui/button";
/** The override is request-level. Automatic selection is resolved by the server. */
export function AgentRunnerSelect({
value,
onChange,
@@ -178,7 +178,6 @@ describe("OnboardingWizard — which step it lands on", () => {
dialogState.onboardingOpen = false;
dialogState.onboardingOptions = {};
dialogState.onboardingRouteDismissed = false;
// The scoped discovery contract must resolve before the hire seam runs.
mockAdaptersApi.list.mockResolvedValue(["claude_local", "codex_local"].map(type => ({
type, loaded: true, disabled: false, defaultRunner: "legacy", supportedRunners: ["legacy"],
})));
@@ -3180,7 +3180,6 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", (
const { root, queryClient } = await openStep4({ adapterType: "claude_local" });
try {
await act(async () => {
// A warm value remains successful while its current fetch is pending.
if (pending === "environments") queryClient.setQueryData(queryKeys.environments.list("company-new"), [LOCAL_ENVIRONMENT]);
else if (pending === "settings") queryClient.setQueryData(queryKeys.instance.settings, { defaultEnvironmentId: null });
else queryClient.setQueryData(queryKeys.instance.experimentalSettings, { enableManagedSandboxOnly: false });
-3
View File
@@ -1060,9 +1060,6 @@ function OnboardingWizardInner({
}, effectiveOnboardingOpen && step === 4 && canUseLocalLogin && credentialMode !== "api" &&
Boolean(managedProvider) && !savedSubscription && !savedKeys.storedLogin.data && !managedBindingForStep(),
);
// A result from a previous selection must not hire or advance this wizard.
// Discovery starts after environment settings resolve. A later target or
// runner change invalidates the configuration an in-flight test was built from.
useEffect(() => {
autoConnectStartedRef.current = false;
hiringAgentRef.current = null;
-2
View File
@@ -1775,8 +1775,6 @@ export function CompanyImport() {
applyChanges(original.adapterSchemaValues ?? {}, configVals.adapterSchemaValues ?? {});
applyChanges(baseline, edited);
if (selectedType === "claude_local") {
// The native builder preserves source schema fields. Project common
// form edits separately so a removed binding cannot be restored there.
const commonBuild = getUIAdapter("claude_local").buildAdapterConfig;
const commonBefore = commonBuild(original);
const commonAfter = commonBuild(configVals);
+203
View File
@@ -0,0 +1,203 @@
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, expect, vi } from "vitest";
import { TooltipProvider } from "@/components/ui/tooltip";
import { queryKeys } from "@/lib/queryKeys";
import { NewAgent } from "./NewAgent";
import { ApiError } from "@/api/client";
const api = vi.hoisted(() => ({
get: vi.fn(),
adapterModels: vi.fn(),
list: vi.fn(),
hire: vi.fn(),
testEnvironment: vi.fn(),
getAdapterAuthSignal: vi.fn(),
getClaudeOAuthTokenStatus: vi.fn(),
}));
const envApi = vi.hoisted(() => ({ list: vi.fn(), capabilities: vi.fn() }));
const settings = vi.hoisted(() => ({
get: vi.fn(),
getExperimental: vi.fn(),
getGeneral: vi.fn(),
}));
const secrets = vi.hoisted(() => ({
create: vi.fn(),
remove: vi.fn(),
list: vi.fn(),
listMyUserSecrets: vi.fn(),
createUserSecretDefinition: vi.fn(),
createMyUserSecret: vi.fn(),
rotateMyUserSecret: vi.fn(),
removeUserSecretDefinition: vi.fn(),
}));
const state = vi.hoisted(() => ({
params: new URLSearchParams(),
adapters: [] as object[],
navigate: vi.fn(),
openNewIssue: vi.fn(),
}));
const managedApi = vi.hoisted(() => ({
list: vi.fn(async () => ({ currentUserId: "user-1", canManageConnections: true, connections: [] })),
create: vi.fn(async () => ({ connectionId: "managed-connection", grantId: "managed-grant" })),
setDefault: vi.fn(async () => ({})),
}));
vi.mock("@/api/ai-connections", () => ({ aiConnectionsApi: managedApi }));
vi.mock("@/api/agents", () => ({ agentsApi: api }));
vi.mock("@/api/environments", () => ({ environmentsApi: envApi }));
vi.mock("@/api/instanceSettings", () => ({ instanceSettingsApi: settings }));
vi.mock("@/api/secrets", () => ({ secretsApi: secrets }));
const adapterApi = vi.hoisted(() => ({ list: vi.fn() }));
vi.mock("@/api/adapters", () => ({ adaptersApi: adapterApi }));
vi.mock("@/context/CompanyContext", () => ({
useCompany: () => ({ selectedCompanyId: "company-1" }),
}));
vi.mock("../context/BreadcrumbContext", () => ({
useBreadcrumbs: () => ({ setBreadcrumbs: vi.fn() }),
}));
vi.mock("@/context/DialogContext", () => ({
useDialogActions: () => ({ openNewIssue: state.openNewIssue }),
}));
vi.mock("@/lib/router", () => ({
useNavigate: () => state.navigate,
useSearchParams: () => [state.params],
}));
vi.mock("@/components/AgentConfigForm", () => ({
ModelDropdown: ({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) => (
<input
aria-label="Model"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
AdapterLoginPanel: ({ onConnected }: { onConnected: (id: string) => void }) => (
<button onClick={() => onConnected("login-session")}>
Complete subscription login
</button>
),
}));
vi.mock("@/components/onboarding/PillGuy", () => ({ PillGuy: () => null }));
vi.mock("motion/react", () => ({
AnimatePresence: ({ children }: any) => children,
MotionConfig: ({ children }: any) => children,
motion: {
span: ({ children }: any) => <span>{children}</span>,
div: ({ children, initial, animate, exit, transition, ...rest }: any) => (
<div {...rest}>{children}</div>
),
},
}));
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
let root: Root;
let container: HTMLDivElement;
let cache: QueryClient;
async function settle() {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 20));
});
}
async function click(text: string) {
const button = [...container.querySelectorAll("button")].find(
(b) => b.textContent?.trim() === text,
);
expect(button, `Missing button ${text}`).toBeTruthy();
await act(async () => button!.click());
await settle();
}
async function fill(label: string, value: string) {
const input = container.querySelector(
`[aria-label="${label}"]`,
) as HTMLInputElement;
expect(input).toBeTruthy();
await act(async () => {
Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
)!.set!.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function render(adapter = "pi_local", runnerProvider = "codex") {
state.params = new URLSearchParams({
name: "Atlas",
adapterType: adapter,
runnerProvider,
});
await renderCurrentParams();
}
async function renderCurrentParams() {
await act(async () =>
root.render(
<QueryClientProvider client={cache}>
<TooltipProvider><NewAgent /></TooltipProvider>
</QueryClientProvider>,
),
);
await settle();
}
async function connect(provider: string) {
await click(provider + "Subscription");
await click("Connect");
}
const pass = {
adapterType: "pi_local",
status: "pass",
checks: [
{ code: "hello_probe_passed", level: "info", message: "Model replied" },
],
testedAt: "2026-09-07T00:00:00Z",
};
beforeEach(() => {
vi.clearAllMocks();
adapterApi.list.mockImplementation(async () => state.adapters);
managedApi.list.mockResolvedValue({ currentUserId: "user-1", canManageConnections: true, connections: [] });
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
cache = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
state.adapters = [
"claude_local",
"codex_local",
"opencode_local",
"pi_local",
"paperclip_runner", "cursor_cloud", "cursor", "gemini_local", "kimi_local", "grok_local", "hermes_local", "hermes_gateway",
].map((type) => ({ type, loaded: true, disabled: false }));
api.adapterModels.mockResolvedValue([]);
api.list.mockResolvedValue([{ id: "ceo", role: "ceo", status: "idle" }]);
api.getAdapterAuthSignal.mockResolvedValue({ status: "present" });
api.getClaudeOAuthTokenStatus.mockRejectedValue(new ApiError("Not found", 404, null));
api.testEnvironment.mockResolvedValue(pass);
api.hire.mockImplementation(async (_company, input) => ({
agent: { ...input, id: "new-agent", status: "idle", urlKey: "atlas" },
}));
envApi.list.mockResolvedValue([
{ id: "local-1", name: "Local", driver: "local", config: {} },
]);
envApi.capabilities.mockResolvedValue({ sandboxProviders: {} });
settings.get.mockResolvedValue({ defaultEnvironmentId: "local-1" });
settings.getExperimental.mockResolvedValue({ enableNativeRunner: true });
settings.getGeneral.mockResolvedValue({ executionMode: "any" });
secrets.list.mockResolvedValue([]);
secrets.create.mockResolvedValue({ id: "org-secret-1" });
secrets.remove.mockResolvedValue({ ok: true });
secrets.listMyUserSecrets.mockResolvedValue([]);
secrets.createUserSecretDefinition.mockResolvedValue({ id: "definition-1" });
secrets.createMyUserSecret.mockResolvedValue({ id: "secret-1" });
secrets.removeUserSecretDefinition.mockResolvedValue({ ok: true });
});
afterEach(async () => {
await act(async () => root.unmount());
cache.clear();
container.remove();
});
export { api, envApi, settings, secrets, state, managedApi, adapterApi, root, container, cache, settle, click, fill, render, connect, pass, renderCurrentParams };
+7 -205
View File
@@ -1,204 +1,13 @@
// @vitest-environment jsdom
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TooltipProvider } from "@/components/ui/tooltip";
import { describe, expect, it, vi } from "vitest";
import { queryKeys } from "@/lib/queryKeys";
import { NewAgent } from "./NewAgent";
import { ApiError } from "@/api/client";
import {
api, envApi, settings, secrets, state, managedApi, adapterApi,
root, container, cache, settle, click, fill, render, connect, pass,
renderCurrentParams,
} from "./NewAgent.test-support";
const api = vi.hoisted(() => ({
get: vi.fn(),
adapterModels: vi.fn(),
list: vi.fn(),
hire: vi.fn(),
testEnvironment: vi.fn(),
getAdapterAuthSignal: vi.fn(),
getClaudeOAuthTokenStatus: vi.fn(),
}));
const envApi = vi.hoisted(() => ({ list: vi.fn(), capabilities: vi.fn() }));
const settings = vi.hoisted(() => ({
get: vi.fn(),
getExperimental: vi.fn(),
getGeneral: vi.fn(),
}));
const secrets = vi.hoisted(() => ({
create: vi.fn(),
remove: vi.fn(),
list: vi.fn(),
listMyUserSecrets: vi.fn(),
createUserSecretDefinition: vi.fn(),
createMyUserSecret: vi.fn(),
rotateMyUserSecret: vi.fn(),
removeUserSecretDefinition: vi.fn(),
}));
const state = vi.hoisted(() => ({
params: new URLSearchParams(),
adapters: [] as object[],
navigate: vi.fn(),
openNewIssue: vi.fn(),
}));
const managedApi = vi.hoisted(() => ({
list: vi.fn(async () => ({ currentUserId: "user-1", canManageConnections: true, connections: [] })),
create: vi.fn(async () => ({ connectionId: "managed-connection", grantId: "managed-grant" })),
setDefault: vi.fn(async () => ({})),
}));
vi.mock("@/api/ai-connections", () => ({ aiConnectionsApi: managedApi }));
vi.mock("@/api/agents", () => ({ agentsApi: api }));
vi.mock("@/api/environments", () => ({ environmentsApi: envApi }));
vi.mock("@/api/instanceSettings", () => ({ instanceSettingsApi: settings }));
vi.mock("@/api/secrets", () => ({ secretsApi: secrets }));
const adapterApi = vi.hoisted(() => ({ list: vi.fn() }));
vi.mock("@/api/adapters", () => ({ adaptersApi: adapterApi }));
vi.mock("@/context/CompanyContext", () => ({
useCompany: () => ({ selectedCompanyId: "company-1" }),
}));
vi.mock("../context/BreadcrumbContext", () => ({
useBreadcrumbs: () => ({ setBreadcrumbs: vi.fn() }),
}));
vi.mock("@/context/DialogContext", () => ({
useDialogActions: () => ({ openNewIssue: state.openNewIssue }),
}));
vi.mock("@/lib/router", () => ({
useNavigate: () => state.navigate,
useSearchParams: () => [state.params],
}));
// Exercise API/persistence contracts with deterministic presentation primitives.
// The actual searchable dropdown and login panel are covered by their tests and browser verification.
vi.mock("@/components/AgentConfigForm", () => ({
ModelDropdown: ({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) => (
<input
aria-label="Model"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
AdapterLoginPanel: ({ onConnected }: { onConnected: (id: string) => void }) => (
<button onClick={() => onConnected("login-session")}>
Complete subscription login
</button>
),
}));
vi.mock("@/components/onboarding/PillGuy", () => ({ PillGuy: () => null }));
vi.mock("motion/react", () => ({
AnimatePresence: ({ children }: any) => children,
MotionConfig: ({ children }: any) => children,
motion: {
span: ({ children }: any) => <span>{children}</span>,
div: ({ children, initial, animate, exit, transition, ...rest }: any) => (
<div {...rest}>{children}</div>
),
},
}));
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
let root: Root;
let container: HTMLDivElement;
let cache: QueryClient;
async function settle() {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 20));
});
}
async function click(text: string) {
const button = [...container.querySelectorAll("button")].find(
(b) => b.textContent?.trim() === text,
);
expect(button, `Missing button ${text}`).toBeTruthy();
await act(async () => button!.click());
await settle();
}
async function fill(label: string, value: string) {
const input = container.querySelector(
`[aria-label="${label}"]`,
) as HTMLInputElement;
expect(input).toBeTruthy();
await act(async () => {
Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
)!.set!.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function render(adapter = "pi_local", runnerProvider = "codex") {
state.params = new URLSearchParams({
name: "Atlas",
adapterType: adapter,
runnerProvider,
});
await act(async () =>
root.render(
<QueryClientProvider client={cache}>
<TooltipProvider><NewAgent /></TooltipProvider>
</QueryClientProvider>,
),
);
await settle();
}
async function connect(provider: string) {
await click(provider + "Subscription");
await click("Connect");
}
const pass = {
adapterType: "pi_local",
status: "pass",
checks: [
{ code: "hello_probe_passed", level: "info", message: "Model replied" },
],
testedAt: "2026-09-07T00:00:00Z",
};
beforeEach(() => {
vi.clearAllMocks();
adapterApi.list.mockImplementation(async () => state.adapters);
managedApi.list.mockResolvedValue({ currentUserId: "user-1", canManageConnections: true, connections: [] });
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
cache = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
state.adapters = [
"claude_local",
"codex_local",
"opencode_local",
"pi_local",
"paperclip_runner", "cursor_cloud", "cursor", "gemini_local", "kimi_local", "grok_local", "hermes_local", "hermes_gateway",
].map((type) => ({ type, loaded: true, disabled: false }));
api.adapterModels.mockResolvedValue([]);
api.list.mockResolvedValue([{ id: "ceo", role: "ceo", status: "idle" }]);
api.getAdapterAuthSignal.mockResolvedValue({ status: "present" });
api.getClaudeOAuthTokenStatus.mockRejectedValue(new ApiError("Not found", 404, null));
api.testEnvironment.mockResolvedValue(pass);
api.hire.mockImplementation(async (_company, input) => ({
agent: { ...input, id: "new-agent", status: "idle", urlKey: "atlas" },
}));
envApi.list.mockResolvedValue([
{ id: "local-1", name: "Local", driver: "local", config: {} },
]);
envApi.capabilities.mockResolvedValue({ sandboxProviders: {} });
settings.get.mockResolvedValue({ defaultEnvironmentId: "local-1" });
settings.getExperimental.mockResolvedValue({ enableNativeRunner: true });
settings.getGeneral.mockResolvedValue({ executionMode: "any" });
secrets.list.mockResolvedValue([]);
secrets.create.mockResolvedValue({ id: "org-secret-1" });
secrets.remove.mockResolvedValue({ ok: true });
secrets.listMyUserSecrets.mockResolvedValue([]);
secrets.createUserSecretDefinition.mockResolvedValue({ id: "definition-1" });
secrets.createMyUserSecret.mockResolvedValue({ id: "secret-1" });
secrets.removeUserSecretDefinition.mockResolvedValue({ ok: true });
});
afterEach(async () => {
await act(async () => root.unmount());
cache.clear();
container.remove();
});
describe("New agent setup", () => {
it("waits for the current instance default instead of testing cached SSH discovery", async () => {
let finish!: (value: object) => void;
@@ -663,14 +472,7 @@ describe("New agent setup", () => {
adapterType: "pi_local",
createdAgentId: "saved-agent",
});
await act(async () =>
root.render(
<QueryClientProvider client={cache}>
<TooltipProvider><NewAgent /></TooltipProvider>
</QueryClientProvider>,
),
);
await settle();
await renderCurrentParams();
expect(container.textContent).toContain("Your agent is ready");
expect(container.textContent).not.toContain("Finish setup");
expect(api.hire).not.toHaveBeenCalled();
+19 -1
View File
@@ -162,6 +162,9 @@ function installStorybookApiFixtures() {
if (url.pathname === "/api/instance/settings/experimental") {
return Response.json(experimentalSettings());
}
if (url.pathname === "/api/instance/settings/general") {
return Response.json(instanceGeneralSettingsSchema.parse({}));
}
if (url.pathname === "/api/health") {
return Response.json({ deploymentMode: "authenticated", localAiLoginSupported: onboardingFixtureState.environments === "local" });
@@ -482,9 +485,11 @@ function installStorybookApiFixtures() {
type: "claude_local",
label: "Claude Code",
source: "builtin",
modelsCount: 2,
modelsCount: 4,
loaded: true,
disabled: false,
supportedRunners: ["paperclip", "legacy"],
defaultRunner: "legacy",
capabilities: {
supportsInstructionsBundle: true,
supportsSkills: true,
@@ -510,6 +515,8 @@ function installStorybookApiFixtures() {
modelsCount: 3,
loaded: true,
disabled: false,
supportedRunners: ["paperclip", "legacy"],
defaultRunner: "legacy",
capabilities: {
supportsInstructionsBundle: true,
supportsSkills: true,
@@ -522,6 +529,16 @@ function installStorybookApiFixtures() {
},
},
},
{
type: "paperclip_runner",
label: "Paperclip Runner",
source: "builtin",
modelsCount: 0,
loaded: true,
disabled: false,
supportedRunners: ["paperclip"],
defaultRunner: "paperclip",
},
]);
}
@@ -532,6 +549,7 @@ function installStorybookApiFixtures() {
const [, , resource] = adapterModelsMatch;
if (resource === "models") {
return Response.json([
{ id: "claude-opus-5-5", label: "Claude Opus 5.5" },
{ id: "claude-opus-4-7", label: "Claude Opus 4.7" },
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
{ id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },