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

@@ -177,8 +177,6 @@ jobs:
run: |
set -uo pipefail
pnpm install --frozen-lockfile --ignore-scripts
# Historical evaluation fixtures read these exact public trees.
# Fetch only their objects; keep the selected checkout unchanged.
git fetch --no-tags --depth=1 origin 2a8a99e4a5f69aa803b3f10b982f583e75a87042 16ef1a5744b23bb043a8b286e6288921da1de98f
status=0
for check in 'pnpm -r typecheck' 'pnpm test:run' 'pnpm check:token-gates' 'pnpm test:e2e:runner:typecheck' 'pnpm test:e2e:runner:unit' 'pnpm build' 'if [ -f scripts/verify-grok-npm-install.mjs ]; then node scripts/verify-grok-npm-install.mjs; fi'; do
@@ -696,3 +696,51 @@ Report attachment: `e6206748-a785-4c02-9bbf-f4db8a3b22f9`, work product
Local receipts: `/private/tmp/claude-responsive-preview-evidence.json`,
`/private/tmp/claude-responsive-evidence-upload.json`, and
`/private/tmp/claude-responsive-evidence-independent-check.json`.
## Canonical composition and current contribution rules — 2026-10-12
The source owner committed the stronger shared startup repair as
`ce23716503a2c9158b5a7ab74ab61571ff4ee7a3`. Local additive merges preserve
that parent and this chat's Claude effort/resume changes. Eight controls that do
not need native executables and runner noemit pass in the combined protocol.
The ninth selected readiness-cycle case timed out while waiting for readiness;
read-only resolution confirms both the default runner artifact and fake provider
fixture are absent, with no private runner override. That native case remains a
hosted CI gate, not a pass. No local native build was performed. Logs:
`/private/tmp/claude-canonical-startup-composition-controls.log`,
`/private/tmp/claude-canonical-startup-no-process-controls.log`, and
`/private/tmp/claude-canonical-startup-composition-types.log`.
The source owner then reconciled current master in `9f59ff36cf`, adding the
Storybook catalog and contribution rules against new code comments and code files
crossing 1000 lines. The guided-creation fixture is extracted to
`ui/src/pages/NewAgent.test-support.tsx`; all 65 cases remain and pass, with UI
noemit and token gates passing. Fixture component rendering stays inside its
mock-owning module, including the saved-confirmation reload case. The active test
file no longer crosses the threshold. Our added explanatory comments are removed;
the source owner handles the common transport's comment cleanup. Semantic code
and internal deadlines stay unchanged.
Removing this chat's added sandbox comments changes its raw source attestation to
`144d62b3415cfedf272b5b052e14af1e41fe50a644af84247fc9c20c7ef7edde`.
Immutable Pi profiles 23 and 24 remain retained. New Pi profile 25 binds only that
source revision plus its profile version, digest
`sha256:dd67387487dadb08da76534c057dc0aa059f7698d1779cbd5fc452813828167a`.
SDK/executable closures, platforms, model/effort contract and other providers'
qualification boundaries do not change. Copilot remains pending profile 18.
Generated TS/Rust descriptors, Rust current/prior fixtures, Docker context and
current documentation are synchronized. All 119 profile/factory/admission/effort
controls pass with one existing historical skip, including corrupt-byte rejection
before spawn/bridge; runner noemit and generation checks pass. The first restricted
attempt stopped at loopback lease admission and is retained, not counted as byte
validation. Native/Rust execution and final package qualification remain cloud gates.
The maintain-storybook source review found concrete fixture incompatibilities:
the general-settings endpoint and scoped runner-discovery defaults were absent.
The shared preview now returns that settings contract and explicitly simulated
Legacy discovery for Claude/Codex, exposes the loaded native adapter, and includes
Opus 5.5 while retaining explicit older-model examples. These fixture defaults are
not rollout qualification. Catalog fingerprints/classification and affected story
rendering must be reconciled after the final parent is frozen; source-only review
is not an executed Storybook or provider pass. Published draft heads remain fixed
while the source owner finishes the canonical contribution/catalog cleanup.
-1
View File
@@ -725,7 +725,6 @@ export interface CLIAdapterModule {
export interface CreateConfigValues {
runner?: "auto" | "paperclip" | "legacy";
/** Transient form resolution; never an execution setting or API override. */
resolvedRunner?: "paperclip" | "legacy";
adapterType: string;
codexPermissionMode?: "never" | "on-request" | "untrusted";
@@ -45,8 +45,6 @@ export function buildClaudeLocalConfig(v: CreateConfigValues): Record<string, un
if (Object.keys(env).length > 0) ac.env = env;
const nativeSelected = v.runner === "paperclip"
|| ((v.runner === undefined || v.runner === "auto") && v.resolvedRunner === "paperclip");
// Only a resolved native form omits the Legacy default. Explicit limits
// still reach the server's compatibility check.
if (!nativeSelected || v.maxTurnsPerRun !== 1000) ac.maxTurnsPerRun = v.maxTurnsPerRun;
if (nativeSelected && v.adapterSchemaValues?.acpxPermissionMode !== undefined) {
ac.acpxPermissionMode = v.adapterSchemaValues.acpxPermissionMode;
-2
View File
@@ -7,7 +7,6 @@ export interface AgentRunnerAvailability {
defaultRunner: AgentRunner;
}
/** Only qualified harnesses graduate their creation default. */
export function paperclipRunnerProfileForHarness(harness: string): { provider: "codex" } | { provider: "acpx"; acpxAgent: "claude" } | undefined {
if (harness === "codex_local") return { provider: "codex" };
if (harness === "claude_local") return { provider: "acpx", acpxAgent: "claude" };
@@ -31,7 +30,6 @@ function supportsLinuxLibc(libc?: "glibc" | "musl" | "unknown", version?: string
/** Creation defaults follow GNU/Linux and macOS artifacts; Alpine/musl is deferred. */
export function paperclipRunnerSupportsPlatform(harness: string, platform: string, architecture: string, libc?: "glibc" | "musl" | "unknown", glibcVersion?: string): boolean {
// Claude graduates on Apple Silicon first; other targets await package and live qualification.
if (harness === "claude_local") return platform === "darwin" && architecture === "arm64";
return !!paperclipRunnerProfileForHarness(harness)
// Omitted libc preserves platform-only callers; detected unknown Linux is not qualified.
@@ -276,8 +276,6 @@ describe("issue update comment wakeups", () => {
mockIssueService.getWakeableParentAfterChildCompletion.mockResolvedValue(null);
mockIssueService.getCurrentScheduledRetry.mockResolvedValue(null);
mockIssueService.listReviewAttention.mockResolvedValue(new Map());
// A fresh route graph is fixture preparation, not request execution. Keep
// per-case module resets while charging the cold transform to hookTimeout.
routeFixture = await loadRouteFixture();
});
@@ -11,8 +11,6 @@ import {
} from "./helpers/embedded-postgres.js";
import { teamsCatalogService } from "../services/teams-catalog.js";
// Bind the real creation resolver to the release target under test so the
// catalog contract is stable across the machines running this fixture.
vi.mock("../services/agent-runner-selection.js", async importOriginal => {
const actual = await importOriginal<typeof import("../services/agent-runner-selection.js")>();
return { ...actual, resolveNewAgentRunnerForCompany: async (_db: unknown, _companyId: string, input: Parameters<typeof actual.resolveNewAgentRunnerForCompany>[2]) =>
@@ -55,7 +55,6 @@ export function poolMemberRuntimeConfig(member: AiConnectionPoolMember, adapterT
const efforts = provider === "openai" ? codexLocalReasoningEffortsForModel(model)
: provider === "anthropic" ? claudeLocalReasoningEffortsForModel(model)
: provider === "xai" ? grokLocalReasoningEffortsForModel(model) : [];
// Claude and Codex carry effort through native admission; other ACPX profiles keep their existing limit.
const effortKey = adapterType === "paperclip_runner" ? profile.provider === "codex" || (profile.provider === "acpx" && profile.acpxAgent === "claude") ? "modelReasoningEffort" : null
: provider === "anthropic" ? "effort" : provider === "xai" ? "reasoningEffort" : provider === "openai" ? "modelReasoningEffort" : "variant";
const requested = overrides.modelReasoningEffort ?? overrides.reasoningEffort ?? overrides.effort ?? overrides.variant;
-3
View File
@@ -1,9 +1,6 @@
import { centsToUsd, unitsToCents, usdToUnits } from "@paperclipai/shared";
import type { AdapterUsageCheckpoint } from "@paperclipai/adapter-utils";
// Direct Anthropic API list prices, verified per model below:
// https://platform.claude.com/docs/en/about-claude/pricing
// Keep this bounded to exact qualified models; aliases and other billers stay unknown.
const RATE_CARDS: Record<string, { rates: readonly string[]; version: string }> = {
"claude-sonnet-5": { rates: ["2", "0.2", "4", "10"], version: "anthropic-standard-2026-10-07" },
"claude-opus-5-5": { rates: ["4", "0.2", "8", "20"], version: "anthropic-standard-2026-10-12" },
@@ -167,7 +167,6 @@ export function validatePaperclipRunnerDotConfig(config: Record<string, unknown>
return bindingId;
}
/** A task may change a local Runner model and supported effort, but not provider identity. */
export function projectPaperclipRunnerTaskConfig(
backend: "codex_app_server" | "opencode_server" | "acpx_runtime",
agentConfig: unknown,
-2
View File
@@ -10,8 +10,6 @@ import { buildPaperclipServerEnvironment, buildRunnerE2EProcessEnvironment } fro
const cells = runnerMatrix.filter(e => e.profile.id === "runner-acpx-pi");
const roots: string[] = [];
beforeEach(() => {
// pnpm's test shim can inject NODE_PATH. Installed-startup fixtures model
// the production launcher's clean environment, not that test shim.
vi.stubEnv("NODE_PATH", undefined);
vi.stubEnv("NODE_OPTIONS", undefined);
});
-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" },