mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
Integrate current Copilot admission and setup repairs
Co-Authored-By: Paperclip <noreply@paperclip.ing> * codex/copilot-saved-token-setup-v30: Preserve Copilot task catalogs and repair ordinary startup Align Copilot contract history tests with profile v33
This commit is contained in:
commit
6a47aed265
10 files changed
+107
-11
No files matched your search
+3
-3
@@ -4266,7 +4266,7 @@
|
||||
"checkConstraints": {
|
||||
"ai_connection_defaults_provider_check": {
|
||||
"name": "ai_connection_defaults_provider_check",
|
||||
"value": "\"ai_connection_defaults\".\"provider\" in ('anthropic','openai','openrouter','xai','google','github')"
|
||||
"value": "\"ai_connection_defaults\".\"provider\" in ('anthropic','openai','openrouter','xai','google')"
|
||||
},
|
||||
"ai_connection_defaults_method_check": {
|
||||
"name": "ai_connection_defaults_method_check",
|
||||
@@ -4739,7 +4739,7 @@
|
||||
"checkConstraints": {
|
||||
"ai_provider_defaults_provider_check": {
|
||||
"name": "ai_provider_defaults_provider_check",
|
||||
"value": "\"ai_provider_defaults\".\"provider\" in ('anthropic','openai','openrouter','xai','google','github')"
|
||||
"value": "\"ai_provider_defaults\".\"provider\" in ('anthropic','openai','openrouter','xai','google')"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
@@ -56355,4 +56355,4 @@
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -391,7 +391,7 @@ describe("NativeExecutionInputV1", () => {
|
||||
})).toThrow("does not match");
|
||||
});
|
||||
|
||||
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => {
|
||||
it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33] as const)("decodes closed Copilot profile history at version %s without admitting a future version", (agentProfileVersion) => {
|
||||
const { qualificationModel: _qualificationModel, reportedModelId: _reportedModelId,
|
||||
permissionPolicy, qualificationStatus: _qualificationStatus, modelPolicy: _modelPolicy,
|
||||
...snapshot } = QUALIFIED_ACPX_PROFILES.copilot;
|
||||
@@ -403,7 +403,7 @@ describe("NativeExecutionInputV1", () => {
|
||||
const parsed = parseNativeExecutionInput(value);
|
||||
expect(parsed.provider).toEqual(provider);
|
||||
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
|
||||
for (const unsupportedVersion of [0, 33, 1.5, "32", null]) {
|
||||
for (const unsupportedVersion of [0, 34, 1.5, "33", null]) {
|
||||
expect(() => parseNativeExecutionInput({
|
||||
...value, provider: { ...provider, profile: { ...provider.profile, agentProfileVersion: unsupportedVersion } },
|
||||
})).toThrow("qualified ACPX v1 profile");
|
||||
|
||||
@@ -49,6 +49,20 @@ describe("idle admission", () => {
|
||||
expect(idleWorkSnapshot().active).toBe(0);
|
||||
});
|
||||
|
||||
it("tracks ordinary middleware with non-router stack metadata", async () => {
|
||||
const server = app(), done = deferred();
|
||||
const middleware = Object.assign(async (_req: express.Request, res: express.Response) => {
|
||||
res.sendStatus(202); await done.promise;
|
||||
}, { stack: { name: "middleware metadata" } });
|
||||
server.use(middleware);
|
||||
expect(() => trackIdleRequestHandlers(server)).not.toThrow();
|
||||
await request(server).get("/work").expect(202);
|
||||
expect(idleWorkSnapshot().active).toBe(1);
|
||||
done.resolve();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(idleWorkSnapshot().active).toBe(0);
|
||||
});
|
||||
|
||||
it("does not mistake a client disconnect for completed accepted work", async () => {
|
||||
const server = app();
|
||||
const entered = deferred(), done = deferred(), closed = deferred();
|
||||
|
||||
@@ -61,7 +61,7 @@ export function trackIdleRequestHandlers(app: Application): void {
|
||||
if (seen.has(layer)) continue;
|
||||
seen.add(layer);
|
||||
if (layer.route) { visit(layer.route.stack, true); continue; }
|
||||
if (layer.handle.stack) { visit(layer.handle.stack); continue; }
|
||||
if (Array.isArray(layer.handle.stack)) { visit(layer.handle.stack); continue; }
|
||||
const original = layer.handle;
|
||||
if (original === idleAdmissionMiddleware) continue;
|
||||
const invoke = (req: Request, args: unknown[]) => {
|
||||
|
||||
@@ -2359,6 +2359,23 @@ describe("IssueProperties", () => {
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("discovers Copilot overrides with the assignee connection and environment", async () => {
|
||||
const binding = { provider: "github", method: "api_key", mode: "shared", connectionId: "11111111-1111-4111-8111-111111111111", grantId: "33333333-3333-4333-8333-333333333333" };
|
||||
mockAgentsApi.list.mockResolvedValue([{ id: "agent-1", name: "Copilot", role: "engineer", status: "active",
|
||||
adapterType: "paperclip_runner", defaultEnvironmentId: "daytona-1",
|
||||
adapterConfig: { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" },
|
||||
runtimeConfig: { aiConnection: binding } }]);
|
||||
mockAgentsApi.adapterModels.mockResolvedValue([{ id: "gpt-5.6-luna", label: "Copilot Luna" }]);
|
||||
const root = renderProperties(container, {
|
||||
issue: createIssue({ assigneeAgentId: "agent-1", assigneeAdapterOverrides: { adapterConfig: { model: "gpt-5.6-luna" } } }),
|
||||
childIssues: [], onUpdate: vi.fn(),
|
||||
});
|
||||
await waitForAssertion(() => expect(mockAgentsApi.adapterModels).toHaveBeenCalledWith("company-1", "paperclip_runner", {
|
||||
environmentId: "daytona-1", provider: "acpx", acpxAgent: "copilot", agentId: "agent-1", aiConnection: binding,
|
||||
}));
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("edits existing custom assignee model options from the properties pane", async () => {
|
||||
const onUpdate = vi.fn();
|
||||
mockAgentsApi.list.mockResolvedValue([
|
||||
|
||||
@@ -34,6 +34,7 @@ import { projectsApi } from "../../api/projects";
|
||||
import { useCompany } from "../../context/CompanyContext";
|
||||
import { useSidebar } from "../../context/SidebarContext";
|
||||
import { queryKeys } from "../../lib/queryKeys";
|
||||
import { copilotTaskModelCatalogOptions } from "../../lib/copilot-task-model-catalog";
|
||||
import { buildCompanyUserInlineOptions, buildCompanyUserLabelMap, buildCompanyUserProfileMap, isAgentTaskTarget } from "../../lib/company-members";
|
||||
import { ISSUE_OVERRIDE_ADAPTER_TYPES, type IssueModelLane } from "../../lib/issue-assignee-overrides";
|
||||
import { useProjectOrder } from "../../hooks/useProjectOrder";
|
||||
@@ -729,11 +730,12 @@ export function IssueProperties({
|
||||
const assignee = issue.assigneeAgentId
|
||||
? agents?.find((a) => a.id === issue.assigneeAgentId)
|
||||
: null;
|
||||
const copilotCatalog = copilotTaskModelCatalogOptions(assignee);
|
||||
const assigneeAdapterType = assignee?.adapterType ?? null;
|
||||
const assigneeAdapterOverrides = issue.assigneeAdapterOverrides ?? null;
|
||||
const showAssigneeAdapterOptions = assigneeAdapterOverrides !== null;
|
||||
const supportsAssigneeOverrides = Boolean(
|
||||
assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType),
|
||||
assigneeAdapterType && (ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType) || copilotCatalog),
|
||||
);
|
||||
const assigneeOverrideLane = overrideLane(assigneeAdapterOverrides);
|
||||
const assigneeOverrideAdapterConfig = asRecord(assigneeAdapterOverrides?.adapterConfig);
|
||||
@@ -750,12 +752,16 @@ export function IssueProperties({
|
||||
const assigneeOverrideChrome = assigneeAdapterType === "claude_local"
|
||||
&& assigneeOverrideAdapterConfig.chrome === true;
|
||||
const catalogProvider = assigneeAdapterType === "paperclip_runner" ? String(normalizeLegacyRunnerProvider(assigneePrimaryAdapterConfig).provider ?? "codex") : undefined;
|
||||
const catalogEnvironmentId = copilotCatalog ? assignee?.defaultEnvironmentId ?? null : null;
|
||||
const { data: assigneeAdapterModels } = useQuery({
|
||||
queryKey:
|
||||
companyId && assigneeAdapterType
|
||||
? queryKeys.agents.adapterModels(companyId, assigneeAdapterType, null, catalogProvider)
|
||||
? [...queryKeys.agents.adapterModels(companyId, assigneeAdapterType, catalogEnvironmentId, catalogProvider), copilotCatalog ?? null]
|
||||
: ["agents", "none", "adapter-models", assigneeAdapterType ?? "none"],
|
||||
queryFn: () => agentsApi.adapterModels(companyId!, assigneeAdapterType!, { provider: catalogProvider }),
|
||||
queryFn: () => agentsApi.adapterModels(companyId!, assigneeAdapterType!, {
|
||||
provider: catalogProvider,
|
||||
...(copilotCatalog ? { environmentId: catalogEnvironmentId, ...copilotCatalog } : {}),
|
||||
}),
|
||||
enabled: Boolean(companyId) && showAssigneeAdapterOptions && supportsAssigneeOverrides,
|
||||
});
|
||||
const modelOverrideOptions = useMemo<InlineEntityOption[]>(() => {
|
||||
|
||||
@@ -259,6 +259,26 @@ describe("composer assignee picker", () => {
|
||||
expect(choices.some((item) => item.includes("GPT-6 Sol"))).toBe(false);
|
||||
});
|
||||
|
||||
it("discovers Copilot task models with the assignee connection and environment", async () => {
|
||||
const binding = { provider: "github", method: "api_key", mode: "shared", connectionId: "11111111-1111-4111-8111-111111111111", grantId: "33333333-3333-4333-8333-333333333333" } as const;
|
||||
const copilot = { ...agent, adapterType: "paperclip_runner", defaultEnvironmentId: "daytona-1",
|
||||
adapterConfig: { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" },
|
||||
runtimeConfig: { aiConnection: binding } } as Agent;
|
||||
const loadModels = vi.spyOn(agentsApi, "adapterModels").mockResolvedValueOnce([
|
||||
{ id: "gpt-5.6-luna", label: "Copilot Luna" },
|
||||
]);
|
||||
render(vi.fn(), vi.fn(), true, { agents: new Map([[agent.id, copilot]]), settings: null });
|
||||
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); });
|
||||
expect(loadModels).toHaveBeenCalledWith("company-1", "paperclip_runner", {
|
||||
environmentId: "daytona-1", provider: "acpx", poolId: undefined,
|
||||
acpxAgent: "copilot", agentId: agent.id, aiConnection: binding,
|
||||
});
|
||||
await click("Select model and effort");
|
||||
await click("Choose exact model");
|
||||
expect(document.body.textContent).toContain("Copilot Luna");
|
||||
expect(document.body.textContent).not.toContain("Claude Sonnet");
|
||||
});
|
||||
|
||||
it("preserves settings when the selected assignee is chosen again", async () => {
|
||||
const onAssigneeChange = vi.fn();
|
||||
const onSettingsChange = vi.fn();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ArrowLeft, Check, ChevronDown, Plus, RotateCcw, Search, X, Zap } from "
|
||||
import { aiRuntimeConnectionBindingSchema, type Agent, type IssueAssigneeAdapterOverrides } from "@paperclipai/shared";
|
||||
import { agentsApi, type AdapterModel } from "@/api/agents";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { copilotTaskModelCatalogOptions } from "@/lib/copilot-task-model-catalog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMobileEntityPickerViewportStyle } from "@/hooks/useMobileEntityPickerViewportStyle";
|
||||
import { getLastComposerEffort, rememberComposerEffort } from "@/lib/recent-composer-effort";
|
||||
@@ -106,12 +107,13 @@ export function ComposerRunSettingsPicker({
|
||||
const provider = composerCatalogProvider(agent);
|
||||
const binding = aiRuntimeConnectionBindingSchema.safeParse(agent?.runtimeConfig?.aiConnection).data;
|
||||
const poolId = binding?.mode === "router" ? binding.connectionId : undefined;
|
||||
const copilotCatalog = copilotTaskModelCatalogOptions(agent);
|
||||
const { data: fetchedModels = [], isPending: modelsPending } = useQuery({
|
||||
queryKey: agent && modelSupported
|
||||
? [...queryKeys.agents.adapterModels(companyId, agent.adapterType, agent.defaultEnvironmentId ?? null, provider), poolId ?? null]
|
||||
? [...queryKeys.agents.adapterModels(companyId, agent.adapterType, agent.defaultEnvironmentId ?? null, provider), poolId ?? null, copilotCatalog ?? null]
|
||||
: ["agents", "composer-models", "none"],
|
||||
queryFn: () => agentsApi.adapterModels(companyId, agent!.adapterType, {
|
||||
environmentId: agent!.defaultEnvironmentId ?? null, provider, poolId,
|
||||
environmentId: agent!.defaultEnvironmentId ?? null, provider, poolId, ...copilotCatalog,
|
||||
}),
|
||||
enabled: Boolean(agent && modelSupported && !modelOptionsOverride),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
import { copilotTaskModelCatalogOptions } from "./copilot-task-model-catalog";
|
||||
|
||||
describe("Copilot task model catalog identity", () => {
|
||||
const agent = { id: "copilot", adapterType: "paperclip_runner", adapterConfig: {
|
||||
provider: "acpx", acpxAgent: "copilot",
|
||||
} } as unknown as Agent;
|
||||
it("changes the catalog identity when a saved connection changes", () => {
|
||||
const a = copilotTaskModelCatalogOptions({ ...agent, runtimeConfig: { aiConnection: {
|
||||
provider: "github", method: "api_key", mode: "shared", connectionId: "11111111-1111-4111-8111-111111111111", grantId: "33333333-3333-4333-8333-333333333333",
|
||||
} } });
|
||||
const b = copilotTaskModelCatalogOptions({ ...agent, runtimeConfig: { aiConnection: {
|
||||
provider: "github", method: "api_key", mode: "shared", connectionId: "22222222-2222-4222-8222-222222222222", grantId: "33333333-3333-4333-8333-333333333333",
|
||||
} } });
|
||||
expect(a).not.toEqual(b);
|
||||
});
|
||||
it("allows normal responsible-user discovery when no explicit binding is saved", () => {
|
||||
expect(copilotTaskModelCatalogOptions(agent)).toEqual({ acpxAgent: "copilot", agentId: "copilot", aiConnection: undefined });
|
||||
});
|
||||
it("does not change catalogs for other runners", () => {
|
||||
expect(copilotTaskModelCatalogOptions({ ...agent, adapterConfig: { provider: "acpx", acpxAgent: "claude" } })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { aiConnectionBindingSchema, type Agent } from "@paperclipai/shared";
|
||||
|
||||
/** Task overrides must discover models through the assignee's saved connection. */
|
||||
export function copilotTaskModelCatalogOptions(agent: Agent | null | undefined) {
|
||||
if (agent?.adapterType !== "paperclip_runner"
|
||||
|| agent.adapterConfig.provider !== "acpx"
|
||||
|| agent.adapterConfig.acpxAgent !== "copilot") return undefined;
|
||||
return {
|
||||
acpxAgent: "copilot",
|
||||
agentId: agent.id,
|
||||
aiConnection: aiConnectionBindingSchema.safeParse(agent.runtimeConfig?.aiConnection).data,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user