mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
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:
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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
@@ -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();
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user