mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
fix(ai): retain account modes in session adoption checks
Exercise responsible-user to delegated adoption with real prepared account configurations and preserve all other fingerprint boundaries. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
d134e3902a
commit
b4bb78eb70
3 files changed
+38
-3
No files matched your search
@@ -8,6 +8,7 @@ import { createDb, companies, agents, companyMemberships, connectionGrants, plug
|
||||
import { startEmbeddedPostgresTestDatabase } from "@paperclipai/db/test-embedded-postgres";
|
||||
import type { AiConnectionPoolMember, AiConnectionRouterRequest, AiConnectionRouterResult, PaperclipPluginManifestV1 } from "@paperclipai/shared";
|
||||
import { aiConnectionRouterService, AiConnectionPoolExhausted, poolMemberRuntimeConfig, applyAiConnectionRouterTaskSettings } from "../services/ai-connection-router.js";
|
||||
import { aiConnectionSessionCompatibilityInputs } from "../services/ai-connection-session.js";
|
||||
import { projectPaperclipRunnerTaskConfig, resolvePaperclipRunnerNativeProviderInput } from "../services/native-runtime/provider-profile.js";
|
||||
import { aiConnectionService } from "../services/ai-connections.js";
|
||||
import { instanceSettingsService } from "../services/instance-settings.js";
|
||||
@@ -18,7 +19,7 @@ import express from "express";
|
||||
import request from "supertest";
|
||||
import { aiConnectionRoutes } from "../routes/ai-connections.js";
|
||||
import { errorHandler } from "../middleware/index.js";
|
||||
import { heartbeatService } from "../services/heartbeat.js";
|
||||
import { heartbeatService, buildEffectiveRunSessionConfigMetadata, resolveTaskSessionConfigFreshness } from "../services/heartbeat.js";
|
||||
import { connectionIntentService } from "../services/connection-intents.js";
|
||||
import { legacyExecutionNeedsReconciliation } from "../services/legacy-execution-recovery.js";
|
||||
import { getServerAdapter, registerServerAdapter, unregisterServerAdapter } from "../adapters/index.js";
|
||||
@@ -275,6 +276,27 @@ describe("durable, authorized connection routing", () => {
|
||||
try { await expect(resolve(pool.id, "new")).rejects.toThrow("plugin"); expect(await resolve(pool.id, "recovery", { persisted: selected })).toEqual(selected); await db.delete(toolConnections).where(eq(toolConnections.id, pool.id)); expect(await resolve(pool.id, "recovery", { persisted: selected })).toEqual(selected); }
|
||||
finally { await db.update(plugins).set({ status: "ready" }).where(eq(plugins.id, pluginId)); }
|
||||
});
|
||||
it("resumes a responsible-user session through a delegated pool using real managed account configurations", async () => {
|
||||
await aiConnectionService(db).setDefault(companyId, "alice", members[0]!.binding.grantId!);
|
||||
const pool = await makePool({ members: [members[0]!] });
|
||||
const selected = await resolve(pool.id, "managed-mode-adoption", { requireExisting: true, existingGrantId: members[0]!.binding.grantId });
|
||||
const directBinding = { mode: "responsible_user" as const, provider: "openai" as const, method: "api_key" as const };
|
||||
const prepare = { companyId, agentId, responsibleUserId: "alice", adapterType: "paperclip_runner", config: selected.runtimeConfig };
|
||||
const before = await prepareManagedAiRuntime(db, { ...prepare, binding: directBinding });
|
||||
const after = await prepareManagedAiRuntime(db, { ...prepare, binding: selected.binding });
|
||||
try {
|
||||
expect(before.config.managedAiConnection.mode).toBe("responsible_user");
|
||||
expect(after.config.managedAiConnection.mode).toBe("delegated");
|
||||
expect(after.sessionIdentity).toBe(before.sessionIdentity);
|
||||
const common = { adapterType: "paperclip_runner", issueOverrides: null, workspaceConfig: {}, environment: {}, environmentEnv: null, projectEnv: null, routineEnv: null, runtimeSkills: [], agentConfigRevision: null };
|
||||
const previous = await buildEffectiveRunSessionConfigMetadata({ ...common, effectiveAdapterConfig: before.config, managedAiHome: before.home, agentRuntimeConfig: { aiConnection: directBinding } });
|
||||
const currentInput = { ...common, effectiveAdapterConfig: after.config, managedAiHome: after.home, agentRuntimeConfig: { aiConnection: { mode: "router", connectionId: pool.id } } };
|
||||
const current = await buildEffectiveRunSessionConfigMetadata(currentInput);
|
||||
const alternatives = aiConnectionSessionCompatibilityInputs({ ...currentInput, originalIssueOverrides: null, binding: selected.binding, router: true, storedIdentity: before.sessionIdentity, sessionIdentity: after.sessionIdentity, credentialIdentity: after.identity, revisions: [] });
|
||||
const compatibleConfigMetadata = await Promise.all(alternatives.map(candidate => buildEffectiveRunSessionConfigMetadata({ ...currentInput, ...candidate })));
|
||||
expect(resolveTaskSessionConfigFreshness({ hasTaskSession: true, configuredModel: String(selected.runtimeConfig.model), taskSessionParams: { __paperclipConfiguredModel: selected.runtimeConfig.model, __paperclipConfigFingerprint: previous.fingerprint, __paperclipConfigFingerprintVersion: previous.version, __paperclipConfigCategoryFingerprints: previous.categoryFingerprints }, configMetadata: current, compatibleConfigMetadata }).reset).toBe(false);
|
||||
} finally { await before.cleanup(); await after.cleanup(); }
|
||||
});
|
||||
it("keeps session identity and fingerprints stable on authenticated refresh, but changes them on manual replacement", async () => {
|
||||
const input = { companyId, agentId, responsibleUserId: "alice", adapterType: "paperclip_runner", binding: members[0]!.binding, config: { provider: "codex", model: "gpt-5.6-sol" } };
|
||||
const first = await prepareManagedAiRuntime(db, input);
|
||||
|
||||
@@ -26,7 +26,7 @@ async function decision(previous: Partial<MetadataInput>, next: Partial<Metadata
|
||||
|
||||
describe("AI pool session adoption", () => {
|
||||
it.each([binding, { ...binding, mode: "delegated" }, { mode: "responsible_user", provider: "openai", method: "api_key" }])("preserves the exact existing configuration through a compatible binding change: %j", async aiConnection => {
|
||||
expect((await decision({ agentRuntimeConfig: { aiConnection } })).reset).toBe(false);
|
||||
expect((await decision({ agentRuntimeConfig: { aiConnection }, effectiveAdapterConfig: { ...config, managedAiConnection: { ...config.managedAiConnection, mode: aiConnection.mode } } })).reset).toBe(false);
|
||||
});
|
||||
it("bridges only account-binding edits in revision history", async () => {
|
||||
const first = revision("revision-1", null, binding);
|
||||
|
||||
@@ -86,5 +86,18 @@ export function aiConnectionSessionCompatibilityInputs(input: {
|
||||
}
|
||||
const overrides = input.router && !isDeepStrictEqual(input.issueOverrides, input.originalIssueOverrides)
|
||||
? [input.issueOverrides, input.originalIssueOverrides] : [input.issueOverrides];
|
||||
return variants.flatMap(variant => configs.flatMap(effectiveAdapterConfig => overrides.map(issueOverrides => ({ ...variant, effectiveAdapterConfig, issueOverrides }))));
|
||||
return variants.flatMap(variant => {
|
||||
const previousMode = record(record(variant.agentRuntimeConfig).aiConnection).mode;
|
||||
const modes = input.router
|
||||
? ["responsible_user", "shared", "delegated"].includes(String(previousMode))
|
||||
? [previousMode] : ["responsible_user", "shared", "delegated"]
|
||||
: [record(input.effectiveAdapterConfig.managedAiConnection).mode];
|
||||
return configs.flatMap(config => modes.flatMap(mode => overrides.map(issueOverrides => ({
|
||||
...variant,
|
||||
// Binding mode is also recorded in the managed account attribution.
|
||||
// Keep grant, user, credential epoch, and every other setting unchanged.
|
||||
effectiveAdapterConfig: { ...config, managedAiConnection: { ...record(config.managedAiConnection), mode } },
|
||||
issueOverrides,
|
||||
}))));
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user