mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
merge: reconcile published native prerequisite and current master
Preserve canonical startup ownership, scoped evidence, current Storybook IDs, and the Codex form controls while carrying Claude effort and immutable source bindings. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
48ffca9438
415 files changed
+22501
-15340
No files matched your search
@@ -156,6 +156,114 @@ test.describe.serial("native chat adapter UI", () => {
|
||||
seed = await seedCompanyAndAgent(request);
|
||||
});
|
||||
|
||||
test("Slack: managed workspace setup has two stops, resumes and completes without pasting credentials", async ({ page }) => {
|
||||
const slack = PROVIDERS.find(provider => provider.provider === "slack")!;
|
||||
const mock = await installChatControlPlaneMock(page, slack, seed, { enableChatConnectors: true, managedSlack: true });
|
||||
await page.goto(`/${seed.prefix}/apps/chat/connect?provider=slack&purpose=chat`);
|
||||
await selectMaya(page);
|
||||
await expect(page.getByRole("navigation", { name: "Connection setup progress" }).getByRole("button")).toHaveCount(2);
|
||||
await expect(page.getByRole("heading", { name: "Send a message to your agent", exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel("App configuration access token", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByLabel("Signing Secret", { exact: true })).toHaveCount(0);
|
||||
await page.reload();
|
||||
expect(mock.slackCreations).toBe(1); expect(mock.slackInstallations).toBe(1);
|
||||
mock.setWebhookVerified();
|
||||
await expect(page.getByRole("heading", { name: "Success", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("navigation", { name: "Connection setup progress" }).getByRole("button")).toHaveCount(2);
|
||||
await page.getByRole("button", { name: "Done", exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/apps\/chat\/endpoint-slack\/settings$/);
|
||||
});
|
||||
test("Slack: a failed manager refresh reloads workspace authorization before retry", async ({ page }) => {
|
||||
const slack = PROVIDERS.find(provider => provider.provider === "slack")!;
|
||||
await installChatControlPlaneMock(page, slack, seed, { enableChatConnectors: true, managedSlack: true });
|
||||
let refreshFailed = false;
|
||||
let authorizationRequests = 0;
|
||||
await page.route("**/chat-slack/setup-options", async route => {
|
||||
if (!refreshFailed) return route.fallback();
|
||||
await route.fulfill({ json: { managedAvailable: true, defaultMethod: "managed", workspaces: [] } });
|
||||
});
|
||||
await page.route("**/slack/managed/provision", async route => {
|
||||
refreshFailed = true;
|
||||
await route.fulfill({ status: 422, json: { error: "Reconnect your Slack workspace.", details: { code: "slack_manager_reauthorize" } } });
|
||||
});
|
||||
await page.route("**/slack/managed/authorize", async route => {
|
||||
authorizationRequests++;
|
||||
await route.fulfill({ json: { authorizationUrl: "https://slack.test/manager-consent", expiresAt: new Date(Date.now() + 600_000).toISOString() } });
|
||||
});
|
||||
await page.route("https://slack.test/manager-consent", route => route.fulfill({ contentType: "text/html", body: "Slack authorization fixture" }));
|
||||
await page.goto(`/${seed.prefix}/apps/chat/connect?provider=slack&purpose=chat&resume=endpoint-slack`);
|
||||
await expect(page.getByRole("alert")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect.poll(() => authorizationRequests).toBe(1);
|
||||
await expect(page).toHaveURL("https://slack.test/manager-consent");
|
||||
});
|
||||
test("Slack: every uncertain creation retry needs a fresh confirmation", async ({ page }) => {
|
||||
const slack = PROVIDERS.find(provider => provider.provider === "slack")!;
|
||||
const mock = await installChatControlPlaneMock(page, slack, seed, { enableChatConnectors: true, managedSlack: true });
|
||||
mock.setSlackUncertain();
|
||||
const attempts: { requestId: string; confirmedNoAppCreated?: boolean }[] = [];
|
||||
await page.route("**/slack/managed/provision", async route => {
|
||||
attempts.push(route.request().postDataJSON());
|
||||
await route.fulfill({ json: {} });
|
||||
});
|
||||
await page.goto(`/${seed.prefix}/apps/chat/connect?provider=slack&purpose=chat&resume=endpoint-slack`);
|
||||
const confirm = page.getByRole("checkbox", { name: "I checked Slack and no app was created. Create a new app." });
|
||||
const create = page.getByRole("button", { name: "Continue", exact: true });
|
||||
await expect(create).toBeDisabled();
|
||||
await confirm.check(); await create.click();
|
||||
await expect(confirm).not.toBeChecked(); await expect(create).toBeDisabled();
|
||||
expect(attempts).toHaveLength(1); expect(attempts[0]!.confirmedNoAppCreated).toBe(true);
|
||||
await confirm.check(); await create.click();
|
||||
await expect(confirm).not.toBeChecked(); await expect(create).toBeDisabled();
|
||||
expect(attempts).toHaveLength(2); expect(attempts[1]!.requestId).not.toBe(attempts[0]!.requestId);
|
||||
});
|
||||
test("Slack: own-app creation consumes confirmation even when the response is lost", async ({ page }) => {
|
||||
const slack = PROVIDERS.find(provider => provider.provider === "slack")!;
|
||||
const mock = await installChatControlPlaneMock(page, slack, seed, { enableChatConnectors: true, automaticSlack: true });
|
||||
mock.setSlackUncertain();
|
||||
let attempts = 0;
|
||||
await page.route("**/slack/registration", async route => {
|
||||
attempts++; expect(route.request().postDataJSON().confirmedNoAppCreated).toBe(true);
|
||||
await route.fulfill({ status: 502, json: { error: "Creation response unavailable" } });
|
||||
});
|
||||
await page.goto(`/${seed.prefix}/apps/chat/connect?provider=slack&purpose=chat&resume=endpoint-slack`);
|
||||
const confirm = page.getByRole("checkbox");
|
||||
await confirm.check();
|
||||
await page.getByLabel("App configuration access token", { exact: true }).fill("fixture-config-token");
|
||||
await page.getByRole("button", { name: "Create Slack app", exact: true }).click();
|
||||
await expect(page.getByRole("alert")).toBeVisible();
|
||||
await expect(confirm).not.toBeChecked();
|
||||
await expect(page.getByRole("button", { name: "Create Slack app", exact: true })).toBeDisabled();
|
||||
expect(attempts).toBe(1);
|
||||
});
|
||||
test("Slack: temporarily unavailable managed setup can retry in place", async ({ page }) => {
|
||||
const slack = PROVIDERS.find(provider => provider.provider === "slack")!;
|
||||
await installChatControlPlaneMock(page, slack, seed, { enableChatConnectors: true, managedSlack: true });
|
||||
let available = false;
|
||||
await page.route("**/chat-slack/setup-options", async route => {
|
||||
if (available) return route.fallback();
|
||||
await route.fulfill({ json: { managedAvailable: false, defaultMethod: "automatic", workspaces: [] } });
|
||||
});
|
||||
await page.goto(`/${seed.prefix}/apps/chat/connect?provider=slack&purpose=chat&resume=endpoint-slack`);
|
||||
await expect(page.getByText("Managed Slack setup is unavailable. Try again later.")).toBeVisible();
|
||||
available = true; await page.getByRole("button", { name: "Retry", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Send a message to your agent", exact: true })).toBeVisible();
|
||||
});
|
||||
test("Slack: own-app selection persists before provisioning", async ({ page }) => {
|
||||
const slack = PROVIDERS.find(provider => provider.provider === "slack")!;
|
||||
await installChatControlPlaneMock(page, slack, seed, { enableChatConnectors: true, managedSlack: true });
|
||||
await page.goto(`/${seed.prefix}/apps/chat/connect?provider=slack&purpose=chat`);
|
||||
await page.getByRole("button", { name: "Choose an active agent" }).click();
|
||||
await page.getByRole("button", { name: "Select Maya" }).click();
|
||||
await page.getByText("Advanced", { exact: true }).click();
|
||||
await page.getByRole("button", { name: "Use your own app" }).click();
|
||||
await expect(page.getByRole("heading", { name: "App configuration access token", exact: true })).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(page.getByRole("heading", { name: "App configuration access token", exact: true })).toBeVisible();
|
||||
await page.getByText("Advanced", { exact: true }).click();
|
||||
await page.getByRole("button", { name: "Use managed setup" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Send a message to your agent", exact: true })).toBeVisible();
|
||||
});
|
||||
test("Slack: automatic creation survives refresh, consent, and connection evidence without copying durable secrets", async ({ page }) => {
|
||||
const slack = PROVIDERS.find(provider => provider.provider === "slack")!;
|
||||
const mock = await installChatControlPlaneMock(page, slack, seed, { enableChatConnectors: true, automaticSlack: true });
|
||||
@@ -440,6 +548,10 @@ test.describe.serial("native chat adapter UI", () => {
|
||||
).toBeVisible();
|
||||
await expectSetupRail(page, true);
|
||||
await selectMaya(page);
|
||||
if (provider.provider === "slack") {
|
||||
await page.getByText("Advanced", { exact: true }).click();
|
||||
await page.getByRole("button", { name: "Create manually", exact: true }).click();
|
||||
}
|
||||
await expect.poll(() => mock.createdWithAgentId).toBe(seed.agentId);
|
||||
expect(mock.createdWithAgentId).not.toBe(seed.otherAgentId);
|
||||
await expect(
|
||||
@@ -573,18 +685,12 @@ test.describe.serial("native chat adapter UI", () => {
|
||||
page.getByRole("heading", { name: provider.provider === "slack" ? "Allowed Channels" : "Destinations" }),
|
||||
).toBeVisible();
|
||||
if (provider.provider === "slack") {
|
||||
await page.locator("summary").filter({ hasText: "Message in a channel" }).click();
|
||||
await expect(page.getByText("@maya-paperclip you there?", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Copy message" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Allowed Channels" })).toBeVisible();
|
||||
await page.locator("summary").filter({ hasText: /^Agent avatar$/ }).click();
|
||||
const avatarSection = page.getByRole("region", { name: "Slack avatar" });
|
||||
await expect(avatarSection.getByRole("link", { name: "Download avatar" })).toBeVisible();
|
||||
await avatarSection.getByText("How to upload in Slack", { exact: true }).click();
|
||||
await expect(avatarSection.getByRole("link", { name: "Open Slack app Settings" })).toBeVisible();
|
||||
const settingsDownloadEvent = page.waitForEvent("download");
|
||||
await avatarSection.getByRole("link", { name: "Download avatar" }).click();
|
||||
expect((await settingsDownloadEvent).suggestedFilename()).toBe("maya-paperclip-avatar.png");
|
||||
await expect(page.getByRole("button", { name: "Send me a test message" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Open Slack configuration" })).toBeVisible();
|
||||
await expect(page.getByText("Message in a channel", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText("Agent avatar", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("link", { name: "View message" })).toHaveCount(0);
|
||||
}
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -281,7 +281,7 @@ export function endpointFixture(provider: ProviderCase, seed: Seed) {
|
||||
proactiveDirectMessages: false,
|
||||
},
|
||||
setup: {
|
||||
slackSetupMethod: undefined as "automatic" | "manual" | "existing" | undefined,
|
||||
slackSetupMethod: undefined as "managed" | "automatic" | "manual" | "existing" | undefined,
|
||||
slackRegistration: undefined as SlackRegistrationState | undefined,
|
||||
slackApp: undefined as SlackAppConfiguration | undefined,
|
||||
slackOAuthCallbackUri: "https://paperclip.example.test/api/chat-slack/oauth/callback",
|
||||
@@ -353,10 +353,11 @@ export async function installChatControlPlaneMock(
|
||||
resourceCount = 2,
|
||||
photonShared = false,
|
||||
automaticSlack = false,
|
||||
}: { enableChatConnectors: boolean; enableGitHubReviewBots?: boolean; resourceCount?: number; photonShared?: boolean; automaticSlack?: boolean },
|
||||
managedSlack = false,
|
||||
}: { enableChatConnectors: boolean; enableGitHubReviewBots?: boolean; resourceCount?: number; photonShared?: boolean; automaticSlack?: boolean; managedSlack?: boolean },
|
||||
): Promise<ChatMock> {
|
||||
const endpoint = endpointFixture(provider, seed);
|
||||
if (automaticSlack) endpoint.setup.slackSetupMethod = "automatic";
|
||||
if (automaticSlack || managedSlack) endpoint.setup.slackSetupMethod = managedSlack ? "managed" : "automatic";
|
||||
let slackIdentityLinked = false;
|
||||
let githubAppConnected = false;
|
||||
let githubConfiguration = { revision: 0, configuration: {
|
||||
@@ -510,6 +511,7 @@ export async function installChatControlPlaneMock(
|
||||
assignedAgentId: seed.agentId,
|
||||
});
|
||||
state.createdWithAgentId = String(body.assignedAgentId);
|
||||
if (body.slackSetupMethod) endpoint.setup.slackSetupMethod = body.slackSetupMethod as "managed" | "automatic" | "manual" | "existing";
|
||||
if (provider.provider === "slack" && body.slackApp) {
|
||||
endpoint.setup.slackApp = body.slackApp as SlackAppConfiguration;
|
||||
endpoint.setup.command = endpoint.setup.slackApp.command;
|
||||
@@ -536,7 +538,7 @@ export async function installChatControlPlaneMock(
|
||||
if (method === "PATCH") {
|
||||
const body = bodyOf(route);
|
||||
if (body.slackApp) endpoint.setup.slackApp = body.slackApp as SlackAppConfiguration;
|
||||
if (body.slackSetupMethod) endpoint.setup.slackSetupMethod = body.slackSetupMethod as "automatic" | "manual" | "existing";
|
||||
if (body.slackSetupMethod) endpoint.setup.slackSetupMethod = body.slackSetupMethod as "managed" | "automatic" | "manual" | "existing";
|
||||
if (typeof body.allowDirectMessages === "boolean")
|
||||
state.allowDirectMessages = body.allowDirectMessages;
|
||||
if (typeof body.allowGroupChats === "boolean")
|
||||
@@ -563,6 +565,16 @@ export async function installChatControlPlaneMock(
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === `/api/companies/${seed.companyId}/chat-slack/setup-options`) {
|
||||
await fulfill(route, { managedAvailable: managedSlack, defaultMethod: managedSlack ? "managed" : "automatic", workspaces: managedSlack ? [{ grantId: "11111111-1111-4111-8111-111111111111", workspaceId: "TE2E", workspaceName: "Paperclip", userId: "UINSTALLER" }] : [] }); return;
|
||||
}
|
||||
if (pathname === `/api/chat-endpoints/${endpoint.id}/slack/managed/provision`) {
|
||||
const body = bodyOf(route);
|
||||
expect(body.grantId).toBe("11111111-1111-4111-8111-111111111111");
|
||||
expect(body).not.toHaveProperty("credentials");
|
||||
state.slackCreations++; state.slackInstallations++;
|
||||
state.setSlackInstalled(); await fulfill(route, {}); return;
|
||||
}
|
||||
if (pathname === `/api/chat-endpoints/${endpoint.id}/slack/registration`) {
|
||||
const body = bodyOf(route);
|
||||
expect(body.credentials).toEqual({ configurationToken: "fixture-config-token" });
|
||||
|
||||
@@ -12,8 +12,6 @@ import { CREDENTIAL_NAMES, type MatrixExecution } from "./types.js";
|
||||
export const installedCliKeys = ["PAPERCLIP_RUNNER_E2E_INSTALLED_CLI", "PAPERCLIP_RUNNER_E2E_INSTALLED_CLI_SHA256", "PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_ROOT", "PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_SHA256"] as const;
|
||||
const overrideKeys = ["PAPERCLIP_RUNNER_BINARY", "PAPERCLIP_RUNNER_REMOTE_BINARY_PATH", "PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH", "PAPERCLIP_RUNNER_ACPX_QUALIFICATION", "PAPERCLIP_ACPX_BUILTIN_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"] as const;
|
||||
const suites = new Set(["extended-harnesses", "pi-native", "pi-controls", "rich-acp-warm-continuity"]);
|
||||
// Reuse the production-onboarding, independent file/revision, and restart
|
||||
// journeys. Installed proof does not add cases or relax their existing graders.
|
||||
const installedCodexCells = new Set([
|
||||
"first-task.runner-codex.local.task-card-accept",
|
||||
"everyday-workflows.runner-codex.local.build-revise",
|
||||
@@ -63,7 +61,6 @@ export async function verifyInstalledCli(env: NodeJS.ProcessEnv, executions: rea
|
||||
return { schema: "paperclip.e2e.installed-cli-admission/v1", entry, cliRoot, cliSha256, serverRoot, serverEntry, serverSha256, version: cli.version, defaultRuntimeResolution: true, qualificationOverride: false };
|
||||
}
|
||||
|
||||
/** Clean-install protocol proof only: no credentials, task, or provider turn. */
|
||||
export async function probeInstalledCodexStartup(input: {
|
||||
consumerRoot: string;
|
||||
serverRoot: string;
|
||||
@@ -110,8 +107,6 @@ export async function probeInstalledCodexStartup(input: {
|
||||
const codexSha256 = digest(await readOwned(codexCommand, 512 * 1024 * 1024));
|
||||
const codexVersion = execFileSync(codexCommand, ["--version"], { cwd: workspace, env: environment, encoding: "utf8", timeout: 10000, maxBuffer: 16384 }).trim();
|
||||
if (!codexVersion || codexVersion.length > 200) throw new Error("Installed Codex startup returned no bounded version identity");
|
||||
// Keep the production security arguments; pin file-only auth for this
|
||||
// credential-free fixture so macOS keychain accounts cannot be consulted.
|
||||
const args = runtime.createIsolatedCodexAppServerArgs(environment);
|
||||
args.splice(args.indexOf("app-server"), 0, "-c", 'cli_auth_credentials_store="file"');
|
||||
bundle = runtime.createRunnerdCodexTransport({ provider: "codex", stateDirectory: directory, environment,
|
||||
@@ -156,9 +151,6 @@ export async function probeInstalledCodexStartup(input: {
|
||||
} catch (error) { cleanupFailures.push(error); }
|
||||
if (startupFailed) {
|
||||
const evidence = bundle?.evidence();
|
||||
// This zero-turn fixture has empty homes and no provider credentials.
|
||||
// Report only bounded failure text and lifecycle state, never identities
|
||||
// or raw durable state. A close failure cannot hide the startup cause.
|
||||
const bounded = (value: unknown) => String(value).replaceAll(directory, "[private-startup]").slice(0, 2000);
|
||||
const primaryMessage = bounded(startupFailure instanceof Error ? startupFailure.message : startupFailure);
|
||||
const diagnostic = { schema: "paperclip.e2e.installed-codex-startup-failure/v1", platform: `${process.platform}-${process.arch}`,
|
||||
|
||||
@@ -64,7 +64,6 @@ export interface LiveFixtureValues {
|
||||
agent: AgentRecord;
|
||||
project?: ProjectRecord;
|
||||
aiConnection?: ManagedAccountFixture;
|
||||
/** Private setup receipt; its lease is included in existing runtime cost accounting. */
|
||||
setupProbe?: { leaseId: string; durationMs: number; defaultRunner?: string };
|
||||
|
||||
onboardingRuntime?: {
|
||||
@@ -82,7 +81,6 @@ function value<T>(resolved: ReadonlyMap<string, unknown>, id: string): T {
|
||||
return result as T;
|
||||
}
|
||||
|
||||
/** Preserve task/run attribution and include only this fixture's explicit setup probe. */
|
||||
export function selectFixtureRuntimeLeases<T extends {
|
||||
id: string; provider?: string | null; heartbeatRunId?: string | null; issueId?: string | null;
|
||||
}>(input: { leases: T[]; runIds: string[]; issueId?: string; setupProbeLeaseId?: string }): T[] {
|
||||
@@ -320,8 +318,6 @@ export async function setupLiveFixtures(input: {
|
||||
throw new Error("Native Codex qualification requires this fixture's immutable image-backed environment.");
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
// One existing explicit connection probe: a fresh lease, bounded platform read,
|
||||
// verified release, and server-owned image evidence before agent creation.
|
||||
const probe = await api.post<{ ok: boolean; details?: { platformQualificationRecorded?: boolean; leaseId?: string } }>(
|
||||
`/api/environments/${environment.id}/probe?companyId=${encodeURIComponent(company.id)}`,
|
||||
{}, { timeout: 330_000 },
|
||||
|
||||
Reference in new issue
Block a user