mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 12:59:13 +01:00
fix(github): initialize and resume fresh manual App imports
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
713a52eb0d
commit
82a6c5b7bb
5 files changed
+70
-11
No files matched your search
@@ -106,7 +106,7 @@ enrollment handoff passed. The test did not authorize Cloud enrollment or
|
||||
create a provider App.
|
||||
|
||||
The focused wizard and retained clipboard/recovery UI suites pass (37 tests).
|
||||
GitHub backend coverage passes (189 cases), including both owner types,
|
||||
GitHub backend coverage passes (192 cases), including both owner types,
|
||||
interrupted registration and vault storage, empty saved repository selections,
|
||||
localhost configuration, concurrent identity attempts, enrolled-origin recovery,
|
||||
tenant binding, and legacy callbacks. The complete OpenAPI suite passes (13
|
||||
@@ -121,7 +121,9 @@ a claim of a green full suite. CI initially
|
||||
identified outdated wizard expectations and missing API documentation, both now
|
||||
covered by the updated focused checks. The full GitHub fixture also verifies
|
||||
installation permissions and preserved legacy tool authority. The browser
|
||||
report attaches screenshots of both wizard screens.
|
||||
report attaches screenshots of both wizard screens. Fresh manual App imports
|
||||
initialize repository access and tools without a registration row; interrupted
|
||||
imports resume those defaults while explicit restrictions stay preserved.
|
||||
|
||||
Cloud's final standard `npm test` passes: 2,561 tests and 73 environment-dependent
|
||||
skips. Dedicated-App regressions cover normalized concurrent starts, stale
|
||||
|
||||
@@ -201,6 +201,7 @@ export interface ChatEndpointSetupState {
|
||||
initialRepositoriesImported?: boolean;
|
||||
repositorySelectionSaved?: boolean;
|
||||
initialRepositoryImportPending?: boolean;
|
||||
initialSetupPending?: boolean;
|
||||
};
|
||||
step: "choose_agent" | "provider_setup" | "test" | "complete";
|
||||
/** Server-generated boundary; only provider events at or after this time can complete setup. */
|
||||
|
||||
@@ -1876,6 +1876,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
> & { wakeup?: ChatChannelServiceOptions["heartbeat"]["wakeup"] } = {},
|
||||
useVerifiedAppId = false,
|
||||
registerWithManifest = false,
|
||||
connectExistingApp = false,
|
||||
) {
|
||||
let setupComplete = false;
|
||||
const deferredSchedule = overrides.scheduleDeferredWork;
|
||||
@@ -2051,6 +2052,10 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
);
|
||||
if (registerWithManifest)
|
||||
await context.service.startGitHubRegistration(endpoint.id, "owner-user", "Maya");
|
||||
if (connectExistingApp)
|
||||
await context.service.storeGitHubApp(endpoint.id, "owner-user", {
|
||||
appId: String(appRegistrationId), privateKey, webhookSecret,
|
||||
});
|
||||
await context.service.configure(
|
||||
endpoint.id,
|
||||
{
|
||||
@@ -2069,7 +2074,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
await db.transaction((tx) => syncGitHubBotTools(tx, configured!, "owner-user", true));
|
||||
}
|
||||
const resources = await context.service.listResources(endpoint.id);
|
||||
if (!registerWithManifest)
|
||||
if (!registerWithManifest && !connectExistingApp)
|
||||
await context.service.replaceResources(endpoint.id, [
|
||||
{ id: resources[0]!.id, enabled: true },
|
||||
]);
|
||||
@@ -2873,11 +2878,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
expect(session!.handoff?.identityLeaseId).toBeUndefined();
|
||||
expect(writes).toBe(2);
|
||||
});
|
||||
it("imports initial GitHub access without a second picker and leaves later additions disabled", async () => {
|
||||
it.each(["manifest", "existing"] as const)("imports initial GitHub access for a %s App without a second picker and leaves later additions disabled", async (method) => {
|
||||
const f = await seedCompany();
|
||||
const context = await configuredGitHubEndpoint(f, {}, true, true);
|
||||
const context = await configuredGitHubEndpoint(f, {}, true, method === "manifest", method === "existing");
|
||||
const current = await context.service.get(context.endpoint.id);
|
||||
expect(current.setup.github?.initialRepositoryImportPending).toBe(true);
|
||||
const registrations = await db.select().from(chatGitHubRegistrations).where(eq(chatGitHubRegistrations.endpointId, current.id));
|
||||
expect(registrations).toHaveLength(method === "manifest" ? 1 : 0);
|
||||
await db.update(chatGitHubRegistrations).set({ status: "completed" })
|
||||
.where(eq(chatGitHubRegistrations.endpointId, current.id));
|
||||
const result = await context.service.githubWizard.advance(
|
||||
@@ -2929,6 +2936,44 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
)?.enabled,
|
||||
).toBe(false);
|
||||
});
|
||||
it("manual App setup resumes tool initialization after an interrupted repository import", async () => {
|
||||
const f = await seedCompany();
|
||||
const context = await configuredGitHubEndpoint(f, {}, true, false, true);
|
||||
const wizard = githubChatWizardService(db, {
|
||||
origin: () => "https://reviews.example.test",
|
||||
fetch: context.providerFetch,
|
||||
startDirect: context.service.startGitHubRegistration,
|
||||
storeApp: context.service.storeGitHubApp,
|
||||
storeCredentials: async () => {},
|
||||
refreshRepositories: context.service.refreshGitHubRepositories,
|
||||
resources: context.service.listResources,
|
||||
replaceResources: async (...args) => {
|
||||
await context.service.replaceResources(...args);
|
||||
throw new Error("Interrupted after repository import");
|
||||
},
|
||||
configure: async () => {},
|
||||
finish: async () => {},
|
||||
});
|
||||
await expect(wizard.advance(context.endpoint.id, "owner-user")).rejects.toThrow("Interrupted after repository import");
|
||||
expect((await context.service.get(context.endpoint.id)).setup.github).toMatchObject({ initialSetupPending: true, initialRepositoryImportPending: false });
|
||||
await context.service.githubWizard.advance(context.endpoint.id, "owner-user");
|
||||
expect((await githubChatManagementService(db, context.providerFetch).configuration(context.endpoint.id, "owner-user")).configuration.toolsEnabled).toBe(true);
|
||||
expect((await context.service.get(context.endpoint.id)).setup.github).toMatchObject({ initialRepositoriesImported: true, initialSetupPending: false });
|
||||
});
|
||||
it("manual App setup preserves explicitly disabled repositories and tools", async () => {
|
||||
const f = await seedCompany();
|
||||
const context = await configuredGitHubEndpoint(f, {}, true, false, true);
|
||||
const management = githubChatManagementService(db, context.providerFetch);
|
||||
const saved = await management.configuration(context.endpoint.id, "owner-user");
|
||||
await management.saveConfiguration(context.endpoint.id, {
|
||||
expectedRevision: saved.revision,
|
||||
configuration: { ...saved.configuration, toolsEnabled: false },
|
||||
}, "owner-user");
|
||||
await context.service.replaceResources(context.endpoint.id, [], "owner-user");
|
||||
await context.service.githubWizard.advance(context.endpoint.id, "owner-user");
|
||||
expect((await context.service.listResources(context.endpoint.id)).every(resource => !resource.enabled)).toBe(true);
|
||||
expect(await management.configuration(context.endpoint.id, "owner-user")).toMatchObject({ revision: 1, configuration: { toolsEnabled: false } });
|
||||
});
|
||||
it("Cloud bot delivery cannot enter another company's endpoint or a different agent binding", async () => {
|
||||
const f = await reviewBotFixture();
|
||||
const event = {
|
||||
|
||||
@@ -38224,7 +38224,14 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
try {
|
||||
await db.transaction(async tx => {
|
||||
await credentialLease.assertOwned(tx);
|
||||
const claimed = await tx.update(chatEndpoints).set({ status: "attention", botExternalId: identity.botExternalId, botUsername: identity.botUsername, providerAccountId: identity.providerAccountId, providerAccountLabel: identity.providerAccountLabel, healthMessage: "Complete GitHub App setup", updatedAt: new Date() }).where(and(eq(chatEndpoints.id, endpointId), isNull(chatEndpoints.botExternalId))).returning({ id: chatEndpoints.id });
|
||||
const [current] = await tx.select({ setup: chatEndpoints.setup }).from(chatEndpoints).where(eq(chatEndpoints.id, endpointId)).for("update");
|
||||
const [resource] = await tx.select({ id: chatEndpointResources.id }).from(chatEndpointResources).where(eq(chatEndpointResources.endpointId, endpointId)).limit(1);
|
||||
// A fresh manual App has no registration session. Record its first
|
||||
// import before vault writes so interrupted credential storage can resume.
|
||||
const initialImport = !current?.setup.github?.repositorySelectionSaved && !resource;
|
||||
const claimed = await tx.update(chatEndpoints).set({ status: "attention", botExternalId: identity.botExternalId, botUsername: identity.botUsername, providerAccountId: identity.providerAccountId, providerAccountLabel: identity.providerAccountLabel,
|
||||
setup: sql`jsonb_set(${chatEndpoints.setup}, '{github}', coalesce(${chatEndpoints.setup}->'github', '{}'::jsonb) || ${JSON.stringify({ initialSetupPending: true, ...(initialImport ? { initialRepositoryImportPending: true } : {}) })}::jsonb)`,
|
||||
healthMessage: "Complete GitHub App setup", updatedAt: new Date() }).where(and(eq(chatEndpoints.id, endpointId), isNull(chatEndpoints.botExternalId))).returning({ id: chatEndpoints.id });
|
||||
if (claimed.length !== 1) throw conflict("The bot changed during registration");
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -38237,10 +38244,12 @@ export function chatChannelService(db: Db, options: ChatChannelServiceOptions) {
|
||||
if (!slug || !/^[a-z0-9-]+$/.test(slug)) throw unprocessable("GitHub did not identify this App");
|
||||
await db.transaction(async tx => {
|
||||
await credentialLease.assertOwned(tx);
|
||||
const [current] = await tx.select({ setup: chatEndpoints.setup, status: chatEndpoints.status }).from(chatEndpoints).where(eq(chatEndpoints.id, endpointId)).for("update");
|
||||
if (!current || ["archived", "active", "paused"].includes(current.status)) throw conflict("Use the reconnect flow for an existing active bot");
|
||||
await tx.update(chatEndpoints).set({
|
||||
status: "attention", botExternalId: identity.botExternalId, botUsername: identity.botUsername,
|
||||
botDisplayName: identity.botLabel, providerAccountId: identity.providerAccountId, providerAccountLabel: identity.providerAccountLabel,
|
||||
setup: { ...record.endpoint.setup, github: { ...record.endpoint.setup.github, stage: "install", appSlug: slug, installationUrl: `https://github.com/apps/${slug}/installations/new`, registrationStatus: "completed" } },
|
||||
setup: { ...current.setup, github: { ...current.setup.github, stage: "install", appSlug: slug, installationUrl: `https://github.com/apps/${slug}/installations/new`, registrationStatus: "completed" } },
|
||||
healthMessage: "Install the App on GitHub to continue", updatedAt: new Date(),
|
||||
}).where(eq(chatEndpoints.id, endpointId));
|
||||
await logActivity(tx as unknown as Db, { companyId: record.endpoint.companyId, actorType: "user", actorId: userId, action: "chat_github.app_connected", entityType: "tool_connection", entityId: record.endpoint.connectionId, details: { endpointId, appId: identity.botExternalId } });
|
||||
|
||||
@@ -720,9 +720,11 @@ export function githubChatWizardService(
|
||||
const saved = await management.configuration(id, userId);
|
||||
if (!bot.setup.github?.initialRepositoriesImported) {
|
||||
const resources = await options.resources(id);
|
||||
// Existing saved selections/behavior belong to the user. Only a fresh registration imports GitHub's choices.
|
||||
// Fresh manifest and manual Apps import GitHub's initial choices. Saved
|
||||
// selections and configuration still belong to the user on recovery.
|
||||
const initializeApp = !!session || bot.setup.github?.initialSetupPending === true;
|
||||
if (
|
||||
session &&
|
||||
initializeApp &&
|
||||
bot.status !== "active" &&
|
||||
saved.revision === 0 &&
|
||||
bot.setup.github?.initialRepositoryImportPending === true &&
|
||||
@@ -738,7 +740,7 @@ export function githubChatWizardService(
|
||||
{ initialGitHubImport: true },
|
||||
);
|
||||
}
|
||||
if (session && saved.revision === 0 && bot.status !== "active")
|
||||
if (initializeApp && saved.revision === 0 && bot.status !== "active")
|
||||
await management.saveConfiguration(
|
||||
id,
|
||||
{
|
||||
@@ -755,7 +757,7 @@ export function githubChatWizardService(
|
||||
},
|
||||
userId,
|
||||
);
|
||||
await setup(bot, { initialRepositoriesImported: true });
|
||||
await setup(bot, { initialRepositoriesImported: true, initialSetupPending: false });
|
||||
}
|
||||
if (bot.status !== "active" && bot.status !== "verifying")
|
||||
await options.configure(id, userId);
|
||||
|
||||
Reference in new issue
Block a user