Files
PaperClipAI/packages/shared/src/app-definitions.test.ts
T
DottaandPaperclip f449b05bc5 feat(apps): unify permissions and action testing (#12802)
## Thinking Path

> - Paperclip is the control plane for companies that use AI agents.
> - Apps give humans and agents controlled access to external services.
> - The existing app detail flow split permissions, tests, setup, and
activity across separate pages.
> - The split made access rules harder to understand and made reconnect
work hard to find.
> - New write actions also defaulted to Ask first, which did not match
the intended connection policy.
> - This pull request combines permission control and action testing,
removes the setup page, and moves connection activity into Audit.
> - The benefit is one clear place to configure, test, reconnect, and
review each app.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The installed app Permissions, Test, Setup, and Activity views.

**Subsystem affected**

Cross-cutting. This change updates the React UI, shared app defaults,
server permission behavior, tests, smoke scripts, and connection
documentation.

**Current behavior**

App access and action testing use separate pages. The app detail view
also links to a setup page after installation. Connection activity uses
a separate tab. New write actions default to Ask first.

**Proposed behavior**

Permissions uses the connection access language from the initial flow.
It includes searchable Read and Write sections, a three-state permission
control, and a Test dialog for each action. Reconnect appears below a
Needs attention header on Permissions and Review. Old Setup and Test
links redirect to Permissions. Old Activity links redirect to the
filtered company Audit feed. New write actions default to Allowed.

**Reason and benefit**

A person can understand and test app access without moving between
several pages. Reconnect work stays visible where the person reviews the
connection. Audit events use one consistent feed and filter model. New
connections have the intended default policy.

**Breaking changes**

The Setup, Test, and app Activity tabs are removed. Existing deep links
redirect to their replacement pages. Existing saved action permissions
do not change. Only defaults for new write actions change.

**Additional context**

This builds on the managed app connection work in #12728. A search found
no duplicate open pull request or issue.

## What Changed

- Combined action testing with Permissions.
- Added searchable Read and Write action groups.
- Added Off, Ask first, and Allowed controls with tooltips.
- Added an action Test dialog with agent selection, arguments, and
formatted results.
- Removed the installed-app Setup and Activity tabs.
- Added reconnect guidance to Permissions and Review when a connection
needs attention.
- Routed connection activity into the company Audit feed and preserved
the Apps & tools filter in streamlined Audit.
- Moved connection removal to the Connectors-page management menu.
- Made new write actions default to Allowed across connection creation
paths.
- Updated regression tests, browser suites, smoke scripts, and
connection documentation.

## Verification

- `pnpm check:token-gates`
- `pnpm exec vitest run packages/shared/src/app-definitions.test.ts
server/src/__tests__/generic-mcp-connection.test.ts
server/src/__tests__/tool-access-service.test.ts
ui/src/components/AppConnectionSidebar.test.tsx
ui/src/pages/apps/AppDetail.test.tsx
ui/src/pages/apps/AppNotConnected.test.tsx
ui/src/pages/apps/AppsConnect.test.tsx ui/src/pages/apps/Browse.test.tsx
ui/src/pages/apps/Connections.test.tsx
ui/src/pages/apps/composio-services.test.ts
ui/src/pages/audit/AuditFeed.test.tsx
ui/src/pages/tools/PasteConfigTab.test.tsx` (517 tests passed)
- `pnpm exec vitest run ui/src/pages/apps/app-detail/TestPanel.test.tsx
ui/src/pages/audit/AuditHub.test.tsx
ui/src/pages/audit/AuditFeed.test.tsx
ui/src/pages/apps/AppDetail.test.tsx ui/src/pages/apps/Browse.test.tsx`
(96 tests passed)
- Targeted Playwright verification for connection removal, rename on
Permissions, inline action testing, and Smoke Lab Audit evidence (5
flows passed)
- `pnpm -r typecheck`
- `pnpm build`
- `pnpm test:run` completed with 5,755 passing tests and 20 unrelated
macOS harness failures. The failures use `/tmp` versus `/private/tmp`,
invalid ports above 65535, and workspace fixtures outside this change.

## Risks

- Low migration risk. This change has no database migration.
- Old app-detail URLs depend on redirect compatibility.
- New connections grant write actions by default. Finalization remains
configure-authorized and audited, Ask first and Off remain available per
action, and existing connections keep their saved policy.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected - check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex, exact model ID `gpt-5`. The client does not expose the
context-window size. The model used reasoning, repository tools, code
execution, and browser verification.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-03 21:23:26 -05:00

238 lines
22 KiB
TypeScript

import { describe,expect,it } from "vitest";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { APP_DEFINITIONS } from "./app-definitions.generated.js";
import { APP_STORE_DEFINITIONS, APP_STORE_HIDDEN_SLUGS, CONNECTABLE_APP_DEFINITIONS, appSupportsCatalogSetup, getAvailableConnectionMethod, getRecommendedConnectionMethod, recommendedDefaultsForApp, resolveConnectionMethodServerUrl } from "./app-definitions.js";
import { GOOGLE_WORKSPACE_CONNECTOR_PROFILE_IDS, GOOGLE_WORKSPACE_CONNECTOR_PROFILES, type GoogleWorkspaceConnectorProfileId } from "./google-workspace-connectors.js";
import { BLOCKED_MCP_PROVIDERS, SELF_SERVE_MCP_CANDIDATES, SELF_SERVE_MCP_RESEARCH } from "./self-serve-mcp-research.js";
import { appDefinitionsSchema } from "./validators/app-definition.js";
const googleScope=(scope:string)=>`https://www.googleapis.com/auth/${scope}`;
const GOOGLE_WORKSPACE_PROFILE_EXPECTATIONS = [
{profile:"gmail.read",appSlug:"gmail",serverUrl:"https://gmailmcp.googleapis.com/mcp/v1",capability:"read",riskTier:"S3",scopes:[googleScope("gmail.readonly")],writeTools:[]},
{profile:"gmail.draft",appSlug:"gmail",serverUrl:"https://gmailmcp.googleapis.com/mcp/v1",capability:"draft",riskTier:"S4",scopes:[googleScope("gmail.readonly"),googleScope("gmail.compose")],writeTools:["create_draft"]},
{profile:"drive.read",appSlug:"google-drive",serverUrl:"https://drivemcp.googleapis.com/mcp/v1",capability:"read",riskTier:"S3",scopes:[googleScope("drive.readonly")],writeTools:[]},
{profile:"drive.write",appSlug:"google-drive",serverUrl:"https://drivemcp.googleapis.com/mcp/v1",capability:"write",riskTier:"S4",scopes:[googleScope("drive.readonly"),googleScope("drive.file")],writeTools:["copy_file","create_file"]},
{profile:"docs.read",appSlug:"google-docs",serverUrl:"https://docsmcp.googleapis.com/mcp/v1",capability:"read",riskTier:"S3",scopes:[googleScope("drive.readonly"),googleScope("documents.readonly")],writeTools:[]},
{profile:"docs.write",appSlug:"google-docs",serverUrl:"https://docsmcp.googleapis.com/mcp/v1",capability:"write",riskTier:"S4",scopes:[googleScope("drive.readonly"),googleScope("drive.file"),googleScope("documents")],writeTools:["update_doc"]},
{profile:"sheets.read",appSlug:"google-sheets",serverUrl:"https://sheetsmcp.googleapis.com/mcp/v1",capability:"read",riskTier:"S3",scopes:[googleScope("drive.readonly"),googleScope("spreadsheets.readonly")],writeTools:[]},
{profile:"sheets.write",appSlug:"google-sheets",serverUrl:"https://sheetsmcp.googleapis.com/mcp/v1",capability:"write",riskTier:"S4",scopes:[googleScope("drive.readonly"),googleScope("drive.file"),googleScope("spreadsheets")],writeTools:["update_spreadsheet","update_values","update_formulas","insert_dimension"]},
{profile:"slides.read",appSlug:"google-slides",serverUrl:"https://slidesmcp.googleapis.com/mcp/v1",capability:"read",riskTier:"S3",scopes:[googleScope("drive.readonly"),googleScope("presentations.readonly")],writeTools:[]},
{profile:"slides.write",appSlug:"google-slides",serverUrl:"https://slidesmcp.googleapis.com/mcp/v1",capability:"write",riskTier:"S4",scopes:[googleScope("drive.readonly"),googleScope("drive.file"),googleScope("presentations")],writeTools:["update_presentation"]},
{profile:"calendar.read",appSlug:"google-calendar",serverUrl:"https://calendarmcp.googleapis.com/mcp/v1",capability:"read",riskTier:"S3",scopes:[googleScope("calendar.calendarlist.readonly"),googleScope("calendar.events.freebusy"),googleScope("calendar.events.readonly")],writeTools:[]},
{profile:"calendar.write",appSlug:"google-calendar",serverUrl:"https://calendarmcp.googleapis.com/mcp/v1",capability:"write",riskTier:"S4",scopes:[googleScope("calendar.calendarlist.readonly"),googleScope("calendar.events.freebusy"),googleScope("calendar.events")],writeTools:["create_event","update_event","delete_event","respond_to_event"]},
{profile:"chat.read",appSlug:"google-chat",serverUrl:"https://chatmcp.googleapis.com/mcp/v1",capability:"read",riskTier:"S3",scopes:[googleScope("chat.spaces.readonly"),googleScope("chat.memberships.readonly"),googleScope("chat.messages.readonly"),googleScope("chat.users.readstate.readonly")],writeTools:[]},
{profile:"chat.write",appSlug:"google-chat",serverUrl:"https://chatmcp.googleapis.com/mcp/v1",capability:"write",riskTier:"S4",scopes:[googleScope("chat.spaces.readonly"),googleScope("chat.memberships.readonly"),googleScope("chat.messages.readonly"),googleScope("chat.users.readstate.readonly"),googleScope("chat.messages.create")],writeTools:["send_message"]},
{profile:"people.read",appSlug:"google-people",serverUrl:"https://people.googleapis.com/mcp/v1",capability:"read",riskTier:"S3",scopes:[googleScope("directory.readonly"),googleScope("userinfo.profile"),googleScope("contacts.readonly")],writeTools:[]},
{profile:"workspace-search.read",appSlug:"google-workspace-search",serverUrl:"https://workspacemcp.googleapis.com/mcp/v1",capability:"read",riskTier:"S3",scopes:[googleScope("gmail.readonly"),googleScope("drive.readonly"),googleScope("calendar.readonly"),googleScope("chat.messages.readonly")],writeTools:[]},
] as const satisfies ReadonlyArray<{profile:GoogleWorkspaceConnectorProfileId;appSlug:string;serverUrl:string;capability:"read"|"write"|"draft";riskTier:"S3"|"S4";scopes:readonly string[];writeTools:readonly string[]}>;
describe("AppDefinition catalog",()=>{
it("validates all Wave 1 definitions",()=>expect(()=>appDefinitionsSchema.parse(APP_DEFINITIONS)).not.toThrow());
it("contains every established provider plus the reviewed self-serve catalog",()=>{
expect(APP_DEFINITIONS.map((app)=>app.slug)).toEqual(expect.arrayContaining(["zapier","github","slack","notion","posthog","linear","google-sheets","context7","composio","oauth-generic","api-key-generic","sentry","vercel","anthropic","gmail","google-drive","google-docs","google-slides","google-calendar","google-chat","google-people","google-workspace-search"]));
expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(43);
expect(BLOCKED_MCP_PROVIDERS.map((entry)=>entry.slug)).toEqual(["g2","vercel","zomato"]);
const definitionSlugs=new Set(APP_DEFINITIONS.map((app)=>app.slug));
const connectableSlugs=new Set(CONNECTABLE_APP_DEFINITIONS.map((app)=>app.slug));
expect(SELF_SERVE_MCP_CANDIDATES.filter((entry)=>!definitionSlugs.has(entry.slug))).toEqual([]);
expect(SELF_SERVE_MCP_CANDIDATES.filter((entry)=>!connectableSlugs.has(entry.slug))).toEqual([]);
for(const entry of BLOCKED_MCP_PROVIDERS)expect(connectableSlugs.has(entry.slug)).toBe(false);
});
it("keeps a complete, unique, dated evidence ledger for all 46 researched MCP providers",()=>{
expect(SELF_SERVE_MCP_RESEARCH.verifiedAt).toBe("2026-08-26");
expect(SELF_SERVE_MCP_RESEARCH.entries).toHaveLength(46);
expect(new Set(SELF_SERVE_MCP_RESEARCH.entries.map((entry)=>entry.slug))).toHaveProperty("size",46);
for(const entry of SELF_SERVE_MCP_RESEARCH.entries){
expect(new URL(entry.docsUrl).protocol).toBe("https:");
expect(new URL(entry.serverUrl).protocol).toBe("https:");
expect(entry.authMode).toBeTruthy();
expect(entry.prerequisite.length).toBeGreaterThan(10);
expect(["S1","S2","S3","S4"]).toContain(entry.riskTier);
}
});
it("uses the reviewed current endpoints and configuration modes",()=>{
const method=(slug:string,key?:string)=>APP_DEFINITIONS.find((app)=>app.slug===slug)?.methods.find((candidate)=>!key||candidate.key===key);
expect(method("jira")?.defaults?.serverUrl).toBe("https://mcp.atlassian.com/v1/mcp/authv2");
expect(method("jira")?.defaults?.scopesHint).toEqual([
"read:me",
"read:account",
"offline_access",
"email",
"read:jira-work",
"write:jira-work",
"search:confluence",
"read:confluence-user",
"read:page:confluence",
"write:page:confluence",
"read:comment:confluence",
"write:comment:confluence",
"read:space:confluence",
"read:hierarchical-content:confluence",
"write:component:compass",
"read:component:compass",
"read:scorecard:compass",
"write:scorecard:compass",
"read:event:compass",
"read:metric:compass",
"read:all:twg",
"write:all:twg",
]);
expect(method("cloudinary")?.defaults?.serverUrl).toBe("https://asset-management.mcp.cloudinary.com/mcp");
expect(method("kernel")?.defaults?.serverUrl).toBe("https://mcp.onkernel.com/mcp");
expect(method("resend")?.defaults?.serverUrl).toBe("https://mcp.resend.com/mcp");
expect(method("clickhouse")?.defaults?.serverUrl).toBe("https://mcp.clickhouse.cloud/clickstack");
expect(method("clickhouse")?.tenantFields?.[0]?.transport).toEqual({location:"header",name:"x-service-id"});
expect(method("mem0")).toMatchObject({auth:"api_key",keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "}});
expect(method("mem0")?.defaults?.serverUrl).toBe("https://mcp.mem0.ai/mcp/");
expect(method("xero")?.defaults?.scopesHint).toEqual([
"openid","profile","email","offline_access","accounting.settings","accounting.invoices.read","accounting.reports.aged.read","accounting.reports.balancesheet.read","accounting.reports.profitandloss.read",
]);
expect(APP_DEFINITIONS.find((app)=>app.slug==="pagerduty")?.methods.map((candidate)=>({key:candidate.key,serverUrl:candidate.defaults?.serverUrl}))).toEqual([
{key:"mcp-api-key-us",serverUrl:"https://mcp.pagerduty.com/mcp"},
{key:"mcp-api-key-eu",serverUrl:"https://mcp.eu.pagerduty.com/mcp"},
]);
expect(method("context7")).toMatchObject({auth:"none",defaults:{serverUrl:"https://mcp.context7.com/mcp"}});
expect(APP_DEFINITIONS.find((app)=>app.slug==="planetscale")?.methods.map((candidate)=>candidate.key)).toEqual(["mcp-oauth","mcp-insights-only"]);
const postman=APP_DEFINITIONS.find((app)=>app.slug==="postman");
expect(postman?.methods.map((candidate)=>candidate.key)).toEqual([
"mcp-oauth-minimal","mcp-oauth-code","mcp-oauth-full","mcp-eu-key-minimal","mcp-eu-key-code","mcp-eu-key-full",
]);
expect(getAvailableConnectionMethod(postman!)?.key).toBe("mcp-oauth-full");
expect(postman?.methods.filter((candidate)=>candidate.auth==="api_key").every((candidate)=>
candidate.keyPlacement?.name==="Authorization"&&candidate.keyPlacement.prefix==="Bearer "
)).toBe(true);
expect(method("supabase")?.tenantFields?.find((field)=>field.key==="readOnly")?.defaultValue).toBe(false);
expect(method("asana")?.ownershipModes).toEqual(["customer"]);
expect(method("zapier")).toMatchObject({key:"generated-url",auth:"none",defaults:{}});
expect(method("zapier")?.credentialFields).toBeUndefined();
});
it("uses discovery-first Notion MCP OAuth metadata",()=>{
const notion=APP_DEFINITIONS.find((app)=>app.slug==="notion");
expect(notion?.redirectConstraints).toBe("https-or-loopback-http");
expect(notion?.methods[0]?.defaults).toEqual({serverUrl:"https://mcp.notion.com/mcp"});
});
it("preserves required Linear OAuth scopes",()=>expect(APP_DEFINITIONS.find((app)=>app.slug==="linear")?.methods[0]?.defaults?.scopesHint).toEqual(["read","write"]));
it("requests only Hugging Face's MCP read scope",()=>expect(APP_DEFINITIONS.find((app)=>app.slug==="hugging-face")?.methods[0]?.defaults?.scopesHint).toEqual(["read-mcp"]));
it("defaults every new connection action to allowed",()=>{for(const app of APP_DEFINITIONS)for(const method of app.methods)expect(recommendedDefaultsForApp(app,method.key)).toEqual({access:"all_agents",askFirstRiskLevels:[]})});
it("defaults explicit read/write capability groups to their write-capable method",()=>{
const drive=APP_DEFINITIONS.find((app)=>app.slug==="google-drive")!;
const gmail=APP_DEFINITIONS.find((app)=>app.slug==="gmail")!;
expect(getAvailableConnectionMethod(drive)?.key).toBe("customer-write-oauth");
expect(getAvailableConnectionMethod(gmail)?.key).toBe("customer-draft-oauth");
expect(getRecommendedConnectionMethod(drive.methods.filter((candidate)=>candidate.ownershipModes.includes("customer")))?.key).toBe("customer-write-oauth");
expect(getRecommendedConnectionMethod(gmail.methods.filter((candidate)=>[
"paperclip-read","customer-read-oauth","customer-draft-oauth",
].includes(candidate.key)))?.key).toBe("paperclip-read");
expect(getRecommendedConnectionMethod(gmail.methods.filter((candidate)=>candidate.capabilityProfile?.key==="draft"))?.key).toBe("paperclip-draft");
expect(getRecommendedConnectionMethod(gmail.methods.filter((candidate)=>
candidate.capabilityProfile?.key==="draft"&&candidate.ownershipModes.includes("customer")
))?.key).toBe("customer-draft-oauth");
});
it("explains Google Workspace Developer Preview enrollment before connection",()=>{
const googleWorkspaceMcpSlugs=["gmail","google-drive","google-docs","google-sheets","google-slides","google-calendar","google-chat","google-people","google-workspace-search"];
for(const slug of googleWorkspaceMcpSlugs){
const prerequisite=APP_DEFINITIONS.find((app)=>app.slug===slug)?.setupPrerequisite;
expect(prerequisite?.actionUrl,slug).toBe("https://developers.google.com/workspace/preview");
expect(prerequisite?.description,slug).toContain("does not enable unrelated Paperclip customers");
expect(prerequisite?.steps?.join(" "),slug).toContain("final project-registration email");
}
});
it("withholds unverified and reserved providers from the app store without deleting their definitions",()=>{
expect([...APP_STORE_HIDDEN_SLUGS].sort()).toEqual([
"beehiiv","bitly","brex","candid","coda","composio","context7","egnyte","embat","github","kernel","local-falcon","make","manufact","oreilly","planetscale","razorpay","sanity","similarweb","slack","ticket-tailor","ticktick","xero",
]);
expect(APP_STORE_DEFINITIONS).toHaveLength(35);
const connectableSlugs=new Set(CONNECTABLE_APP_DEFINITIONS.map((entry)=>entry.slug));
const storeSlugs=new Set(APP_STORE_DEFINITIONS.map((entry)=>entry.slug));
for(const slug of APP_STORE_HIDDEN_SLUGS){
expect(connectableSlugs.has(slug),slug).toBe(true);
expect(storeSlugs.has(slug),slug).toBe(false);
}
});
it("ships complete local branding provenance for all 35 store-visible providers",()=>{
const uiPublic=path.resolve(path.dirname(fileURLToPath(import.meta.url)),"../../../ui/public");
const manifest=JSON.parse(fs.readFileSync(path.join(uiPublic,"brands/apps/manifest.json"),"utf8")) as {providers:Array<{slug:string;catalogVisible:boolean;localAsset:string;darkAsset?:string;officialSourceUrl:string;upstreamAssetUrl:string;assetType:"svg"|"png";darkVariantRequired:boolean}>};
const visible=manifest.providers.filter((entry)=>entry.catalogVisible);
expect(visible).toHaveLength(35);
expect(new Set(visible.map((entry)=>entry.slug))).toHaveProperty("size",35);
expect(new Set(visible.map((entry)=>entry.localAsset))).toHaveProperty("size",35);
expect(new Set(APP_STORE_DEFINITIONS.map((entry)=>entry.slug))).toEqual(new Set(visible.map((entry)=>entry.slug)));
for(const app of APP_STORE_DEFINITIONS){
const provenance=visible.find((entry)=>entry.slug===app.slug)!;
expect(provenance).toBeTruthy();
expect(provenance.localAsset).toBe(app.branding.logoUrl);
expect(provenance.darkAsset).toBe(app.branding.darkLogoUrl);
expect(provenance.darkVariantRequired).toBe(Boolean(provenance.darkAsset));
expect(new URL(provenance.officialSourceUrl).protocol).toBe("https:");
expect(new URL(provenance.upstreamAssetUrl).protocol).toBe("https:");
expect(provenance.localAsset).toMatch(/^\/brands\/apps\/.+\.(svg|png)$/);
expect(provenance.localAsset).not.toContain("google.com/s2/favicons");
const asset=fs.readFileSync(path.join(uiPublic,provenance.localAsset));
if(provenance.assetType==="png"){
expect(asset.subarray(0,8).toString("hex")).toBe("89504e470d0a1a0a");
expect(asset.readUInt32BE(16)).toBeGreaterThanOrEqual(128);
expect(asset.readUInt32BE(20)).toBeGreaterThanOrEqual(128);
}else{
const svg=asset.toString("utf8");
expect(svg).toMatch(/^<svg\b/);
expect(svg).not.toMatch(/<script|<foreignObject|\son[a-z]+\s*=/i);
}
if(provenance.darkAsset)expect(fs.existsSync(path.join(uiPublic,provenance.darkAsset))).toBe(true);
}
});
it("keeps every researched self-serve candidate implemented while blocked providers stay absent",()=>{
const definitions=new Map(CONNECTABLE_APP_DEFINITIONS.map((entry)=>[entry.slug,entry]));
for(const candidate of SELF_SERVE_MCP_CANDIDATES)expect(appSupportsCatalogSetup(definitions.get(candidate.slug))).toBe(true);
for(const blocked of BLOCKED_MCP_PROVIDERS)expect(definitions.has(blocked.slug)).toBe(false);
});
it("keeps all Google Workspace profiles aligned with their app, endpoint, scopes, ownership, risk, and write policy",()=>{
expect(GOOGLE_WORKSPACE_CONNECTOR_PROFILE_IDS).toEqual(GOOGLE_WORKSPACE_PROFILE_EXPECTATIONS.map((entry)=>entry.profile));
expect(Object.keys(GOOGLE_WORKSPACE_CONNECTOR_PROFILES)).toEqual([...GOOGLE_WORKSPACE_CONNECTOR_PROFILE_IDS]);
for(const expected of GOOGLE_WORKSPACE_PROFILE_EXPECTATIONS){
expect(GOOGLE_WORKSPACE_CONNECTOR_PROFILES[expected.profile],expected.profile).toEqual({
appSlug:expected.appSlug,
serverUrl:expected.serverUrl,
scopes:expected.scopes,
writeTools:expected.writeTools,
});
const app=APP_DEFINITIONS.find((candidate)=>candidate.slug===expected.appSlug);
const managed=app?.methods.find((method)=>method.connectorProfile===expected.profile);
expect(managed,expected.profile).toMatchObject({
auth:"oauth",
oauthStrategy:"paperclip_cloud_connector",
connectorProfile:expected.profile,
capabilityProfile:{key:expected.capability},
grantKinds:["user","organization"],
ownershipModes:["platform_shared"],
defaults:{serverUrl:expected.serverUrl,scopesHint:expected.scopes},
riskTier:expected.riskTier,
});
expect(managed?.riskTier,`${expected.profile}:write-risk`).toBe(expected.writeTools.length>0?"S4":"S3");
const customer=app?.methods.find((method)=>
method.auth==="oauth"
&&method.oauthStrategy===undefined
&&method.capabilityProfile?.key===expected.capability
);
expect(customer,`${expected.profile}:customer-fallback`).toMatchObject({
grantKinds:["user","organization"],
ownershipModes:["customer"],
defaults:{serverUrl:expected.serverUrl,scopesHint:expected.scopes},
riskTier:expected.riskTier,
});
}
});
it("configures Shopify's current UCP and compatibility MCP methods without OAuth",()=>{const shopify=APP_DEFINITIONS.find((app)=>app.slug==="shopify");expect(shopify?.methods.map((method)=>method.key)).toEqual(["ucp-commerce","storefront-mcp"]);const ucp=shopify?.methods[0];const compatibility=shopify?.methods[1];expect(ucp).toMatchObject({auth:"none",defaults:{serverUrlTemplate:"https://{storeDomain}/api/ucp/mcp",toolArgumentDefaults:{meta:{"ucp-agent":{profile:"https://shopify.dev/ucp/agent-profiles/examples/2026-04-08/valid-with-capabilities.json"}}}},tenantFields:[expect.objectContaining({key:"storeDomain",required:true})]});expect(compatibility).toMatchObject({auth:"none",defaults:{serverUrlTemplate:"https://{storeDomain}/api/mcp"}});expect(resolveConnectionMethodServerUrl(ucp!,{storeDomain:"paperclip-demo.myshopify.com"})).toBe("https://paperclip-demo.myshopify.com/api/ucp/mcp");expect(resolveConnectionMethodServerUrl(compatibility!,{storeDomain:"paperclip-demo.myshopify.com"})).toBe("https://paperclip-demo.myshopify.com/api/mcp");expect(resolveConnectionMethodServerUrl(ucp!,{})).toBeNull();expect(shopify?.setupPrerequisite).toMatchObject({title:"Launch the storefront before connecting",actionUrl:"https://admin.shopify.com/"});expect(shopify?.setupPrerequisite?.steps?.join(" ")).toContain("Storefront visibility to Public")});
it("offers PostHog OAuth and API-key methods with zero-config defaults and advanced narrowing",()=>{const posthog=APP_DEFINITIONS.find((app)=>app.slug==="posthog");expect(posthog?.methods.map((method)=>method.key)).toEqual(["mcp-oauth","mcp-api-key"]);for(const method of posthog?.methods??[]){const projectField=method.tenantFields?.find((field)=>field.key==="projectId");expect(method.riskTier).toBe("S3");expect(method.tenantFields?.find((field)=>field.key==="readOnly")).toMatchObject({defaultValue:false,advanced:true});expect(projectField).toMatchObject({advanced:true,transport:{location:"header",name:"x-posthog-project-id"}});expect(projectField?.required).not.toBe(true);expect(method.tenantFields?.filter((field)=>field.advanced).map((field)=>field.key)).toEqual(["projectId","readOnly","features","tools"]);expect(method.tenantFields?.find((field)=>field.key==="mode")).toMatchObject({hidden:true,defaultValue:"tools",transport:{location:"query",name:"mode"}});expect(method.configRequirements).toBeUndefined();expect(method.requiredResourceFilters).toBeUndefined();expect(method.guidanceMd).toContain("optional advanced controls")}});
it("requires only reviewed provider or safety-boundary configuration on the default path",()=>{const required=APP_DEFINITIONS.flatMap((app)=>app.methods.flatMap((method)=>[...(method.tenantFields??[]),...(method.extensionFields??[])].filter((field)=>field.required&&field.advanced!==true&&!field.hidden).map((field)=>`${app.slug}:${method.key}:${field.key}`))).sort();expect(required).toEqual(["clickhouse:mcp-oauth:serviceId","shopify:storefront-mcp:storeDomain","shopify:ucp-commerce:storeDomain","supabase:mcp-api-key:projectRef","supabase:mcp-oauth:projectRef"])});
it("limits Vercel Connect setup to the reviewed pilot methods",()=>{
const reviewed=APP_DEFINITIONS.flatMap((app)=>app.methods.flatMap((method)=>method.credentialSources?.vercelConnect?[{slug:app.slug,key:method.key,review:method.credentialSources.vercelConnect}]:[]));
expect(reviewed.map(({slug,key})=>`${slug}:${key}`).sort()).toEqual(["linear:mcp-oauth","notion:mcp-oauth","posthog:mcp-api-key","posthog:mcp-oauth"]);
expect(reviewed.find(({slug})=>slug==="linear")?.review).toMatchObject({services:["linear"],principalModes:["user"],scopes:["read","write"],header:{name:"Authorization",prefix:"Bearer "}});
expect(reviewed.find(({slug,key})=>slug==="posthog"&&key==="mcp-oauth")?.review.services).toEqual(["posthog","mcp.posthog.com/mcp"]);
expect(reviewed.find(({slug,key})=>slug==="posthog"&&key==="mcp-api-key")?.review.principalModes).toEqual(["app"]);
expect(APP_DEFINITIONS.find((app)=>app.slug==="vercel")?.availability?.available).toBe(false);
});
it("enforces method and field invariants",()=>{for(const app of APP_DEFINITIONS)for(const method of app.methods){if(method.auth==="api_key")expect(method.keyPlacement).toBeTruthy();if(method.auth==="oauth")expect(method.ownershipModes.length).toBeGreaterThan(0);for(const field of method.credentialFields??[])if(field.required&&field.type!=="checkbox")expect(field.placeholder).toBeTruthy()}});
});