mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
Merge verified provider-routing task-card repair
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
f485863c21
4 files changed
+45
-8
No files matched your search
@@ -651,3 +651,14 @@ subscription combinations remain unqualified. Gemini CLI 0.58.0 has an upstream
|
||||
ACP new-file error conversion defect. The provider-free filesystem probe exposes
|
||||
that defect without modifying the installed CLI. Provider overloads and one
|
||||
follow-up timeout also remain live qualification limits.
|
||||
|
||||
### Gateway completion compatibility
|
||||
|
||||
Provider-facing `paperclip_finish` accepts an omitted or `null` continuation for
|
||||
`done`, `completed`, and `needs_review`. This supports gateways that require all
|
||||
declared tool properties to be present. Normalization removes only `null`;
|
||||
non-yielding tool calls still reject a continuation object, and `yielded` still
|
||||
requires a complete `response_wake` object.
|
||||
|
||||
Task-card account repair uses the provider reconnect form for routed accounts,
|
||||
retaining the saved endpoint, protocol, model aliases, and connection identity.
|
||||
@@ -66,12 +66,6 @@ effects, audit, retries, conflicts, redaction, continuation, and terminal
|
||||
decisions. The production adapter stays App-owned and invokes Paperclip's real
|
||||
route/service authorities; it does not copy those rules into this package.
|
||||
|
||||
Provider-facing `paperclip_finish` accepts an omitted or `null` continuation for
|
||||
`done`, `completed`, and `needs_review`. This supports gateways that require all
|
||||
declared tool properties to be present. Normalization removes only `null`;
|
||||
non-yielding tool calls still reject a continuation object, and `yielded` still
|
||||
requires a complete `response_wake` object.
|
||||
|
||||
## Quick start
|
||||
|
||||
Native provider debug-trace correlation uses an incremental index owned by its transport. Pending
|
||||
|
||||
@@ -30,6 +30,7 @@ vi.mock("@/api/email", () => ({ emailApi: {
|
||||
} }));
|
||||
|
||||
const credentialRender = vi.hoisted(() => vi.fn());
|
||||
const routedCredentialRender = vi.hoisted(() => vi.fn());
|
||||
const setupOptionsMock = vi.hoisted(() => vi.fn());
|
||||
const completeMock = vi.hoisted(() => vi.fn());
|
||||
const declineMock = vi.hoisted(() => vi.fn());
|
||||
@@ -68,6 +69,16 @@ vi.mock("@/components/ai-connections/AiConnectionCredentialStep", () => ({
|
||||
</div>; },
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ai-connections/AiProviderSetup", () => ({
|
||||
AiProviderSetup: (props: { reconnect: { id: string }; onComplete: (binding: { connectionId: string; grantId: string; method: "api_key" }) => void; onCancel: () => void }) => {
|
||||
routedCredentialRender(props);
|
||||
return <div data-testid="routed-ai-repair">
|
||||
<button onClick={() => props.onComplete({ connectionId: props.reconnect.id, grantId: "grant", method: "api_key" })}>Reconnect routed account</button>
|
||||
<button onClick={props.onCancel}>Cancel routed repair</button>
|
||||
</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./ConnectionSetupFlow", () => ({
|
||||
ConnectionSetupFlow: (props: {
|
||||
requestedAgentId?: string;
|
||||
@@ -477,6 +488,21 @@ describe("ConnectionIntentInteractionBody dialog behavior", () => {
|
||||
describe("AI repair inside the card", () => {
|
||||
const interaction: ConnectionIntentInteraction = { ...pendingConnectionIntentInteraction, payload: { ...pendingConnectionIntentInteraction.payload, purpose: "ai" } };
|
||||
const connection = { id: "selected-account", name: "My Codex account", provider: "openai", method: "api_key", ownership: "personal", ownerName: "Dotta", status: "revoked" };
|
||||
it.each(["openrouter", "bedrock", "gateway"] as const)("retains the selected %s route through task-card account repair", async kind => {
|
||||
const routing = kind === "bedrock"
|
||||
? { kind, protocol: "bedrock", region: "us-east-1", auth: "bearer", models: [] }
|
||||
: { kind, protocol: "responses", auth: "bearer", models: [], ...(kind === "gateway" ? { baseUrl: "https://gateway.example/v1" } : {}) };
|
||||
const routedConnection = { ...connection, provider: kind === "bedrock" ? "anthropic" : kind === "openrouter" ? "openrouter" : "openai", routing };
|
||||
setupOptionsMock.mockResolvedValue({ interaction, existingConnections: [], aiRepair: { connection: routedConnection, canReconnect: true } });
|
||||
completeMock.mockResolvedValue({ ...interaction, status: "accepted" });
|
||||
renderBody(interaction); await flush();
|
||||
await act(() => button("Fix connection")!.click());
|
||||
expect(document.querySelector('[data-testid="shared-ai-credentials"]')).toBeNull();
|
||||
expect(routedCredentialRender.mock.lastCall![0]).toMatchObject({ companyId: interaction.companyId, agentId: interaction.payload.requestingAgentId, reconnect: routedConnection });
|
||||
await act(() => button("Reconnect routed account")!.click());
|
||||
expect(completeMock).toHaveBeenCalledWith(interaction.id, connection.id);
|
||||
});
|
||||
|
||||
it.each([false, true])("requires atomic validated legacy adoption (validation fails: %s)", async (fails) => {
|
||||
const binding = { provider: "openai", method: "subscription", mode: "responsible_user" };
|
||||
setupOptionsMock.mockResolvedValue({ interaction, existingConnections: [], aiConnection: binding, aiConnectionRequiresAdoption: true });
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AiProviderSetup } from "@/components/ai-connections/AiProviderSetup";
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -15,6 +14,7 @@ import { connectionIntentsApi } from "@/api/connection-intents";
|
||||
import { aiConnectionsApi } from "@/api/ai-connections";
|
||||
import { agentsApi } from "@/api/agents";
|
||||
import { AiConnectionCredentialStep } from "@/components/ai-connections/AiConnectionCredentialStep";
|
||||
import { AiProviderSetup } from "@/components/ai-connections/AiProviderSetup";
|
||||
import { defaultAiConnectionName } from "@/components/ai-connections/model";
|
||||
import { AppLogo } from "@/pages/apps/AppLogo";
|
||||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
@@ -394,7 +394,13 @@ export function ConnectionIntentInteractionBody({
|
||||
{completeMutation.isPending ? "Continuing…" : "Continue task"}
|
||||
</Button>
|
||||
</div>
|
||||
: repair ? repair.canReconnect ? repair.connection.routing ? <AiProviderSetup companyId={interaction.companyId} agentId={interaction.payload.requestingAgentId} reconnect={repair.connection} onComplete={result => selectAiAccountMutation.mutate({ ...result, generation })} onCancel={() => { closeSetup(); returnFocusToCard(); }} /> : <AiConnectionCredentialStep
|
||||
: repair ? repair.canReconnect ? repair.connection.routing ? <AiProviderSetup
|
||||
companyId={interaction.companyId}
|
||||
agentId={interaction.payload.requestingAgentId}
|
||||
reconnect={repair.connection}
|
||||
onComplete={(binding) => selectAiAccountMutation.mutate({ connectionId: binding.connectionId, grantId: binding.grantId, method: binding.method ?? "api_key", generation })}
|
||||
onCancel={() => { closeSetup(); returnFocusToCard(); }}
|
||||
/> : <AiConnectionCredentialStep
|
||||
companyId={interaction.companyId}
|
||||
provider={repair.connection.provider}
|
||||
initialMethod={repair.connection.method}
|
||||
|
||||
Reference in new issue
Block a user