mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-06 23:20:53 +01:00
Finish Copilot production setup on current master
Preserve the reviewed candidate source and mainline provider behavior. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
cc1670aa56
commit
aaa65bbfbf
74 files changed
+57873
-251
No files matched your search
@@ -1,4 +1,6 @@
|
||||
import { probeAcpxClaudeInstallation, probeAcpxPiInstallation } from "../vendor/paperclip-runner/live/index.js";
|
||||
import { probeCopilotExecutionTarget } from "../services/copilot-connection-probe.js";
|
||||
import { HttpError } from "../errors.js";
|
||||
import { probeAcpxClaudeInstallation, probeAcpxCursorInstallation, probeAcpxPiInstallation } from "../vendor/paperclip-runner/live/index.js";
|
||||
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
||||
import { buildSandboxNpmInstallCommand } from "@paperclipai/adapter-utils";
|
||||
import type { ServerAdapterModule } from "../adapters/index.js";
|
||||
@@ -17,6 +19,8 @@ import {
|
||||
setOverridePaused,
|
||||
} from "../adapters/registry.js";
|
||||
|
||||
vi.mock("../services/copilot-connection-probe.js",()=>({probeCopilotExecutionTarget:vi.fn(async()=>({status:"verified",models:[{id:"gpt-5.6-luna"}]}))}));
|
||||
|
||||
vi.mock("../vendor/paperclip-runner/live/index.js", () => ({
|
||||
probeAcpxClaudeInstallation: vi.fn(async () => undefined),
|
||||
probeAcpxGrokInstallation: vi.fn(async () => undefined),
|
||||
@@ -348,20 +352,29 @@ describe("server adapter registry", () => {
|
||||
});
|
||||
expect(probe).toHaveBeenLastCalledWith("openrouter/deepseek/deepseek-v4-flash-0731");
|
||||
});
|
||||
it("reports qualification-only readiness for an exact host-authorized candidate", async () => {
|
||||
const key = "PAPERCLIP_RUNNER_ACPX_QUALIFICATION";
|
||||
const previous = process.env[key];
|
||||
process.env[key] = JSON.stringify([{ agent: "copilot", model: "exact-model" }]);
|
||||
try {
|
||||
const result = await requireServerAdapter("paperclip_runner").testEnvironment({
|
||||
companyId: "company-1", adapterType: "paperclip_runner",
|
||||
config: { provider: "acpx", acpxAgent: "copilot", model: "exact-model" },
|
||||
});
|
||||
expect(result).toMatchObject({ status: "warn", checks: [{ code: "acpx_candidate_qualification_only" }] });
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env[key];
|
||||
else process.env[key] = previous;
|
||||
}
|
||||
it("requires a saved Copilot token rather than ambient authentication",async()=>{
|
||||
const prior=process.env.COPILOT_GITHUB_TOKEN;process.env.COPILOT_GITHUB_TOKEN="ambient-not-authority";
|
||||
const probe=vi.mocked(probeCopilotExecutionTarget);probe.mockClear();
|
||||
try{
|
||||
const result=await requireServerAdapter("paperclip_runner").testEnvironment({companyId:"company-1",adapterType:"paperclip_runner",config:{provider:"acpx",acpxAgent:"copilot",model:"gpt-5.6-luna"}});
|
||||
expect(result).toMatchObject({status:"fail",checks:[{code:"COPILOT_AUTH_REQUIRED"}]});expect(probe).not.toHaveBeenCalled();
|
||||
}finally{if(prior===undefined)delete process.env.COPILOT_GITHUB_TOKEN;else process.env.COPILOT_GITHUB_TOKEN=prior;}
|
||||
});
|
||||
it.each([undefined,{kind:"remote",remoteCwd:"/workspace"}])("verifies the selected Copilot runtime and model without a prompt",async(executionTarget)=>{
|
||||
const result=await requireServerAdapter("paperclip_runner").testEnvironment({companyId:"company-1",adapterType:"paperclip_runner",config:{provider:"acpx",acpxAgent:"copilot",model:"gpt-5.6-luna",env:{COPILOT_GITHUB_TOKEN:"fixture-token"}},executionTarget} as any);
|
||||
expect(probeCopilotExecutionTarget).toHaveBeenLastCalledWith("fixture-token",executionTarget,"gpt-5.6-luna");
|
||||
expect(result).toMatchObject({status:"pass",checks:[{code:"copilot_metadata_verified"}]});
|
||||
});
|
||||
it.each(["COPILOT_AUTH_REQUIRED","COPILOT_ENTITLEMENT_DENIED","COPILOT_MODEL_UNAVAILABLE","COPILOT_INSTALLATION_INVALID","COPILOT_REQUEST_FAILED"])("reports distinct Copilot failure %s",async code=>{
|
||||
vi.mocked(probeCopilotExecutionTarget).mockRejectedValueOnce(new HttpError(422,"Fixture metadata failure",{code}));
|
||||
const result=await requireServerAdapter("paperclip_runner").testEnvironment({companyId:"company-1",adapterType:"paperclip_runner",config:{provider:"acpx",acpxAgent:"copilot",model:"gpt-5.6-luna",env:{COPILOT_GITHUB_TOKEN:"fixture-token"}}});
|
||||
expect(result).toMatchObject({status:"fail",checks:[{code}]});
|
||||
});
|
||||
it("sanitizes unexpected Copilot metadata failures",async()=>{
|
||||
vi.mocked(probeCopilotExecutionTarget).mockRejectedValueOnce(new Error("fixture private implementation detail"));
|
||||
const result=await requireServerAdapter("paperclip_runner").testEnvironment({companyId:"company-1",adapterType:"paperclip_runner",config:{provider:"acpx",acpxAgent:"copilot",model:"gpt-5.6-luna",env:{COPILOT_GITHUB_TOKEN:"fixture-token"}}});
|
||||
expect(result).toMatchObject({status:"fail",checks:[{code:"COPILOT_REQUEST_FAILED",message:"Copilot metadata verification failed."}]});
|
||||
expect(JSON.stringify(result)).not.toContain("private implementation detail");
|
||||
});
|
||||
it("wraps built-in npm runtime installs with the sandbox-aware install helper", () => {
|
||||
const expectedClaudeInstall = `if ! command -v 'claude' >/dev/null 2>&1; then ${buildSandboxNpmInstallCommand("@anthropic-ai/claude-code")}; fi`;
|
||||
|
||||
@@ -389,6 +389,7 @@ describe("adapter routes", () => {
|
||||
options: [
|
||||
{ value: "claude", label: "Claude" },
|
||||
{ value: "grok", label: "Grok Build" },
|
||||
{ value: "copilot", label: "GitHub Copilot" },
|
||||
],
|
||||
});
|
||||
expect(JSON.stringify(res.body)).not.toContain("Codex via ACPX");
|
||||
|
||||
@@ -32,6 +32,7 @@ const mockEnvironmentService = vi.hoisted(() => ({
|
||||
releaseLease: vi.fn(),
|
||||
listBoundCompanyIds: vi.fn(async () => [] as string[]),
|
||||
findManagedSandboxEnvironment: vi.fn(async () => null as Record<string, unknown> | null),
|
||||
findKubernetesEnvironment: vi.fn(async () => null as Record<string, unknown> | null),
|
||||
}));
|
||||
|
||||
const mockReleaseRunLease = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
@@ -46,7 +47,7 @@ const mockEnvironmentRuntime = vi.hoisted(() => ({
|
||||
const mockResolveEnvironmentExecutionTarget = vi.hoisted(() => vi.fn());
|
||||
const mockInstanceSettingsService = vi.hoisted(() => ({
|
||||
get: vi.fn(async () => ({ defaultEnvironmentId: null as string | null })),
|
||||
getGeneral: vi.fn(async () => ({ censorUsernameInLogs: false })),
|
||||
getGeneral: vi.fn(async () => ({ censorUsernameInLogs: false, executionMode: "local" })),
|
||||
getExperimental: vi.fn(async () => ({ enableManagedSandboxOnly: false })),
|
||||
}));
|
||||
|
||||
@@ -121,6 +122,10 @@ vi.mock("../routes/ai-connections.js", async (importOriginal) => ({
|
||||
validateAiApiKey: mockValidateAiApiKey,
|
||||
}));
|
||||
const mockMarkAuthenticationFailed = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const mockCopilotProbe = vi.hoisted(() => vi.fn(async () => ({ status: "verified", promptSent: false })));
|
||||
vi.mock("../services/copilot-connection-probe.js", async importOriginal => ({
|
||||
...(await importOriginal<object>()), probeCopilotExecutionTarget: mockCopilotProbe,
|
||||
}));
|
||||
vi.mock("../services/ai-connections.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../services/ai-connections.js")>()),
|
||||
aiConnectionService: () => ({ markAuthenticationFailed: mockMarkAuthenticationFailed }),
|
||||
@@ -192,6 +197,8 @@ describe("agent test-environment route", () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
mockInstanceSettingsService.get.mockResolvedValue({ defaultEnvironmentId: null });
|
||||
mockInstanceSettingsService.getGeneral.mockResolvedValue({ censorUsernameInLogs: false, executionMode: "local" });
|
||||
mockEnvironmentService.findKubernetesEnvironment.mockResolvedValue(null);
|
||||
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: false });
|
||||
mockEnvironmentService.findManagedSandboxEnvironment.mockResolvedValue(null);
|
||||
mockAccessService.decide.mockResolvedValue({
|
||||
@@ -327,6 +334,41 @@ describe("agent test-environment route", () => {
|
||||
expect(testEnvironmentSpy).toHaveBeenCalledWith(expect.objectContaining({ executionTarget: target }));
|
||||
});
|
||||
|
||||
it.each([null, "33333333-3333-4333-8333-333333333333"])("verifies Copilot on forced Kubernetes before the host selection (%s)", async environmentId => {
|
||||
const kubernetesId = "11111111-1111-4111-8111-111111111111";
|
||||
mockInstanceSettingsService.getGeneral.mockResolvedValue({ censorUsernameInLogs: false, executionMode: "kubernetes" });
|
||||
mockEnvironmentService.findKubernetesEnvironment.mockResolvedValue({ id: kubernetesId });
|
||||
const target = { kind: "remote", transport: "sandbox", remoteCwd: "/workspace", providerKey: "kubernetes", runner: { execute: vi.fn() } };
|
||||
mockResolveEnvironmentExecutionTarget.mockResolvedValue(target);
|
||||
mockPrepareManagedAiRuntime.mockImplementation(async (_db, input) => ({
|
||||
config: { ...input.config, env: { COPILOT_GITHUB_TOKEN: "private-fixture" } },
|
||||
attribution: { provider: "github", method: "api_key" }, cleanup: vi.fn(),
|
||||
}));
|
||||
const app = await createApp();
|
||||
const response = await request(app).post("/api/companies/company-1/adapters/paperclip_runner/test-environment").send({
|
||||
environmentId, adapterConfig: { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" },
|
||||
aiConnection: { provider: "github", method: "api_key", mode: "responsible_user" },
|
||||
});
|
||||
expect(response.status, JSON.stringify(response.body)).toBe(200);
|
||||
expect(response.body.status).toBe("pass");
|
||||
expect(mockEnvironmentRuntime.acquireRunLease).toHaveBeenCalledWith(expect.objectContaining({ environment: expect.objectContaining({ id: kubernetesId }) }));
|
||||
expect(mockCopilotProbe).toHaveBeenCalledExactlyOnceWith("private-fixture", target, "gpt-5.6-luna");
|
||||
expect(mockReleaseRunLease).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("refuses a Copilot test before credentials when forced Kubernetes is unavailable", async () => {
|
||||
mockInstanceSettingsService.getGeneral.mockResolvedValue({ censorUsernameInLogs: false, executionMode: "kubernetes" });
|
||||
const app = await createApp();
|
||||
const response = await request(app).post("/api/companies/company-1/adapters/paperclip_runner/test-environment").send({
|
||||
adapterConfig: { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" },
|
||||
aiConnection: { provider: "github", method: "api_key", mode: "responsible_user" },
|
||||
});
|
||||
expect(response.status).toBe(422);
|
||||
expect(response.body.details.code).toBe("copilot_environment_unavailable");
|
||||
expect(mockPrepareManagedAiRuntime).not.toHaveBeenCalled();
|
||||
expect(mockCopilotProbe).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("checks the inherited environment's company before resolving credentials", async () => {
|
||||
mockInstanceSettingsService.get.mockResolvedValue({ defaultEnvironmentId: "11111111-1111-4111-8111-111111111111" });
|
||||
mockEnvironmentService.listBoundCompanyIds.mockResolvedValue(["company-2"]);
|
||||
|
||||
@@ -32,6 +32,10 @@ import express from "express";
|
||||
import request from "supertest";
|
||||
import { aiConnectionRoutes, canInstallSharedAiConnectionForNewAgent, responsibleUserForAiRequest } from "../routes/ai-connections.js";
|
||||
import { validateAiApiKey } from "../routes/ai-connections.js";
|
||||
import * as copilotProbe from "../services/copilot-connection-probe.js";
|
||||
import * as settingsModule from "../services/instance-settings.js";
|
||||
import { agentRoutes } from "../routes/agents.js";
|
||||
import type { PluginWorkerManager } from "../services/plugin-worker-manager.js";
|
||||
vi.mock("../services/local-ai-browser-login.js", () => ({
|
||||
startLocalBrowserLogin: () => ({ authorizationUrl: "https://auth.openai.com/codex/device", code: "ABCD-EFGHJ", abort: () => {} }),
|
||||
}));
|
||||
@@ -61,6 +65,219 @@ beforeAll(async () => {
|
||||
afterAll(async () => { await database?.cleanup(); vi.unstubAllEnvs(); if (home) await rm(home, { recursive: true, force: true }); });
|
||||
|
||||
describe("managed AI connections", () => {
|
||||
it.each(["create", "hire", "edit"])("refuses Copilot %s before probing or persisting when forced Kubernetes is missing", async action => {
|
||||
const { errorHandler } = await import("../middleware/index.js");
|
||||
const owner = `copilot-kubernetes-${action}`, id = randomUUID();
|
||||
const config = { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" };
|
||||
const aiConnection = { provider: "github", method: "api_key", mode: "responsible_user" } as const;
|
||||
await db.insert(companyMemberships).values({ companyId, principalId: owner, principalType: "user", status: "active", membershipRole: "member" });
|
||||
await db.insert(agents).values({ id, companyId, name: "Kubernetes fixture", adapterType: "paperclip_runner", adapterConfig: config, runtimeConfig: { aiConnection } });
|
||||
const account = await service.save(companyId, owner, { provider: "github", method: "api_key", ownership: "personal", name: "Kubernetes fixture", apiKey: "github_pat_fixture_kubernetes", agentIds: [id], allAgents: false }, "github_pat_fixture_kubernetes");
|
||||
await service.setDefault(companyId, owner, account.grantId);
|
||||
const [company] = await db.select().from(companies).where(eq(companies.id, companyId));
|
||||
await db.update(companies).set({ requireBoardApprovalForNewAgents: false }).where(eq(companies.id, companyId));
|
||||
const before = await db.select({ id: agents.id }).from(agents).where(eq(agents.companyId, companyId));
|
||||
const originalSettings = settingsModule.instanceSettingsService;
|
||||
const settings = vi.spyOn(settingsModule, "instanceSettingsService").mockImplementation(database => {
|
||||
const service = originalSettings(database);
|
||||
return { ...service, getGeneral: async () => ({ ...await service.getGeneral(), executionMode: "kubernetes" as const }) };
|
||||
});
|
||||
const probe = vi.spyOn(copilotProbe, "probeCopilotExecutionTarget");
|
||||
const app = express(); app.use(express.json());
|
||||
app.use((req, _res, next) => { req.actor = { type: "board", source: "local_implicit", userId: owner, companyIds: [companyId], isInstanceAdmin: true }; next(); });
|
||||
app.use("/api", agentRoutes(db)); app.use(errorHandler);
|
||||
try {
|
||||
const changedConfig = { ...config, model: "prospective-model" };
|
||||
const response = action === "edit"
|
||||
? await request(app).patch(`/api/agents/${id}`).send({ adapterConfig: changedConfig })
|
||||
: await request(app).post(`/api/companies/${companyId}/${action === "hire" ? "agent-hires" : "agents"}`).send({ name: "Forced Kubernetes", role: "general", adapterType: "paperclip_runner", adapterConfig: changedConfig, runtimeConfig: { aiConnection } });
|
||||
expect(response.status, JSON.stringify(response.body)).toBe(422);
|
||||
expect(response.body.details.code).toBe("copilot_environment_unavailable");
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
expect((await db.select().from(agents).where(eq(agents.id, id)))[0].adapterConfig).toEqual(config);
|
||||
expect(await db.select({ id: agents.id }).from(agents).where(eq(agents.companyId, companyId))).toHaveLength(before.length);
|
||||
} finally {
|
||||
probe.mockRestore(); settings.mockRestore();
|
||||
await db.update(companies).set({ requireBoardApprovalForNewAgents: company.requireBoardApprovalForNewAgents }).where(eq(companies.id, companyId));
|
||||
}
|
||||
});
|
||||
it.each(["create", "hire", "edit"].flatMap(action => [true, false].map(connectionProvided => ({ action, connectionProvided }))))(
|
||||
"refuses unavailable Copilot models before $action persists configuration (connection binding: $connectionProvided)", async ({ action, connectionProvided }) => {
|
||||
const { unprocessable } = await import("../errors.js");
|
||||
const { errorHandler } = await import("../middleware/index.js");
|
||||
const owner = `copilot-model-${action}-${connectionProvided}`, id = randomUUID();
|
||||
const savedConfig = { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" };
|
||||
const aiConnection = { provider: "github", method: "api_key", mode: "responsible_user" } as const;
|
||||
await db.insert(companyMemberships).values({ companyId, principalId: owner, principalType: "user", status: "active", membershipRole: "member" });
|
||||
await db.insert(agents).values({ id, companyId, name: `Copilot ${action} fixture`, adapterType: "paperclip_runner", adapterConfig: savedConfig, runtimeConfig: connectionProvided ? { aiConnection } : {} });
|
||||
const token = `github_pat_fixture_model_${action}`;
|
||||
const account = await service.save(companyId, owner, { provider: "github", method: "api_key", ownership: "personal", name: `Model ${action}`, apiKey: token, agentIds: [id], allAgents: false }, token);
|
||||
await service.setDefault(companyId, owner, account.grantId);
|
||||
const [company] = await db.select().from(companies).where(eq(companies.id, companyId));
|
||||
await db.update(companies).set({ requireBoardApprovalForNewAgents: false }).where(eq(companies.id, companyId));
|
||||
const before = await db.select({ id: agents.id }).from(agents).where(eq(agents.companyId, companyId));
|
||||
const probe = vi.spyOn(copilotProbe, "probeCopilotExecutionTarget").mockRejectedValue(unprocessable("The selected Copilot model is unavailable for this account.", { code: "COPILOT_MODEL_UNAVAILABLE" }));
|
||||
const app = express(); app.use(express.json());
|
||||
app.use((req, _res, next) => { req.actor = { type: "board", source: "local_implicit", userId: owner, companyIds: [companyId], isInstanceAdmin: true }; next(); });
|
||||
app.use("/api", agentRoutes(db)); app.use(errorHandler);
|
||||
const config = { ...savedConfig, model: "unavailable-copilot-model" };
|
||||
try {
|
||||
const response = action === "edit"
|
||||
? await request(app).patch(`/api/agents/${id}`).send({ adapterConfig: config })
|
||||
: await request(app).post(`/api/companies/${companyId}/${action === "hire" ? "agent-hires" : "agents"}`).send({ name: `Unavailable ${action}`, role: "general", adapterType: "paperclip_runner", adapterConfig: config, ...(connectionProvided ? { runtimeConfig: { aiConnection } } : {}) });
|
||||
expect(response.status, JSON.stringify(response.body)).toBe(422);
|
||||
expect(response.body).toMatchObject({ error: "The selected Copilot model is unavailable for this account." });
|
||||
expect(probe).toHaveBeenCalledExactlyOnceWith(token, null, "unavailable-copilot-model");
|
||||
expect((await db.select().from(agents).where(eq(agents.id, id)))[0].adapterConfig).toEqual(savedConfig);
|
||||
expect(await db.select({ id: agents.id }).from(agents).where(eq(agents.companyId, companyId))).toHaveLength(before.length);
|
||||
expect(JSON.stringify(response.body)).not.toContain(token);
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
await db.update(companies).set({ requireBoardApprovalForNewAgents: company.requireBoardApprovalForNewAgents }).where(eq(companies.id, companyId));
|
||||
}
|
||||
});
|
||||
|
||||
it("verifies the exact available Copilot model before a model-only edit with an unchanged connection", async () => {
|
||||
const { errorHandler } = await import("../middleware/index.js");
|
||||
const owner = "copilot-model-valid", id = randomUUID();
|
||||
const aiConnection = { provider: "github", method: "api_key", mode: "responsible_user" } as const;
|
||||
const config = { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna" };
|
||||
await db.insert(companyMemberships).values({ companyId, principalId: owner, principalType: "user", status: "active", membershipRole: "member" });
|
||||
await db.insert(agents).values({ id, companyId, name: "Copilot available model", adapterType: "paperclip_runner", adapterConfig: { ...config, model: "previous-model" }, runtimeConfig: { aiConnection } });
|
||||
const token = "github_pat_fixture_model_valid";
|
||||
const account = await service.save(companyId, owner, { provider: "github", method: "api_key", ownership: "personal", name: "Valid model", apiKey: token, agentIds: [id], allAgents: false }, token);
|
||||
await service.setDefault(companyId, owner, account.grantId);
|
||||
const probe = vi.spyOn(copilotProbe, "probeCopilotExecutionTarget").mockResolvedValue({ status: "verified", version: "1.0.88", profileDigest: "fixture-profile", promptSent: false, models: [{ id: config.model, label: "GPT" }] });
|
||||
const app = express(); app.use(express.json());
|
||||
app.use((req, _res, next) => { req.actor = { type: "board", source: "local_implicit", userId: owner, companyIds: [companyId], isInstanceAdmin: true }; next(); });
|
||||
app.use("/api", agentRoutes(db)); app.use(errorHandler);
|
||||
try {
|
||||
const response = await request(app).patch(`/api/agents/${id}`).send({ adapterConfig: config });
|
||||
expect(response.status, JSON.stringify(response.body)).toBe(200);
|
||||
expect(probe).toHaveBeenCalledExactlyOnceWith(token, null, config.model);
|
||||
expect((await db.select().from(agents).where(eq(agents.id, id)))[0].adapterConfig).toMatchObject(config);
|
||||
expect(response.body.runtimeConfig.aiConnection).toEqual(aiConnection);
|
||||
} finally { probe.mockRestore(); }
|
||||
});
|
||||
it("passes the live sandbox worker manager to Copilot connection verification", async () => {
|
||||
const pluginWorkerManager = {} as PluginWorkerManager;
|
||||
const probe = vi.spyOn(copilotProbe, "probeCopilotConnection").mockResolvedValue({
|
||||
status: "verified", version: "1.0.88", profileDigest: "fixture-profile",
|
||||
promptSent: false, models: [{ id: "gpt-5.6-luna", label: "GPT" }],
|
||||
});
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
req.actor = { type: "board", source: "local_implicit", userId: "alice", companyIds: [companyId] };
|
||||
next();
|
||||
});
|
||||
app.use("/api", aiConnectionRoutes(db, { pluginWorkerManager }));
|
||||
try {
|
||||
const response = await request(app).post(`/api/companies/${companyId}/ai-connections`).send({
|
||||
provider: "github", method: "api_key", name: "Copilot remote route", ownership: "personal",
|
||||
apiKey: "github_pat_fixture_remote", agentIds: [], allAgents: false,
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
expect(probe).toHaveBeenCalledWith(db, companyId, "github_pat_fixture_remote", undefined, undefined, { pluginWorkerManager });
|
||||
} finally { probe.mockRestore(); }
|
||||
});
|
||||
|
||||
it("discovers Copilot models in the selected environment with its sandbox worker", async () => {
|
||||
const pluginWorkerManager = {} as PluginWorkerManager;
|
||||
const [environment] = await db.insert(environments).values({ name: "Copilot model discovery", driver: "sandbox", config: { provider: "daytona" } }).returning();
|
||||
const token = "github_pat_fixture_model_discovery";
|
||||
const account = await service.save(companyId, "alice", { provider: "github", method: "api_key", ownership: "personal", name: "Model discovery account", apiKey: token, agentIds: [agentId], allAgents: false }, token);
|
||||
await service.setDefault(companyId, "alice", account.grantId);
|
||||
const probe = vi.spyOn(copilotProbe, "probeCopilotConnection").mockResolvedValue({ status: "verified", version: "1.0.88", profileDigest: "fixture-profile", promptSent: false, models: [{ id: "gpt-5.6-luna", label: "GPT" }] });
|
||||
const app = express(); app.use(express.json());
|
||||
app.use((req, _res, next) => { req.actor = { type: "board", source: "local_implicit", userId: "alice", companyIds: [companyId] }; next(); });
|
||||
app.use("/api", agentRoutes(db, { pluginWorkerManager }));
|
||||
try {
|
||||
const response = await request(app).get(`/api/companies/${companyId}/adapters/paperclip_runner/models`).query({ provider: "acpx", acpxAgent: "copilot", environmentId: environment.id, agentId, aiConnection: JSON.stringify({ provider: "github", method: "api_key", mode: "responsible_user" }) });
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers["cache-control"]).toBe("no-store");
|
||||
expect(response.body).toEqual([{ id: "gpt-5.6-luna", label: "GPT" }]);
|
||||
expect(probe).toHaveBeenCalledWith(db, companyId, token, environment.id, undefined, { pluginWorkerManager });
|
||||
expect(JSON.stringify(response.body)).not.toContain(token);
|
||||
} finally { probe.mockRestore(); }
|
||||
});
|
||||
|
||||
it("keeps Copilot model discovery behind shared installation and agent configuration authority", async () => {
|
||||
const { errorHandler } = await import("../middleware/index.js");
|
||||
const owner = "copilot-discovery-owner", audience = "copilot-discovery-audience";
|
||||
await db.insert(companyMemberships).values([owner, audience].map(principalId => ({ companyId, principalId, principalType: "user", status: "active", membershipRole: "member" })));
|
||||
await db.insert(principalPermissionGrants).values([owner, audience].map(principalId => ({ companyId, principalId, principalType: "user", permissionKey: "agents:create" })));
|
||||
const token = "github_pat_fixture_shared_discovery";
|
||||
const account = await service.save(companyId, owner, { provider: "github", method: "api_key", ownership: "shared", name: "Shared discovery", apiKey: token, agentIds: [], allAgents: false }, token);
|
||||
await db.insert(connectionGrantMembers).values([owner, audience].map(subjectId => ({ companyId, grantId: account.grantId, subjectType: "user", subjectId })));
|
||||
const binding = { provider: "github", method: "api_key", mode: "shared", ...account };
|
||||
const probe = vi.spyOn(copilotProbe, "probeCopilotConnection").mockResolvedValue({ status: "verified", version: "1.0.88", profileDigest: "fixture-profile", promptSent: false, models: [{ id: "gpt-5.6-luna", label: "GPT" }] });
|
||||
const app = express(); app.use(express.json());
|
||||
app.use((req, _res, next) => { req.actor = { type: "board", source: "session", userId: String(req.headers["x-test-user"] ?? audience), companyIds: [companyId], memberships: [{ companyId, membershipRole: "member", status: "active" }] }; next(); });
|
||||
app.use("/api", agentRoutes(db)); app.use(errorHandler);
|
||||
const url = `/api/companies/${companyId}/adapters/paperclip_runner/models`;
|
||||
const query = { provider: "acpx", acpxAgent: "copilot", aiConnection: JSON.stringify(binding) };
|
||||
try {
|
||||
const denied = await request(app).get(url).query(query);
|
||||
expect(denied.status, JSON.stringify(denied.body)).toBe(403);
|
||||
expect(denied.body.error).toContain("not permitted for this agent");
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
const allowed = await request(app).get(url).set("x-test-user", owner).query(query);
|
||||
expect(allowed.status, JSON.stringify(allowed.body)).toBe(200);
|
||||
expect(probe).toHaveBeenCalledExactlyOnceWith(db, companyId, token, null, undefined, { pluginWorkerManager: undefined });
|
||||
probe.mockClear();
|
||||
const foreign = await request(app).get(url).set("x-test-user", owner).query({ ...query, agentId: randomUUID() });
|
||||
expect(foreign.status).toBe(404);
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
expect(JSON.stringify([denied.body, allowed.body, foreign.body])).not.toContain(token);
|
||||
} finally { probe.mockRestore(); }
|
||||
});
|
||||
|
||||
it("preserves repository GitHub credentials for managed non-Copilot execution", async () => {
|
||||
const owner = "repo-github-owner";
|
||||
await db.insert(companyMemberships).values({ companyId, principalId: owner, principalType: "user", status: "active", membershipRole: "member" });
|
||||
await create(owner, owner);
|
||||
for (const configured of [{ GH_TOKEN: "repo-gh-fixture", GITHUB_TOKEN: "repo-github-fixture" }, {}]) {
|
||||
const runtime = await prepareManagedAiRuntime(db, { ...input, responsibleUserId: owner, config: { env: configured } });
|
||||
try {
|
||||
expect(runtime.config.env).toMatchObject({ COPILOT_GITHUB_TOKEN: "", ANTHROPIC_API_KEY: `fixture-${owner}` });
|
||||
for (const key of ["GH_TOKEN", "GITHUB_TOKEN"] as const) {
|
||||
if (key in configured) expect(runtime.config.env?.[key]).toBe(configured[key as keyof typeof configured]);
|
||||
else expect(runtime.config.env).not.toHaveProperty(key);
|
||||
}
|
||||
} finally { await runtime.cleanup(); }
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a vaulted Copilot token with GitHub attribution and refuses revoked or foreign grants", async () => {
|
||||
const owner = "copilot-owner";
|
||||
const token = "github_pat_fixture_copilot";
|
||||
await db.insert(companyMemberships).values({ companyId, principalId: owner, principalType: "user", status: "active", membershipRole: "member" });
|
||||
const account = await service.save(companyId, owner, { provider: "github", method: "api_key", ownership: "personal", name: "Copilot", apiKey: token, agentIds: [agentId], allAgents: false }, token);
|
||||
const selectedBinding = { provider: "github", method: "api_key", mode: "responsible_user" } as const;
|
||||
const config = { provider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna", env: { COPILOT_GITHUB_TOKEN: "ambient-never-use", GH_TOKEN: "project-never-use" } };
|
||||
const runInput = { companyId, agentId, responsibleUserId: owner, adapterType: "paperclip_runner", binding: selectedBinding, config };
|
||||
const selected = await service.select({ ...runInput, userId: owner, runnerProvider: "acpx", acpxAgent: "copilot", model: config.model });
|
||||
expect(await service.credential(selected)).toBe(token);
|
||||
expect(JSON.stringify(await service.list(companyId, owner))).not.toContain(token);
|
||||
const runtime = await prepareManagedAiRuntime(db, runInput);
|
||||
try {
|
||||
expect(runtime.config).toMatchObject({ provider: "acpx", acpxAgent: "copilot", model: config.model, env: { COPILOT_GITHUB_TOKEN: token, GH_TOKEN: "", GITHUB_TOKEN: "", OPENAI_API_KEY: "", ANTHROPIC_API_KEY: "" } });
|
||||
expect(runtime.attribution).toMatchObject({ provider: "github", method: "api_key", connectionId: account.connectionId, grantId: account.grantId });
|
||||
expect(config.env.COPILOT_GITHUB_TOKEN).toBe("ambient-never-use");
|
||||
} finally { await runtime.cleanup(); }
|
||||
await expect(access(runtime.home!)).rejects.toThrow();
|
||||
await expect(service.select({ ...runInput, userId: "bob", runnerProvider: "acpx", acpxAgent: "copilot" })).rejects.toThrow();
|
||||
await expect(service.select({ ...runInput, companyId: otherCompanyId, userId: owner, binding: { ...selectedBinding, mode: "shared", ...account }, runnerProvider: "acpx", acpxAgent: "copilot" })).rejects.toThrow();
|
||||
const network = vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("usage must not send credentials"));
|
||||
try {
|
||||
expect(await service.probeUsage(companyId, owner, account.connectionId, account.grantId)).toMatchObject({ provider: "github", status: "unsupported", limits: [] });
|
||||
expect(network).not.toHaveBeenCalled();
|
||||
} finally { network.mockRestore(); }
|
||||
await db.update(connectionGrants).set({ status: "revoked" }).where(eq(connectionGrants.id, account.grantId));
|
||||
await expect(prepareManagedAiRuntime(db, runInput)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("marks explicit AI selection rejections without changing their HTTP contract", async () => {
|
||||
const owner = "selection-blocker-owner";
|
||||
await db.insert(companyMemberships).values({ companyId, principalId: owner, principalType: "user", status: "active", membershipRole: "member" });
|
||||
@@ -807,12 +1024,18 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
await expect(access(runtime.home!)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("preserves Google account defaults when the provider constraint migration is reapplied", async () => {
|
||||
it("preserves Google and Copilot account defaults when the current provider constraint migration is reapplied", async () => {
|
||||
const saved = await service.save(companyId, "bob", {
|
||||
provider: "google", method: "api_key", ownership: "personal", name: "Google migration",
|
||||
apiKey: "fixture", allAgents: true, agentIds: [],
|
||||
}, "google-migration-key");
|
||||
const migration = await readFile(new URL("../../../packages/db/src/migrations/0306_familiar_titania.sql", import.meta.url), "utf8");
|
||||
await db.insert(companyMemberships).values({ companyId, principalId: "migration-copilot", principalType: "user", status: "active", membershipRole: "member" });
|
||||
const copilot = await service.save(companyId, "migration-copilot", {
|
||||
provider: "github", method: "api_key", ownership: "personal", name: "Copilot migration",
|
||||
apiKey: "fixture", allAgents: true, agentIds: [],
|
||||
}, "github_pat_fixture_migration");
|
||||
await service.setDefault(companyId, "migration-copilot", copilot.grantId);
|
||||
const migration = await readFile(new URL("../../../packages/db/src/migrations/0320_copilot_defaults.sql", import.meta.url), "utf8");
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
await db.transaction(async (tx) => {
|
||||
for (const statement of migration.split("--> statement-breakpoint")) {
|
||||
@@ -825,6 +1048,12 @@ console.log(JSON.stringify({ type: "result", subtype: "success", result: "hello"
|
||||
binding: { provider: "google", method: "api_key", mode: "responsible_user" },
|
||||
});
|
||||
expect(selected.grant.id).toBe(saved.grantId);
|
||||
const selectedCopilot = await service.select({
|
||||
companyId, agentId, userId: "migration-copilot", adapterType: "paperclip_runner",
|
||||
runnerProvider: "acpx", acpxAgent: "copilot", model: "gpt-5.6-luna",
|
||||
binding: { provider: "github", method: "api_key", mode: "responsible_user" },
|
||||
});
|
||||
expect(selectedCopilot.grant.id).toBe(copilot.grantId);
|
||||
});
|
||||
|
||||
it.each([false, true])("reports the authoritative connection-manager capability for custom grants (manager: %s)", async (manager) => {
|
||||
|
||||
@@ -372,7 +372,7 @@ describeEmbeddedPostgres("connectionIntentService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("only advertises GitHub tool methods in search and setup options", async () => {
|
||||
it("separates GitHub runtime discovery from tool setup options", async () => {
|
||||
const service = connectionIntentService(db);
|
||||
const search = await service.search(claims, "github");
|
||||
const github = search.results.find((result) => result.service === "github");
|
||||
@@ -383,6 +383,12 @@ describeEmbeddedPostgres("connectionIntentService", () => {
|
||||
key: "mcp-key",
|
||||
label: "Personal access token (advanced)",
|
||||
auth: "api_key",
|
||||
purpose: "tool",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
key: "copilot-token",
|
||||
purpose: "ai",
|
||||
setupPath: expect.stringContaining("/apps/connect?source=github"),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@@ -407,6 +413,7 @@ describeEmbeddedPostgres("connectionIntentService", () => {
|
||||
expect(setup.service.methods.map((method) => method.key)).not.toContain(
|
||||
"chat-agent",
|
||||
);
|
||||
expect(setup.service.methods.map((method) => method.key)).not.toContain("copilot-token");
|
||||
});
|
||||
|
||||
it("serializes OAuth intent completion behind addressed-user membership revocation", async () => {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { environmentRuntimeService } from "../services/environment-runtime.js";
|
||||
import type { PluginWorkerManager } from "../services/plugin-worker-manager.js";
|
||||
const mocks = vi.hoisted(() => ({ metadata: vi.fn(), command: vi.fn(), environment: vi.fn(), bindings: vi.fn(), acquire: vi.fn(), realize: vi.fn(), release: vi.fn(), resolve: vi.fn(), general: vi.fn(), experimental: vi.fn(), defaults: vi.fn(), kubernetes: vi.fn(), managed: vi.fn() }));
|
||||
vi.mock("../vendor/paperclip-runner/live/index.js", async importOriginal => ({ ...await importOriginal<object>(), probeCopilotMetadata: mocks.metadata }));
|
||||
vi.mock("@paperclipai/adapter-utils/execution-target", () => ({ runAdapterExecutionTargetShellCommand: mocks.command }));
|
||||
vi.mock("../middleware/logger.js", () => ({ logger: { warn: vi.fn() } }));
|
||||
vi.mock("../services/environments.js", () => ({ environmentService: () => ({ getById: mocks.environment, listBoundCompanyIds: mocks.bindings, findKubernetesEnvironment: mocks.kubernetes, findManagedSandboxEnvironment: mocks.managed }) }));
|
||||
vi.mock("../services/instance-settings.js", () => ({ instanceSettingsService: () => ({ get: mocks.defaults, getGeneral: mocks.general, getExperimental: mocks.experimental }) }));
|
||||
vi.mock("../services/environment-runtime.js", () => ({ environmentRuntimeService: vi.fn(() => ({ acquireRunLease: mocks.acquire, realizeWorkspace: mocks.realize, getDriver: () => ({ releaseRunLease: mocks.release }) })) }));
|
||||
vi.mock("../services/environment-execution-target.js", () => ({ resolveEnvironmentExecutionTarget: mocks.resolve }));
|
||||
import { logger } from "../middleware/logger.js";
|
||||
import { probeCopilotConnection, probeCopilotExecutionTarget } from "../services/copilot-connection-probe.js";
|
||||
import { QUALIFIED_ACPX_PROFILES } from "../../../packages/paperclip-runner/src/drivers/acpx/qualified-profiles.js";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
|
||||
const verified = { status: "verified", version: "1.0.88", profileDigest: QUALIFIED_ACPX_PROFILES.copilot.commandDigest, promptSent: false, models: [{ id: "gpt-5.6-luna", label: "GPT" }] };
|
||||
const remote = { kind: "remote", remoteCwd: "/workspace" } as AdapterExecutionTarget;
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks(); mocks.general.mockResolvedValue({ executionMode: "local" }); mocks.experimental.mockResolvedValue({}); mocks.defaults.mockResolvedValue({ defaultEnvironmentId: null });
|
||||
mocks.environment.mockResolvedValue({ id: "selected", driver: "sandbox", status: "active", config: { provider: "daytona" } }); mocks.bindings.mockResolvedValue(["company"]);
|
||||
mocks.acquire.mockResolvedValue({ lease: { id: "lease", metadata: {} } }); mocks.realize.mockResolvedValue({}); mocks.release.mockResolvedValue(undefined); mocks.resolve.mockResolvedValue(remote);
|
||||
mocks.command.mockResolvedValue({ timedOut: false, stdout: JSON.stringify(verified), exitCode: 0 }); mocks.metadata.mockResolvedValue(verified);
|
||||
});
|
||||
describe("Copilot connection verification", () => {
|
||||
it.each(["ssh", "sandbox", "plugin"])("keeps an explicitly selected %s environment under managed-only execution", async driver => {
|
||||
mocks.experimental.mockResolvedValue({ enableManagedSandboxOnly: true });
|
||||
mocks.environment.mockResolvedValue({ id: "selected", driver, status: "active", config: { provider: "daytona" } });
|
||||
expect(await probeCopilotConnection({} as Db, "company", "private-token", "selected")).toEqual(verified);
|
||||
expect(mocks.managed).not.toHaveBeenCalled();
|
||||
expect(mocks.resolve).toHaveBeenCalledWith(expect.objectContaining({ environment: expect.objectContaining({ id: "selected", driver }) }));
|
||||
expect(mocks.metadata).not.toHaveBeenCalled();
|
||||
expect(mocks.command).toHaveBeenCalledOnce();
|
||||
});
|
||||
it.each([null, "local"])("redirects only a local selection (%s) onto the managed environment", async selection => {
|
||||
mocks.experimental.mockResolvedValue({ enableManagedSandboxOnly: true });
|
||||
mocks.managed.mockResolvedValue({ id: "managed" });
|
||||
mocks.environment.mockImplementation(async id => ({ id, driver: id === "local" ? "local" : "sandbox", status: "active", config: { provider: "daytona" } }));
|
||||
expect(await probeCopilotConnection({} as Db, "company", "private-token", selection)).toEqual(verified);
|
||||
expect(mocks.resolve).toHaveBeenCalledWith(expect.objectContaining({ environment: expect.objectContaining({ id: "managed" }) }));
|
||||
expect(mocks.metadata).not.toHaveBeenCalled();
|
||||
});
|
||||
it("gives forced Kubernetes precedence over an explicitly selected remote and managed-only policy", async () => {
|
||||
mocks.general.mockResolvedValue({ executionMode: "kubernetes" });
|
||||
mocks.experimental.mockResolvedValue({ enableManagedSandboxOnly: true });
|
||||
mocks.kubernetes.mockResolvedValue({ id: "kubernetes" });
|
||||
mocks.environment.mockImplementation(async id => ({ id, driver: "sandbox", status: "active", config: { provider: "kubernetes" } }));
|
||||
expect(await probeCopilotConnection({} as Db, "company", "private-token", "selected")).toEqual(verified);
|
||||
expect(mocks.resolve).toHaveBeenCalledWith(expect.objectContaining({ environment: expect.objectContaining({ id: "kubernetes" }) }));
|
||||
expect(mocks.managed).not.toHaveBeenCalled();
|
||||
expect(mocks.metadata).not.toHaveBeenCalled();
|
||||
});
|
||||
it("probes the selected environment and releases its owned lease", async () => {
|
||||
const db = {} as Db;
|
||||
const pluginWorkerManager = {} as PluginWorkerManager;
|
||||
expect(await probeCopilotConnection(db, "company", "private-token", "selected", "gpt-5.6-luna", { pluginWorkerManager })).toEqual(verified);
|
||||
expect(environmentRuntimeService).toHaveBeenCalledWith(db, { pluginWorkerManager });
|
||||
expect(mocks.metadata).not.toHaveBeenCalled(); expect(mocks.acquire).toHaveBeenCalledOnce();
|
||||
expect(mocks.command.mock.calls[0]![3]).toMatchObject({ env: { COPILOT_GITHUB_TOKEN: "private-token", PAPERCLIP_COPILOT_PROBE_MODEL: "gpt-5.6-luna" }, timeoutSec: 35 });
|
||||
expect(mocks.command.mock.calls[0]![2]).not.toContain("private-token");
|
||||
expect(mocks.release).toHaveBeenCalledWith(expect.objectContaining({ status: "released" }));
|
||||
});
|
||||
it("rejects a foreign company before creating a lease or transmitting the token", async () => {
|
||||
mocks.bindings.mockResolvedValue(["other-company"]);
|
||||
await expect(probeCopilotConnection({} as Db, "company", "private-token", "selected")).rejects.toMatchObject({ status: 403 });
|
||||
expect(mocks.command).not.toHaveBeenCalled(); expect(mocks.metadata).not.toHaveBeenCalled(); expect(mocks.acquire).not.toHaveBeenCalled();
|
||||
});
|
||||
it("does not fall back to host credentials when the remote runtime is missing", async () => {
|
||||
mocks.command.mockResolvedValue({ timedOut: false, stdout: "", exitCode: 127 });
|
||||
await expect(probeCopilotConnection({} as Db, "company", "private-token", "selected")).rejects.toMatchObject({ details: { code: "COPILOT_INSTALLATION_INVALID" } });
|
||||
expect(mocks.metadata).not.toHaveBeenCalled(); expect(mocks.release).toHaveBeenCalledWith(expect.objectContaining({ status: "failed" }));
|
||||
});
|
||||
it("sanitizes remote errors and releases the lease on authentication failure", async () => {
|
||||
mocks.command.mockResolvedValue({ timedOut: false, stdout: JSON.stringify({ status: "failed", code: "COPILOT_AUTH_REQUIRED", promptSent: false, message: "private-token" }), exitCode: 1 });
|
||||
const error = await probeCopilotConnection({} as Db, "company", "private-token", "selected").catch(e => e);
|
||||
expect(error.details.code).toBe("COPILOT_AUTH_REQUIRED"); expect(error.message).not.toContain("private-token"); expect(mocks.release).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("preserves authentication classification when lease cleanup also fails", async () => {
|
||||
mocks.command.mockResolvedValue({ timedOut: false, stdout: JSON.stringify({ status: "failed", code: "COPILOT_AUTH_REQUIRED", promptSent: false, message: "private-token" }), exitCode: 1 });
|
||||
mocks.release.mockRejectedValue(new Error("cleanup private-token"));
|
||||
const error = await probeCopilotConnection({} as Db, "company", "private-token", "selected").catch(e => e);
|
||||
expect(error).toMatchObject({ status: 422, details: { code: "COPILOT_AUTH_REQUIRED" } });
|
||||
expect(error.message).not.toContain("private-token");
|
||||
expect(logger.warn).toHaveBeenCalledWith({ companyId: "company", environmentId: "selected", leaseId: "lease", code: "COPILOT_PROBE_CLEANUP_FAILED" }, expect.any(String));
|
||||
expect(JSON.stringify(vi.mocked(logger.warn).mock.calls)).not.toContain("private-token");
|
||||
expect(mocks.release).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("does not verify a connection when its successful probe cannot clean up", async () => {
|
||||
mocks.release.mockRejectedValue(new Error("cleanup private-token"));
|
||||
const error = await probeCopilotConnection({} as Db, "company", "private-token", "selected").catch(e => e);
|
||||
expect(error).toMatchObject({ status: 422, details: { code: "COPILOT_REQUEST_FAILED", cleanupCode: "COPILOT_PROBE_CLEANUP_FAILED" } });
|
||||
expect(error.message).not.toContain("private-token");
|
||||
expect(mocks.release).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("rejects stale profile results and unavailable model selections", async () => {
|
||||
mocks.command.mockResolvedValue({ timedOut: false, stdout: JSON.stringify({ ...verified, profileDigest: "stale" }), exitCode: 0 });
|
||||
await expect(probeCopilotExecutionTarget("private-token", remote)).rejects.toMatchObject({ details: { code: "COPILOT_REQUEST_FAILED" } });
|
||||
mocks.command.mockResolvedValue({ timedOut: false, stdout: JSON.stringify(verified), exitCode: 0 });
|
||||
await expect(probeCopilotExecutionTarget("private-token", remote, "missing")).rejects.toMatchObject({ details: { code: "COPILOT_MODEL_UNAVAILABLE" } });
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { request as httpRequest } from "node:http";
|
||||
import { createServer as createViteServer } from "vite";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { idleAdmissionMiddleware, trackIdleRequestHandlers } from "../middleware/idle-admission.js";
|
||||
import { idleWorkSnapshot, startTaskDrain, stopTaskDrain, readTaskDrain, beginIdleTrackedWork } from "../services/task-admission.js";
|
||||
@@ -49,6 +50,36 @@ 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("supports real Vite Connect route metadata without losing async work", async () => {
|
||||
const vite = await createViteServer({ configFile: false, appType: "custom",
|
||||
server: { middlewareMode: true, watch: null }, optimizeDeps: { noDiscovery: true, include: [] } });
|
||||
const server = app(), done = deferred();
|
||||
try {
|
||||
server.use(vite.middlewares);
|
||||
server.post("/work", async (_req, res) => { res.sendStatus(202); await done.promise; });
|
||||
expect(() => trackIdleRequestHandlers(server)).not.toThrow();
|
||||
await request(server).post("/work").expect(202);
|
||||
expect(idleWorkSnapshot().active).toBe(1);
|
||||
done.resolve();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(idleWorkSnapshot().active).toBe(0);
|
||||
} finally { await vite.close(); }
|
||||
});
|
||||
|
||||
it("tracks real Vite middleware without confusing Connect mount paths with Express routes", async () => {
|
||||
const { createServer } = await import("vite");
|
||||
const vite = await createServer({
|
||||
|
||||
Reference in new issue
Block a user