mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-19 01:46:15 +02:00
Refresh first-token Copilot discovery and honor native Runner admission in onboarding
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a7212bd765
commit
7010b852e4
4 files changed
+64
-11
No files matched your search
@@ -111,7 +111,7 @@ async function flushReact() {
|
||||
});
|
||||
}
|
||||
|
||||
async function mount() {
|
||||
async function mount(nativeRunnerEnabled?: boolean) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
@@ -124,6 +124,7 @@ async function mount() {
|
||||
session: { id: "session-1", userId: "user-1" },
|
||||
user: { id: "user-1", name: "Example", email: "user-1@example.com", image: null },
|
||||
});
|
||||
if (nativeRunnerEnabled !== undefined) queryClient.setQueryData(queryKeys.instance.experimentalSettings, { enableNativeRunner: nativeRunnerEnabled, enableOpenAiDot: true });
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -288,10 +289,19 @@ describe("OnboardingWizard adapter selection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not offer or retain Copilot when only OpenAI Dot is enabled", async () => {
|
||||
mockAdapterRegistry.list = [{ type: "paperclip_runner" }, { type: "codex_local" }];
|
||||
window.localStorage.setItem(ONBOARDING_STORAGE_KEY, JSON.stringify({ step: 0, adapterType: "paperclip_runner", onboardingCopilot: true, model: "gpt-5.6-luna" }));
|
||||
const { root, container } = await mount(false);
|
||||
expect(JSON.parse(window.localStorage.getItem(ONBOARDING_STORAGE_KEY)!)).toMatchObject({ adapterType: "codex_local", onboardingCopilot: false, model: "" });
|
||||
expect(container.textContent).not.toContain("GitHub Copilot");
|
||||
await act(async () => root.unmount());
|
||||
});
|
||||
|
||||
it("restores a qualified Copilot draft without changing its explicit model", async () => {
|
||||
mockAdapterRegistry.list = [{ type: "paperclip_runner" }];
|
||||
window.localStorage.setItem(ONBOARDING_STORAGE_KEY, JSON.stringify({ step: 0, adapterType: "paperclip_runner", onboardingCopilot: true, model: "gpt-5.6-luna" }));
|
||||
const { root } = await mount();
|
||||
const { root } = await mount(true);
|
||||
expect(JSON.parse(window.localStorage.getItem(ONBOARDING_STORAGE_KEY)!)).toMatchObject({ adapterType: "paperclip_runner", onboardingCopilot: true, model: "gpt-5.6-luna" });
|
||||
await act(async () => root.unmount());
|
||||
});
|
||||
|
||||
@@ -959,6 +959,7 @@ function OnboardingWizardInner({
|
||||
queryFn: () => instanceSettingsApi.getExperimental(),
|
||||
enabled: effectiveOnboardingOpen && step >= 3 && step <= 5,
|
||||
});
|
||||
const copilotOnboardingEnabled = copilotOnboardingQualified && experimentalSettingsForLogin?.enableNativeRunner === true;
|
||||
const loginEnvironmentResolution = useMemo(() => {
|
||||
try {
|
||||
return { environmentId: resolveAdapterTestEnvironmentId({
|
||||
@@ -1159,14 +1160,14 @@ function OnboardingWizardInner({
|
||||
)
|
||||
.map((a) => ({ ...getAdapterDisplay(a.type), type: a.type }));
|
||||
|
||||
const copilot = copilotOnboardingQualified && !disabledTypes.has("paperclip_runner") && listUIAdapters().some((a) => a.type === "paperclip_runner")
|
||||
const copilot = copilotOnboardingEnabled && !disabledTypes.has("paperclip_runner") && listUIAdapters().some((a) => a.type === "paperclip_runner")
|
||||
? [{ ...getAdapterDisplay("copilot_runtime"), type: "paperclip_runner", label: "GitHub Copilot", recommended: true }]
|
||||
: [];
|
||||
return {
|
||||
recommendedAdapters: [...all.filter((a) => a.recommended), ...copilot],
|
||||
moreAdapters: all.filter((a) => !a.recommended),
|
||||
};
|
||||
}, [disabledTypes]);
|
||||
}, [disabledTypes, copilotOnboardingEnabled]);
|
||||
|
||||
/**
|
||||
* A source chosen from the visible row. Read off the row rather than off
|
||||
@@ -1501,14 +1502,18 @@ function OnboardingWizardInner({
|
||||
// registered once the adapters query resolves, so before that a saved
|
||||
// external adapter is indistinguishable from a disabled one - and snapping
|
||||
// would replace the customer's choice with a built-in and persist it.
|
||||
if (!adapterRegistryLoaded) return;
|
||||
if (!adapterRegistryLoaded || (adapterType === "paperclip_runner" && !loginPolicyReady)) return;
|
||||
const visible = [...recommendedAdapters, ...moreAdapters].filter(
|
||||
(a) => !a.comingSoon,
|
||||
);
|
||||
if (visible.length === 0) return;
|
||||
if (visible.length === 0) {
|
||||
if (adapterType === "paperclip_runner" && !copilotOnboardingEnabled) { setAdapterType("claude_local"); setModel(""); setCopilotConnection(undefined); setSourcePicked(false); }
|
||||
return;
|
||||
}
|
||||
if (visible.some((a) => a.type === adapterType)) return;
|
||||
const next = (visible.find((a) => a.type !== "paperclip_runner") ?? visible[0]).type as AdapterType;
|
||||
setAdapterType(next);
|
||||
if (adapterType === "paperclip_runner") { setModel(""); setCopilotConnection(undefined); }
|
||||
// The snap is not a choice. It replaces a name the customer can no longer
|
||||
// see with the first one they can, which is the right thing to hold — but
|
||||
// holding it *as chosen* would put a filled tile and an open sign-in panel
|
||||
@@ -1530,7 +1535,7 @@ function OnboardingWizardInner({
|
||||
return;
|
||||
}
|
||||
setModel("");
|
||||
}, [adapterRegistryLoaded, recommendedAdapters, moreAdapters, adapterType]);
|
||||
}, [adapterRegistryLoaded, recommendedAdapters, moreAdapters, adapterType, loginPolicyReady, copilotOnboardingEnabled]);
|
||||
|
||||
const COMMAND_PLACEHOLDERS: Record<string, string> = {
|
||||
claude_local: "claude",
|
||||
@@ -2039,7 +2044,7 @@ function OnboardingWizardInner({
|
||||
// doesn't hire a second agent.
|
||||
async function handleGiveHeartbeat() {
|
||||
if (!createdCompanyId) return;
|
||||
if (isCopilot && (!copilotOnboardingQualified || !sourceSelected || !copilotModelReady)) {
|
||||
if (isCopilot && (!copilotOnboardingEnabled || !sourceSelected || !copilotModelReady)) {
|
||||
setError("Connect GitHub Copilot and select an available model before creating this agent.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -864,7 +864,9 @@ function Setup({
|
||||
</>}
|
||||
{!connectionAdapter && aiProviderForAdapter(brandType) && (
|
||||
<AiConnectionField companyId={companyId} agentName={name} adapterType={brandType} model={model} environmentId={environmentId ?? undefined} value={aiBinding}
|
||||
onChange={binding => { binding.mode !== "router" && setRuntimeAiBinding(binding); resetTest(); }} />
|
||||
onChange={binding => { binding.mode !== "router" && setRuntimeAiBinding(binding); resetTest();
|
||||
if (brandType === "copilot_runtime") void cache.invalidateQueries({ queryKey: ["agents", companyId, "adapter-models", brandType] });
|
||||
}} />
|
||||
)}
|
||||
{(connectionModels ? connectionModels.error : models.error) && <p role="alert" className="text-sm text-destructive">Could not load models. Retry or enter a model ID manually.</p>}
|
||||
{((showModel && !usingKimiApi) ||
|
||||
@@ -875,8 +877,8 @@ function Setup({
|
||||
presentation="native"
|
||||
models={connectionModels?.models ?? models.data ?? []}
|
||||
loadingModels={connectionModels?.isLoading ?? models.isLoading}
|
||||
onRefreshModels={connectionModels?.refreshModels}
|
||||
refreshingModels={connectionModels?.refreshing}
|
||||
onRefreshModels={connectionModels?.refreshModels ?? (brandType === "copilot_runtime" ? async () => { await models.refetch(); } : undefined)}
|
||||
refreshingModels={connectionModels?.refreshing ?? (brandType === "copilot_runtime" ? models.isFetching : undefined)}
|
||||
value={model}
|
||||
onChange={(value) => {
|
||||
setModel(connectionModels?.resolveModel(value) ?? value);
|
||||
|
||||
@@ -157,6 +157,7 @@ const pass = {
|
||||
};
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
managedApi.setDefault.mockResolvedValue({});
|
||||
managedApi.list.mockResolvedValue({ currentUserId: "user-1", canManageConnections: true, connections: [] });
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
@@ -200,6 +201,41 @@ afterEach(async () => {
|
||||
container.remove();
|
||||
});
|
||||
describe("New agent setup", () => {
|
||||
it("discovers models after the first Copilot token becomes the responsible user's default", async () => {
|
||||
let defaultSaved = false;
|
||||
managedApi.setDefault.mockImplementation(async () => { defaultSaved = true; return {}; });
|
||||
api.adapterModels.mockImplementation(async () => {
|
||||
if (!defaultSaved) throw new ApiError("Connect Copilot first", 422, null);
|
||||
return [{ id: "gpt-5.6-luna", label: "GPT" }];
|
||||
});
|
||||
await render("paperclip_runner", "copilot");
|
||||
const beforeConnection = api.adapterModels.mock.calls.length;
|
||||
expect(beforeConnection).toBeGreaterThan(0);
|
||||
const trigger = document.querySelector('[role="combobox"][aria-label="Connection"]')!;
|
||||
await act(async () => trigger.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })));
|
||||
await settle();
|
||||
const option = [...document.querySelectorAll<HTMLElement>('[role="option"]')].find(item => item.textContent?.includes("Connect an account"));
|
||||
expect(option).toBeDefined();
|
||||
await act(async () => option!.click());
|
||||
await settle();
|
||||
const input = document.querySelector<HTMLInputElement>('[aria-label="API key"]')!;
|
||||
expect(input).toBeTruthy();
|
||||
await act(async () => {
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, "github_pat_fixture_first");
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
const save = [...document.querySelectorAll<HTMLButtonElement>('[role="dialog"] button')].find(button => button.textContent?.trim() === "Connect")!;
|
||||
expect(save).toBeTruthy();
|
||||
await act(async () => save.click());
|
||||
await settle();
|
||||
expect(managedApi.setDefault).toHaveBeenCalledWith("company-1", "managed-grant");
|
||||
expect(api.adapterModels).toHaveBeenCalledTimes(beforeConnection + 1);
|
||||
expect(api.adapterModels.mock.invocationCallOrder[beforeConnection]).toBeGreaterThan(managedApi.setDefault.mock.invocationCallOrder[0]);
|
||||
expect(api.adapterModels).toHaveBeenLastCalledWith("company-1", "paperclip_runner", expect.objectContaining({
|
||||
provider: "acpx", acpxAgent: "copilot", aiConnection: { provider: "github", method: "api_key", mode: "responsible_user" },
|
||||
}));
|
||||
});
|
||||
|
||||
it("creates Dot through its independent choice without CLI or model setup, then routes to pairing", async () => {
|
||||
settings.getExperimental.mockResolvedValue({ enableNativeRunner: false, enableOpenAiDot: true, enablePublicMcp: true });
|
||||
await render("paperclip_runner", "openai_dot");
|
||||
|
||||
Reference in new issue
Block a user