@12-apps/mcp 3.15.0 → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-F3LMK6OL.js +25 -0
- package/dist/chunk-F3LMK6OL.js.map +1 -0
- package/dist/e2e/index.d.ts +109 -0
- package/dist/e2e/index.js +27 -0
- package/dist/e2e/index.js.map +1 -0
- package/dist/e2e/steps/journey.steps.d.ts +2 -0
- package/dist/e2e/steps/journey.steps.js +79 -0
- package/dist/e2e/steps/journey.steps.js.map +1 -0
- package/dist/{guide-KQNcXlMG.d.ts → guide-CrzdsdNf.d.ts} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{locales-eKE_OJw4.d.ts → locales-Cv0Pecvu.d.ts} +1 -1
- package/dist/manifest/index.d.ts +29 -7
- package/dist/manifest/index.js +2 -1
- package/dist/manifest/index.js.map +1 -1
- package/dist/oauth/index.d.ts +1 -1
- package/dist/react/index.d.ts +3 -3
- package/features/ai-connect.feature +46 -0
- package/package.json +22 -5
- package/src/e2e/globs.ts +70 -0
- package/src/e2e/index.ts +16 -0
- package/src/e2e/steps/journey.steps.ts +136 -0
- package/src/e2e/world.ts +84 -0
- package/src/manifest/index.ts +24 -7
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-7QVYU63E.js";
|
|
4
|
+
|
|
5
|
+
// src/e2e/world.ts
|
|
6
|
+
var installed = null;
|
|
7
|
+
function defineMcpConnectWorld(world) {
|
|
8
|
+
installed = world;
|
|
9
|
+
}
|
|
10
|
+
__name(defineMcpConnectWorld, "defineMcpConnectWorld");
|
|
11
|
+
function mcpConnectWorld() {
|
|
12
|
+
if (!installed) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"No mcp e2e world is installed. Call defineMcpConnectWorld({ \u2026 }) from a module inside your own `steps` glob \u2014 playwright-bdd imports those before any scenario runs, which is what makes the registration land in every worker."
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return installed;
|
|
18
|
+
}
|
|
19
|
+
__name(mcpConnectWorld, "mcpConnectWorld");
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
defineMcpConnectWorld,
|
|
23
|
+
mcpConnectWorld
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=chunk-F3LMK6OL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/e2e/world.ts"],"sourcesContent":["import type { Page } from '@playwright/test';\n\n/**\n * The port a HOST implements to run the packaged AI-connect journeys.\n *\n * These journeys were written because the walkthrough they cover was tested\n * NOWHERE. `./react` ships the whole flow — the landing, the assistant picker,\n * the endpoint to copy, the configure and connect steps, the confirmation and\n * its re-test — and twenty test ids go with it. Not one of them appeared in\n * this package's own suite, and not one appeared in the origin host's specs\n * either: that host's `ai.e2e.ts` drives its OWN plan lock and upsell modal,\n * reaching only `ai-onboarding` and the status board on the way past.\n *\n * So the flow a store owner actually walks was covered by nothing, in either\n * repo. That is the gap the `e2e` capability exists to convert into a\n * declaration rather than an omission nobody can see.\n *\n * What stays the host's: how it signs an owner in, where it mounts the flow,\n * and which assistant its `hosts` config offers — the guides are REQUIRED\n * config (FUT-760), so the package has no assistant of its own to name.\n */\n\n/** Facts about the host that the assertions have to name. */\nexport interface McpConnectFixtures {\n /**\n * The id of an assistant the host offers whose guide has NO `pluginUrl`.\n *\n * The flow BRANCHES on that field: a guide carrying one gets `InstallStep`\n * (a single \"open the install link\" button), and a guide without one gets\n * the three-step manual path — copy the endpoint, configure, connect. These\n * journeys walk the MANUAL path, so the host has to point at a guide that\n * takes it. Naming the branch here rather than guessing keeps the scenario\n * from failing in a host whose first assistant happens to ship a plugin.\n */\n manualHostId: string;\n /** The endpoint URL that host serves, as it is rendered for copying. */\n endpointUrl: string;\n}\n\n/** What a host must be able to do for these journeys to run in it. */\nexport interface McpConnectWorld {\n /**\n * Put the browser in a known signed-in state as somebody who may connect an\n * assistant, with the flow's progress RESET to its first run.\n *\n * The reset is load-bearing rather than hygiene: the wizard persists its step\n * through `@12-apps/onboarding`, so a scenario that advanced it would hand\n * the next one a flow resuming from the middle — and the landing step, which\n * two of these scenarios assert, would never render.\n */\n signInAsOwner(page: Page): Promise<void>;\n /** Land on the screen that mounts `AiIntegrationOnboarding`. */\n openAiIntegrationScreen(page: Page): Promise<void>;\n fixtures: McpConnectFixtures;\n}\n\nlet installed: McpConnectWorld | null = null;\n\n/**\n * Install the host's implementation. Call this from a module inside the host's\n * OWN steps glob — playwright-bdd imports every step file before any scenario\n * runs, so a top-level call there is registered in time, in every worker.\n */\nexport function defineMcpConnectWorld(world: McpConnectWorld): void {\n installed = world;\n}\n\n/**\n * The installed world, or a refusal naming the fix.\n *\n * Throws rather than returning null: a step that ran against an absent world\n * would fail on whatever it touched next, somewhere unrelated to the actual\n * mistake, which is a diagnosis nobody should have to make twice.\n */\nexport function mcpConnectWorld(): McpConnectWorld {\n if (!installed) {\n throw new Error(\n 'No mcp e2e world is installed. Call defineMcpConnectWorld({ … }) from a module inside ' +\n \"your own `steps` glob — playwright-bdd imports those before any scenario runs, \" +\n 'which is what makes the registration land in every worker.',\n );\n }\n return installed;\n}\n"],"mappings":";;;;;AAwDA,IAAI,YAAoC;AAOjC,SAAS,sBAAsB,OAA8B;AAClE,cAAY;AACd;AAFgB;AAWT,SAAS,kBAAmC;AACjD,MAAI,CAAC,WAAW;AACd,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACA,SAAO;AACT;AATgB;","names":[]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/** Every packaged `.feature`, for `defineBddConfig({ features })`. */
|
|
4
|
+
declare const mcpFeatures: string;
|
|
5
|
+
/**
|
|
6
|
+
* The base `defineBddConfig({ featuresRoot })` must be given, and it is NOT
|
|
7
|
+
* optional decoration.
|
|
8
|
+
*
|
|
9
|
+
* bddgen mirrors each feature's path RELATIVE TO `featuresRoot` under
|
|
10
|
+
* `outputDir`. Left unset it defaults to the config's own directory, so a
|
|
11
|
+
* feature living under `node_modules/...` compiles to a path Playwright then
|
|
12
|
+
* IGNORES, because its default `testIgnore` excludes `**\/node_modules/**`.
|
|
13
|
+
*
|
|
14
|
+
* The result is the worst kind of green: bddgen reports the features compiled,
|
|
15
|
+
* Playwright collects zero specs from them, and the run passes with the whole
|
|
16
|
+
* packaged suite silently absent.
|
|
17
|
+
*
|
|
18
|
+
* A host running SEVERAL packaged suites gives `featuresRoot` the directory they
|
|
19
|
+
* all sit under, since bddgen takes exactly one — and that is safe to do,
|
|
20
|
+
* because a feature outside it is a hard exit from bddgen, never a quiet
|
|
21
|
+
* omission.
|
|
22
|
+
*/
|
|
23
|
+
declare const mcpFeaturesRoot: string;
|
|
24
|
+
/**
|
|
25
|
+
* Every packaged step definition, for `defineBddConfig({ steps })`.
|
|
26
|
+
*
|
|
27
|
+
* COMPILED JavaScript, and that is the reason this package has a build step at
|
|
28
|
+
* all while everything else it exports is raw `.ts` through the `exports` map.
|
|
29
|
+
* Those entries are consumed by an application's BUNDLER, which transpiles
|
|
30
|
+
* whatever it is pointed at. These are loaded by NODE — `playwright.config.ts`
|
|
31
|
+
* imports this module, and bddgen imports the step files — and Node refuses to
|
|
32
|
+
* strip types from anything under `node_modules`
|
|
33
|
+
* (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). Playwright's own TS transform
|
|
34
|
+
* does not rescue them either; it skips `node_modules` by design.
|
|
35
|
+
*
|
|
36
|
+
* The host's own steps glob must ALSO be listed — that is where its
|
|
37
|
+
* `defineMcpConnectWorld` call lives, and playwright-bdd imports every step
|
|
38
|
+
* file before any scenario runs, which is what makes the registration land in
|
|
39
|
+
* time in every worker.
|
|
40
|
+
*/
|
|
41
|
+
declare const mcpSteps: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The port a HOST implements to run the packaged AI-connect journeys.
|
|
45
|
+
*
|
|
46
|
+
* These journeys were written because the walkthrough they cover was tested
|
|
47
|
+
* NOWHERE. `./react` ships the whole flow — the landing, the assistant picker,
|
|
48
|
+
* the endpoint to copy, the configure and connect steps, the confirmation and
|
|
49
|
+
* its re-test — and twenty test ids go with it. Not one of them appeared in
|
|
50
|
+
* this package's own suite, and not one appeared in the origin host's specs
|
|
51
|
+
* either: that host's `ai.e2e.ts` drives its OWN plan lock and upsell modal,
|
|
52
|
+
* reaching only `ai-onboarding` and the status board on the way past.
|
|
53
|
+
*
|
|
54
|
+
* So the flow a store owner actually walks was covered by nothing, in either
|
|
55
|
+
* repo. That is the gap the `e2e` capability exists to convert into a
|
|
56
|
+
* declaration rather than an omission nobody can see.
|
|
57
|
+
*
|
|
58
|
+
* What stays the host's: how it signs an owner in, where it mounts the flow,
|
|
59
|
+
* and which assistant its `hosts` config offers — the guides are REQUIRED
|
|
60
|
+
* config (FUT-760), so the package has no assistant of its own to name.
|
|
61
|
+
*/
|
|
62
|
+
/** Facts about the host that the assertions have to name. */
|
|
63
|
+
interface McpConnectFixtures {
|
|
64
|
+
/**
|
|
65
|
+
* The id of an assistant the host offers whose guide has NO `pluginUrl`.
|
|
66
|
+
*
|
|
67
|
+
* The flow BRANCHES on that field: a guide carrying one gets `InstallStep`
|
|
68
|
+
* (a single "open the install link" button), and a guide without one gets
|
|
69
|
+
* the three-step manual path — copy the endpoint, configure, connect. These
|
|
70
|
+
* journeys walk the MANUAL path, so the host has to point at a guide that
|
|
71
|
+
* takes it. Naming the branch here rather than guessing keeps the scenario
|
|
72
|
+
* from failing in a host whose first assistant happens to ship a plugin.
|
|
73
|
+
*/
|
|
74
|
+
manualHostId: string;
|
|
75
|
+
/** The endpoint URL that host serves, as it is rendered for copying. */
|
|
76
|
+
endpointUrl: string;
|
|
77
|
+
}
|
|
78
|
+
/** What a host must be able to do for these journeys to run in it. */
|
|
79
|
+
interface McpConnectWorld {
|
|
80
|
+
/**
|
|
81
|
+
* Put the browser in a known signed-in state as somebody who may connect an
|
|
82
|
+
* assistant, with the flow's progress RESET to its first run.
|
|
83
|
+
*
|
|
84
|
+
* The reset is load-bearing rather than hygiene: the wizard persists its step
|
|
85
|
+
* through `@12-apps/onboarding`, so a scenario that advanced it would hand
|
|
86
|
+
* the next one a flow resuming from the middle — and the landing step, which
|
|
87
|
+
* two of these scenarios assert, would never render.
|
|
88
|
+
*/
|
|
89
|
+
signInAsOwner(page: Page): Promise<void>;
|
|
90
|
+
/** Land on the screen that mounts `AiIntegrationOnboarding`. */
|
|
91
|
+
openAiIntegrationScreen(page: Page): Promise<void>;
|
|
92
|
+
fixtures: McpConnectFixtures;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Install the host's implementation. Call this from a module inside the host's
|
|
96
|
+
* OWN steps glob — playwright-bdd imports every step file before any scenario
|
|
97
|
+
* runs, so a top-level call there is registered in time, in every worker.
|
|
98
|
+
*/
|
|
99
|
+
declare function defineMcpConnectWorld(world: McpConnectWorld): void;
|
|
100
|
+
/**
|
|
101
|
+
* The installed world, or a refusal naming the fix.
|
|
102
|
+
*
|
|
103
|
+
* Throws rather than returning null: a step that ran against an absent world
|
|
104
|
+
* would fail on whatever it touched next, somewhere unrelated to the actual
|
|
105
|
+
* mistake, which is a diagnosis nobody should have to make twice.
|
|
106
|
+
*/
|
|
107
|
+
declare function mcpConnectWorld(): McpConnectWorld;
|
|
108
|
+
|
|
109
|
+
export { type McpConnectFixtures, type McpConnectWorld, defineMcpConnectWorld, mcpConnectWorld, mcpFeatures, mcpFeaturesRoot, mcpSteps };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineMcpConnectWorld,
|
|
3
|
+
mcpConnectWorld
|
|
4
|
+
} from "../chunk-F3LMK6OL.js";
|
|
5
|
+
import {
|
|
6
|
+
__name
|
|
7
|
+
} from "../chunk-7QVYU63E.js";
|
|
8
|
+
|
|
9
|
+
// src/e2e/globs.ts
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
var require_ = createRequire(import.meta.url);
|
|
13
|
+
function packageRoot() {
|
|
14
|
+
return dirname(require_.resolve("@12-apps/mcp/package.json"));
|
|
15
|
+
}
|
|
16
|
+
__name(packageRoot, "packageRoot");
|
|
17
|
+
var mcpFeatures = join(packageRoot(), "features/**/*.feature");
|
|
18
|
+
var mcpFeaturesRoot = join(packageRoot(), "features");
|
|
19
|
+
var mcpSteps = join(packageRoot(), "dist/e2e/steps/**/*.js");
|
|
20
|
+
export {
|
|
21
|
+
defineMcpConnectWorld,
|
|
22
|
+
mcpConnectWorld,
|
|
23
|
+
mcpFeatures,
|
|
24
|
+
mcpFeaturesRoot,
|
|
25
|
+
mcpSteps
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/e2e/globs.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\n\n/**\n * Where this package's journeys and their steps live, ON DISK, in the consumer's\n * `node_modules`.\n *\n * `defineBddConfig` takes filesystem globs, not module specifiers — it hands\n * them to a glob matcher, so `'@12-apps/mcp/features/**'` matches\n * nothing and, worse, matches nothing SILENTLY: bddgen compiles the features it\n * found, finds none, and the run is green with zero journeys.\n *\n * So the paths are RESOLVED here instead of written down by every consumer. A\n * host that hard-codes `node_modules/@12-apps/mcp/...` is broken by\n * pnpm's nested store, by a workspace link, and by this package's own layout\n * changing; resolving from the package's own entry point is correct under all\n * three.\n */\nconst require_ = createRequire(import.meta.url);\n\n/**\n * The package root, found from a file this package definitely exports.\n * `./package.json` is exported precisely so this lookup needs no guess about\n * directory depth.\n */\nfunction packageRoot(): string {\n return dirname(require_.resolve('@12-apps/mcp/package.json'));\n}\n\n/** Every packaged `.feature`, for `defineBddConfig({ features })`. */\nexport const mcpFeatures: string = join(packageRoot(), 'features/**/*.feature');\n\n/**\n * The base `defineBddConfig({ featuresRoot })` must be given, and it is NOT\n * optional decoration.\n *\n * bddgen mirrors each feature's path RELATIVE TO `featuresRoot` under\n * `outputDir`. Left unset it defaults to the config's own directory, so a\n * feature living under `node_modules/...` compiles to a path Playwright then\n * IGNORES, because its default `testIgnore` excludes `**\\/node_modules/**`.\n *\n * The result is the worst kind of green: bddgen reports the features compiled,\n * Playwright collects zero specs from them, and the run passes with the whole\n * packaged suite silently absent.\n *\n * A host running SEVERAL packaged suites gives `featuresRoot` the directory they\n * all sit under, since bddgen takes exactly one — and that is safe to do,\n * because a feature outside it is a hard exit from bddgen, never a quiet\n * omission.\n */\nexport const mcpFeaturesRoot: string = join(packageRoot(), 'features');\n\n/**\n * Every packaged step definition, for `defineBddConfig({ steps })`.\n *\n * COMPILED JavaScript, and that is the reason this package has a build step at\n * all while everything else it exports is raw `.ts` through the `exports` map.\n * Those entries are consumed by an application's BUNDLER, which transpiles\n * whatever it is pointed at. These are loaded by NODE — `playwright.config.ts`\n * imports this module, and bddgen imports the step files — and Node refuses to\n * strip types from anything under `node_modules`\n * (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). Playwright's own TS transform\n * does not rescue them either; it skips `node_modules` by design.\n *\n * The host's own steps glob must ALSO be listed — that is where its\n * `defineMcpConnectWorld` call lives, and playwright-bdd imports every step\n * file before any scenario runs, which is what makes the registration land in\n * time in every worker.\n */\nexport const mcpSteps: string = join(packageRoot(), 'dist/e2e/steps/**/*.js');\n"],"mappings":";;;;;;;;;AAAA,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAiB9B,IAAM,WAAW,cAAc,YAAY,GAAG;AAO9C,SAAS,cAAsB;AAC7B,SAAO,QAAQ,SAAS,QAAQ,2BAA2B,CAAC;AAC9D;AAFS;AAKF,IAAM,cAAsB,KAAK,YAAY,GAAG,uBAAuB;AAoBvE,IAAM,kBAA0B,KAAK,YAAY,GAAG,UAAU;AAmB9D,IAAM,WAAmB,KAAK,YAAY,GAAG,wBAAwB;","names":[]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mcpConnectWorld
|
|
3
|
+
} from "../../chunk-F3LMK6OL.js";
|
|
4
|
+
import {
|
|
5
|
+
__name
|
|
6
|
+
} from "../../chunk-7QVYU63E.js";
|
|
7
|
+
|
|
8
|
+
// src/e2e/steps/journey.steps.ts
|
|
9
|
+
import { expect } from "@playwright/test";
|
|
10
|
+
import { createBdd } from "playwright-bdd";
|
|
11
|
+
var { Given, When, Then } = createBdd();
|
|
12
|
+
Given("I am signed in as somebody who may connect an assistant", async ({ page }) => {
|
|
13
|
+
await mcpConnectWorld().signInAsOwner(page);
|
|
14
|
+
});
|
|
15
|
+
When("I open the AI integration screen", async ({ page }) => {
|
|
16
|
+
await mcpConnectWorld().openAiIntegrationScreen(page);
|
|
17
|
+
await expect(page.getByTestId("ai-onboarding")).toBeVisible();
|
|
18
|
+
});
|
|
19
|
+
Then("the landing explains the permission model before anything is connected", async ({ page }) => {
|
|
20
|
+
await expect(page.getByTestId("ai-landing")).toBeVisible();
|
|
21
|
+
await expect(page.getByTestId("ai-permission-callout")).toBeVisible();
|
|
22
|
+
});
|
|
23
|
+
Then("it shows examples of what an assistant can be asked", async ({ page }) => {
|
|
24
|
+
await expect(page.getByTestId("ai-capability-examples")).toBeVisible();
|
|
25
|
+
});
|
|
26
|
+
When("I start the walkthrough", async ({ page }) => {
|
|
27
|
+
await page.getByTestId("ai-landing-start").click();
|
|
28
|
+
await expect(page.getByTestId("ai-host-select")).toBeVisible();
|
|
29
|
+
});
|
|
30
|
+
When("I choose an assistant that has no one-click install", async ({ page }) => {
|
|
31
|
+
await page.getByTestId(`ai-host-card-${mcpConnectWorld().fixtures.manualHostId}`).click();
|
|
32
|
+
});
|
|
33
|
+
Then("I am asked to copy the store's endpoint", async ({ page }) => {
|
|
34
|
+
await expect(page.getByTestId("ai-copy-step")).toBeVisible();
|
|
35
|
+
await expect(page.getByTestId("mcp-endpoint-url")).toContainText(
|
|
36
|
+
mcpConnectWorld().fixtures.endpointUrl
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
async function copyEndpoint(page) {
|
|
40
|
+
await page.getByTestId("ai-copy-endpoint").click();
|
|
41
|
+
}
|
|
42
|
+
__name(copyEndpoint, "copyEndpoint");
|
|
43
|
+
async function openConnectorPage(page) {
|
|
44
|
+
await page.getByTestId(`ai-host-link-${mcpConnectWorld().fixtures.manualHostId}`).click();
|
|
45
|
+
}
|
|
46
|
+
__name(openConnectorPage, "openConnectorPage");
|
|
47
|
+
When("I copy the endpoint", async ({ page }) => {
|
|
48
|
+
await copyEndpoint(page);
|
|
49
|
+
});
|
|
50
|
+
Then("the walkthrough has moved on to configuring the connector", async ({ page }) => {
|
|
51
|
+
await expect(page.getByTestId("ai-configure-step")).toBeVisible();
|
|
52
|
+
await expect(page.getByTestId("ai-copy-step")).toHaveCount(0);
|
|
53
|
+
});
|
|
54
|
+
Then("continuing is refused until I open the connector page", async ({ page }) => {
|
|
55
|
+
await expect(page.getByTestId("ai-configure-next")).toBeDisabled();
|
|
56
|
+
});
|
|
57
|
+
Then("once opened, continuing reaches the connect step", async ({ page }) => {
|
|
58
|
+
await openConnectorPage(page);
|
|
59
|
+
await expect(page.getByTestId("ai-configure-next")).toBeEnabled();
|
|
60
|
+
await page.getByTestId("ai-configure-next").click();
|
|
61
|
+
await expect(page.getByTestId("ai-connect-step")).toBeVisible();
|
|
62
|
+
});
|
|
63
|
+
When("I go back a step", async ({ page }) => {
|
|
64
|
+
await page.getByTestId("ai-step-back").click();
|
|
65
|
+
});
|
|
66
|
+
When("I work through configuring and connecting", async ({ page }) => {
|
|
67
|
+
await openConnectorPage(page);
|
|
68
|
+
await page.getByTestId("ai-configure-next").click();
|
|
69
|
+
await expect(page.getByTestId("ai-connect-step")).toBeVisible();
|
|
70
|
+
await page.getByTestId("ai-connect-done").click();
|
|
71
|
+
});
|
|
72
|
+
Then("the confirmation is still waiting for the assistant", async ({ page }) => {
|
|
73
|
+
await expect(page.getByTestId("ai-confirm-waiting")).toBeVisible();
|
|
74
|
+
await expect(page.getByTestId("ai-confirm-connected")).toHaveCount(0);
|
|
75
|
+
});
|
|
76
|
+
Then("it offers to test the connection again", async ({ page }) => {
|
|
77
|
+
await expect(page.getByTestId("ai-confirm-retest")).toBeVisible();
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=journey.steps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/e2e/steps/journey.steps.ts"],"sourcesContent":["import { expect, type Page } from '@playwright/test';\nimport { createBdd } from 'playwright-bdd';\n\nimport { mcpConnectWorld } from '../world.js';\n\n/**\n * The packaged AI-connect journeys' step definitions.\n *\n * Every locator is a test id THIS package's own components render —\n * `ai-landing`, `ai-host-select`, `ai-copy-step`, `ai-configure-step`,\n * `ai-connect-step`, `ai-confirm-waiting`, `mcp-endpoint-url` — which is what\n * makes the scenarios portable: they mean the same thing in any app mounting\n * `AiIntegrationOnboarding`. Everything a host owns (sign-in, where the flow is\n * mounted, which assistants it offers) arrives through the world port.\n *\n * ## Why `package.json` declares this file under `sideEffects`\n *\n * Every Given/When/Then below runs at IMPORT time — registering a step is the\n * whole point of the module, and it exports nothing anybody imports by name. To\n * a bundler doing tree-shaking that reads as dead weight, and dropping it is\n * licensed: the result is a suite where bddgen reports the features compiled\n * and every scenario then fails on an undefined step.\n *\n * Not one step asserts a SENTENCE. The assistants' names, the button words and\n * the permission prose are REQUIRED host config (FUT-760) — this package ships\n * no assistant and no copy of its own — so a spec written against them could\n * only ever have run in one adopter.\n */\nconst { Given, When, Then } = createBdd();\n\nGiven('I am signed in as somebody who may connect an assistant', async ({ page }) => {\n await mcpConnectWorld().signInAsOwner(page);\n});\n\nWhen('I open the AI integration screen', async ({ page }) => {\n await mcpConnectWorld().openAiIntegrationScreen(page);\n await expect(page.getByTestId('ai-onboarding')).toBeVisible();\n});\n\nThen('the landing explains the permission model before anything is connected', async ({ page }) => {\n await expect(page.getByTestId('ai-landing')).toBeVisible();\n // The callout is the point of the landing: an owner is about to hand a third\n // party a key to their store, and this is the screen that says what the key\n // opens. It renders BEFORE any assistant is chosen, which is the only moment\n // the answer is still \"nothing\".\n await expect(page.getByTestId('ai-permission-callout')).toBeVisible();\n});\n\nThen('it shows examples of what an assistant can be asked', async ({ page }) => {\n await expect(page.getByTestId('ai-capability-examples')).toBeVisible();\n});\n\nWhen('I start the walkthrough', async ({ page }) => {\n await page.getByTestId('ai-landing-start').click();\n await expect(page.getByTestId('ai-host-select')).toBeVisible();\n});\n\nWhen('I choose an assistant that has no one-click install', async ({ page }) => {\n // The flow BRANCHES on the guide's `pluginUrl`: with one it becomes\n // Escolher → Instalar → Confirmar and there is no endpoint to copy at all.\n // The host names a guide that takes the manual path, because which\n // assistants exist is its configuration and not this package's.\n await page.getByTestId(`ai-host-card-${mcpConnectWorld().fixtures.manualHostId}`).click();\n});\n\nThen(\"I am asked to copy the store's endpoint\", async ({ page }) => {\n await expect(page.getByTestId('ai-copy-step')).toBeVisible();\n await expect(page.getByTestId('mcp-endpoint-url')).toContainText(\n mcpConnectWorld().fixtures.endpointUrl,\n );\n});\n\n/** Copying is the ACTION that advances this step — there is no next button. */\nasync function copyEndpoint(page: Page): Promise<void> {\n await page.getByTestId('ai-copy-endpoint').click();\n}\n\n/**\n * Open the assistant's connector page — the one action that unlocks \"next\".\n *\n * By its OWN id, not `getByRole('link')`: this control is a Button calling\n * `window.open`, and the only real `<a>` in the step is the DOCS link, which\n * deliberately does not unlock anything. A role query would have taken the\n * docs link and then failed on an assertion about the button.\n */\nasync function openConnectorPage(page: Page): Promise<void> {\n await page.getByTestId(`ai-host-link-${mcpConnectWorld().fixtures.manualHostId}`).click();\n}\n\nWhen('I copy the endpoint', async ({ page }) => {\n await copyEndpoint(page);\n});\n\nThen('the walkthrough has moved on to configuring the connector', async ({ page }) => {\n // The claim is that COPYING advanced it. The copy step offers no \"next\", so\n // reaching the configure step at all proves the copy handler drove the\n // wizard rather than some button the operator happened to press.\n await expect(page.getByTestId('ai-configure-step')).toBeVisible();\n await expect(page.getByTestId('ai-copy-step')).toHaveCount(0);\n});\n\nThen('continuing is refused until I open the connector page', async ({ page }) => {\n // The guard that stops an owner walking past the one step that does the\n // actual work: the connector page has to be opened before \"next\" unlocks.\n await expect(page.getByTestId('ai-configure-next')).toBeDisabled();\n});\n\nThen('once opened, continuing reaches the connect step', async ({ page }) => {\n await openConnectorPage(page);\n await expect(page.getByTestId('ai-configure-next')).toBeEnabled();\n await page.getByTestId('ai-configure-next').click();\n await expect(page.getByTestId('ai-connect-step')).toBeVisible();\n});\n\nWhen('I go back a step', async ({ page }) => {\n await page.getByTestId('ai-step-back').click();\n});\n\nWhen('I work through configuring and connecting', async ({ page }) => {\n await openConnectorPage(page);\n await page.getByTestId('ai-configure-next').click();\n await expect(page.getByTestId('ai-connect-step')).toBeVisible();\n await page.getByTestId('ai-connect-done').click();\n});\n\nThen('the confirmation is still waiting for the assistant', async ({ page }) => {\n // Waiting rather than connected, because nothing has actually connected: the\n // wizard reaching its last step is not evidence of a live connection, and\n // this is the step that refuses to claim otherwise.\n await expect(page.getByTestId('ai-confirm-waiting')).toBeVisible();\n await expect(page.getByTestId('ai-confirm-connected')).toHaveCount(0);\n});\n\nThen('it offers to test the connection again', async ({ page }) => {\n await expect(page.getByTestId('ai-confirm-retest')).toBeVisible();\n});\n"],"mappings":";;;;;;;;AAAA,SAAS,cAAyB;AAClC,SAAS,iBAAiB;AA2B1B,IAAM,EAAE,OAAO,MAAM,KAAK,IAAI,UAAU;AAExC,MAAM,2DAA2D,OAAO,EAAE,KAAK,MAAM;AACnF,QAAM,gBAAgB,EAAE,cAAc,IAAI;AAC5C,CAAC;AAED,KAAK,oCAAoC,OAAO,EAAE,KAAK,MAAM;AAC3D,QAAM,gBAAgB,EAAE,wBAAwB,IAAI;AACpD,QAAM,OAAO,KAAK,YAAY,eAAe,CAAC,EAAE,YAAY;AAC9D,CAAC;AAED,KAAK,0EAA0E,OAAO,EAAE,KAAK,MAAM;AACjG,QAAM,OAAO,KAAK,YAAY,YAAY,CAAC,EAAE,YAAY;AAKzD,QAAM,OAAO,KAAK,YAAY,uBAAuB,CAAC,EAAE,YAAY;AACtE,CAAC;AAED,KAAK,uDAAuD,OAAO,EAAE,KAAK,MAAM;AAC9E,QAAM,OAAO,KAAK,YAAY,wBAAwB,CAAC,EAAE,YAAY;AACvE,CAAC;AAED,KAAK,2BAA2B,OAAO,EAAE,KAAK,MAAM;AAClD,QAAM,KAAK,YAAY,kBAAkB,EAAE,MAAM;AACjD,QAAM,OAAO,KAAK,YAAY,gBAAgB,CAAC,EAAE,YAAY;AAC/D,CAAC;AAED,KAAK,uDAAuD,OAAO,EAAE,KAAK,MAAM;AAK9E,QAAM,KAAK,YAAY,gBAAgB,gBAAgB,EAAE,SAAS,YAAY,EAAE,EAAE,MAAM;AAC1F,CAAC;AAED,KAAK,2CAA2C,OAAO,EAAE,KAAK,MAAM;AAClE,QAAM,OAAO,KAAK,YAAY,cAAc,CAAC,EAAE,YAAY;AAC3D,QAAM,OAAO,KAAK,YAAY,kBAAkB,CAAC,EAAE;AAAA,IACjD,gBAAgB,EAAE,SAAS;AAAA,EAC7B;AACF,CAAC;AAGD,eAAe,aAAa,MAA2B;AACrD,QAAM,KAAK,YAAY,kBAAkB,EAAE,MAAM;AACnD;AAFe;AAYf,eAAe,kBAAkB,MAA2B;AAC1D,QAAM,KAAK,YAAY,gBAAgB,gBAAgB,EAAE,SAAS,YAAY,EAAE,EAAE,MAAM;AAC1F;AAFe;AAIf,KAAK,uBAAuB,OAAO,EAAE,KAAK,MAAM;AAC9C,QAAM,aAAa,IAAI;AACzB,CAAC;AAED,KAAK,6DAA6D,OAAO,EAAE,KAAK,MAAM;AAIpF,QAAM,OAAO,KAAK,YAAY,mBAAmB,CAAC,EAAE,YAAY;AAChE,QAAM,OAAO,KAAK,YAAY,cAAc,CAAC,EAAE,YAAY,CAAC;AAC9D,CAAC;AAED,KAAK,yDAAyD,OAAO,EAAE,KAAK,MAAM;AAGhF,QAAM,OAAO,KAAK,YAAY,mBAAmB,CAAC,EAAE,aAAa;AACnE,CAAC;AAED,KAAK,oDAAoD,OAAO,EAAE,KAAK,MAAM;AAC3E,QAAM,kBAAkB,IAAI;AAC5B,QAAM,OAAO,KAAK,YAAY,mBAAmB,CAAC,EAAE,YAAY;AAChE,QAAM,KAAK,YAAY,mBAAmB,EAAE,MAAM;AAClD,QAAM,OAAO,KAAK,YAAY,iBAAiB,CAAC,EAAE,YAAY;AAChE,CAAC;AAED,KAAK,oBAAoB,OAAO,EAAE,KAAK,MAAM;AAC3C,QAAM,KAAK,YAAY,cAAc,EAAE,MAAM;AAC/C,CAAC;AAED,KAAK,6CAA6C,OAAO,EAAE,KAAK,MAAM;AACpE,QAAM,kBAAkB,IAAI;AAC5B,QAAM,KAAK,YAAY,mBAAmB,EAAE,MAAM;AAClD,QAAM,OAAO,KAAK,YAAY,iBAAiB,CAAC,EAAE,YAAY;AAC9D,QAAM,KAAK,YAAY,iBAAiB,EAAE,MAAM;AAClD,CAAC;AAED,KAAK,uDAAuD,OAAO,EAAE,KAAK,MAAM;AAI9E,QAAM,OAAO,KAAK,YAAY,oBAAoB,CAAC,EAAE,YAAY;AACjE,QAAM,OAAO,KAAK,YAAY,sBAAsB,CAAC,EAAE,YAAY,CAAC;AACtE,CAAC;AAED,KAAK,0CAA0C,OAAO,EAAE,KAAK,MAAM;AACjE,QAAM,OAAO,KAAK,YAAY,mBAAmB,CAAC,EAAE,YAAY;AAClE,CAAC;","names":[]}
|
|
@@ -147,4 +147,4 @@ interface AiConnectPromptCopy {
|
|
|
147
147
|
*/
|
|
148
148
|
declare function aiConnectPrompt(spec: AiConnectPromptSpec, copy: AiConnectPromptCopy): string;
|
|
149
149
|
|
|
150
|
-
export { type
|
|
150
|
+
export { type AiConnectPromptSpec as A, type AiHostGuide as a, type AiCapability as b, type AiPermissionModel as c, type AiProvider as d, type AiConnectPromptCopy as e, type AiHostBrand as f, type AiHostConfigureStage as g, type AiHostLink as h, aiConnectPrompt as i, providerForHostId as p };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { J as JsonSchema, a as ToolAnnotations, G as GeneratedTool, D as DispatchConfig, b as DispatchResult, R as RequestAuth, T as ToolManifest } from './generate-BCDqBUjZ.js';
|
|
2
2
|
export { A as AuthResolver, c as GenerateOptions, O as OpenApiDocument, d as OpenApiOperation, e as OpenApiParameter, f as OpenApiRequestBody, g as OpenApiResponse, P as ParameterLocation, h as ToolParameter, i as generateTools } from './generate-BCDqBUjZ.js';
|
|
3
|
-
export {
|
|
3
|
+
export { b as AiCapability, e as AiConnectPromptCopy, A as AiConnectPromptSpec, f as AiHostBrand, g as AiHostConfigureStage, a as AiHostGuide, h as AiHostLink, d as AiProvider, i as aiConnectPrompt, p as providerForHostId } from './guide-CrzdsdNf.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
export { A as AI_CAPABILITIES, a as AI_CONNECT_PROMPT, b as AI_HOST_GUIDES, c as AI_PERMISSION_MODEL, E as EN_US_AI_CAPABILITIES, d as EN_US_AI_CONNECT_PROMPT, e as EN_US_AI_HOST_GUIDES, f as EN_US_AI_PERMISSION_MODEL, P as PT_BR_AI_CAPABILITIES, g as PT_BR_AI_CONNECT_PROMPT, h as PT_BR_AI_HOST_GUIDES, i as PT_BR_AI_PERMISSION_MODEL } from './locales-
|
|
5
|
+
export { A as AI_CAPABILITIES, a as AI_CONNECT_PROMPT, b as AI_HOST_GUIDES, c as AI_PERMISSION_MODEL, E as EN_US_AI_CAPABILITIES, d as EN_US_AI_CONNECT_PROMPT, e as EN_US_AI_HOST_GUIDES, f as EN_US_AI_PERMISSION_MODEL, P as PT_BR_AI_CAPABILITIES, g as PT_BR_AI_CONNECT_PROMPT, h as PT_BR_AI_HOST_GUIDES, i as PT_BR_AI_PERMISSION_MODEL } from './locales-Cv0Pecvu.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Raised when a JSON Schema cannot be turned into a flat, self-contained tool
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as AiConnectPromptSpec, a as AiHostGuide, b as AiCapability, c as AiPermissionModel } from './guide-CrzdsdNf.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The AI hosts a store owner can connect, in recommended order. Same OAuth flow
|
package/dist/manifest/index.d.ts
CHANGED
|
@@ -53,13 +53,29 @@
|
|
|
53
53
|
* three hosts pass differently, which is the opposite of what a surface
|
|
54
54
|
* contribution is for. When the flow grows a real bound surface, the
|
|
55
55
|
* inventory grows with it.
|
|
56
|
-
* - **No `env`.**
|
|
57
|
-
* `
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
56
|
+
* - **No `env`.** Three helpers here DO read `process.env`
|
|
57
|
+
* (`trustedOriginsFromEnv`, and `loadSigningKeyFromEnv` for the key and its
|
|
58
|
+
* id) — this used to say "the package reads nothing itself", which was
|
|
59
|
+
* simply untrue and is the kind of sentence that makes a narrowing unfalsifiable.
|
|
60
|
+
* The accurate reason is narrower and still holds: nothing is read at import
|
|
61
|
+
* time or unconditionally, so a host that never calls these helpers has no
|
|
62
|
+
* environment dependency on this package at all; and where they ARE called,
|
|
63
|
+
* the variable's NAME is the caller's — passed as an argument by
|
|
64
|
+
* `trustedOriginsFromEnv`, and defaulted but overridable per call by
|
|
65
|
+
* `loadSigningKeyFromEnv`. `env` declares variables a host must answer for.
|
|
66
|
+
* Declaring these would oblige every host to answer for names it may
|
|
67
|
+
* legitimately have spelled differently, or never reads at all.
|
|
68
|
+
* - **`e2e` IS declared** (`../e2e`). It was narrowed away with "this package
|
|
69
|
+
* packages no journeys", which was true as a statement of fact and circular
|
|
70
|
+
* as an argument: it packaged none because nobody had written any, while
|
|
71
|
+
* `./react` shipped the entire AI-connect walkthrough — landing, assistant
|
|
72
|
+
* picker, endpoint copy, configure, connect, confirm — behind twenty test
|
|
73
|
+
* ids that NO suite touched. Not this package's (its two React tests cover
|
|
74
|
+
* the status board and the step primitives), and not the origin host's,
|
|
75
|
+
* whose `ai.e2e.ts` drives its own plan lock and upsell modal and only
|
|
76
|
+
* passes `ai-onboarding` on the way. A flow every adopter's owner has to
|
|
77
|
+
* walk was covered in neither repo, which is exactly the omission this
|
|
78
|
+
* capability exists to turn into a declaration.
|
|
63
79
|
* - **No `jobs`.** Nothing here sweeps: authorization codes are stateless
|
|
64
80
|
* signed blobs (the partial's header says so — there is no `oauth_codes`
|
|
65
81
|
* table and nothing to expire), and refresh-token revocation happens on
|
|
@@ -88,6 +104,12 @@ declare const mcpManifest: {
|
|
|
88
104
|
readonly namespace: "mcp";
|
|
89
105
|
};
|
|
90
106
|
readonly server: readonly ["http"];
|
|
107
|
+
readonly e2e: {
|
|
108
|
+
readonly entry: "@12-apps/mcp/e2e";
|
|
109
|
+
readonly world: {
|
|
110
|
+
readonly factory: "defineMcpConnectWorld";
|
|
111
|
+
};
|
|
112
|
+
};
|
|
91
113
|
};
|
|
92
114
|
|
|
93
115
|
export { mcpManifest };
|
package/dist/manifest/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/manifest/index.ts"],"sourcesContent":["/**\n * `@12-apps/mcp/manifest` — the SHARED wiring manifest.\n *\n * Identity, the Prisma contribution (the three tables behind the\n * authorization server) and the runtime inventory: `http` on the server.\n *\n * ON THE `db` DECLARATION — the reason this manifest exists at all.\n *\n * This package ships `prisma/mcp.prisma` and a migration beside it, and until\n * now nothing said so in a form a host assembler could read. The origin\n * host's assembler discovers partials in two steps: a package that carries\n * `\"wiring\": { \"db\": ... }` in its package.json is taken at its word, and a\n * package that carries nothing falls back to a STRUCTURAL scan — every\n * `prisma/*.prisma` under the package root is treated as a partial. So three\n * tables reach somebody's database because a `readdir` found them, not\n * because this package said they should. `@12-apps/notifications`' manifest\n * closed exactly this gap for its four models and the anti-pattern audit\n * names it directly; declaring changes no assembler behaviour (the\n * declaration is read where the scan used to run) and closes the one case\n * where composition was happening by accident.\n *\n * The mirror is what makes the declaration reachable: host assemblers are\n * plain Node reading `node_modules` and cannot execute this TypeScript, so\n * the contribution is repeated under `package.json` `\"wiring\": { \"db\": … }`\n * and `assertDbMirror` pins the two together in this package's own test run.\n *\n * `composed`, not `isolated`, and the choice is forced. An isolated stack\n * needs models carrying no relation into host tables — true of the three\n * here as SHIPPED (`user_id` and `user_email` are deliberately by-value\n * scalars, see the partial's header) — but the host is invited to add the FK\n * in its own migration, and the origin host's is `ON DELETE CASCADE`. A\n * package cannot declare isolation for models whose adopters relate them\n * into their own account tables.\n *\n * ## THE NARROWINGS, each deliberate\n *\n * - **No `mcp` capability.** This package IS the MCP runtime — the\n * OpenAPI→tools generator, the registry, the JSON-RPC transport, the\n * coverage gate. It advertises no tools of its own, and a manifest that\n * declared any would be the runtime describing itself to itself.\n * - **No `permissions`.** Authorization here is the OAuth scope set\n * (`MCP_SUPPORTED_SCOPES`) plus whatever the host's own RBAC says about\n * the proxied endpoint — the point of bearer passthrough is that an agent\n * inherits the caller's permissions rather than holding its own. There is\n * no id for this package to contribute.\n * - **No `web` inventory**, though `./react` ships the whole AI-connect\n * onboarding flow. A `surface` contribution is a `createWeb*` FACTORY —\n * one config object in, an object of component types out, memoised once by\n * the binder. `./react` has no such factory: it exports components a host\n * mounts with its own props (`AiIntegrationOnboarding` takes the store,\n * the endpoint URL and the live connection at the call site). Inventing a\n * factory here to have something to declare would freeze a props table\n * three hosts pass differently, which is the opposite of what a surface\n * contribution is for. When the flow grows a real bound surface, the\n * inventory grows with it.\n * - **No `env`.**
|
|
1
|
+
{"version":3,"sources":["../../src/manifest/index.ts"],"sourcesContent":["/**\n * `@12-apps/mcp/manifest` — the SHARED wiring manifest.\n *\n * Identity, the Prisma contribution (the three tables behind the\n * authorization server) and the runtime inventory: `http` on the server.\n *\n * ON THE `db` DECLARATION — the reason this manifest exists at all.\n *\n * This package ships `prisma/mcp.prisma` and a migration beside it, and until\n * now nothing said so in a form a host assembler could read. The origin\n * host's assembler discovers partials in two steps: a package that carries\n * `\"wiring\": { \"db\": ... }` in its package.json is taken at its word, and a\n * package that carries nothing falls back to a STRUCTURAL scan — every\n * `prisma/*.prisma` under the package root is treated as a partial. So three\n * tables reach somebody's database because a `readdir` found them, not\n * because this package said they should. `@12-apps/notifications`' manifest\n * closed exactly this gap for its four models and the anti-pattern audit\n * names it directly; declaring changes no assembler behaviour (the\n * declaration is read where the scan used to run) and closes the one case\n * where composition was happening by accident.\n *\n * The mirror is what makes the declaration reachable: host assemblers are\n * plain Node reading `node_modules` and cannot execute this TypeScript, so\n * the contribution is repeated under `package.json` `\"wiring\": { \"db\": … }`\n * and `assertDbMirror` pins the two together in this package's own test run.\n *\n * `composed`, not `isolated`, and the choice is forced. An isolated stack\n * needs models carrying no relation into host tables — true of the three\n * here as SHIPPED (`user_id` and `user_email` are deliberately by-value\n * scalars, see the partial's header) — but the host is invited to add the FK\n * in its own migration, and the origin host's is `ON DELETE CASCADE`. A\n * package cannot declare isolation for models whose adopters relate them\n * into their own account tables.\n *\n * ## THE NARROWINGS, each deliberate\n *\n * - **No `mcp` capability.** This package IS the MCP runtime — the\n * OpenAPI→tools generator, the registry, the JSON-RPC transport, the\n * coverage gate. It advertises no tools of its own, and a manifest that\n * declared any would be the runtime describing itself to itself.\n * - **No `permissions`.** Authorization here is the OAuth scope set\n * (`MCP_SUPPORTED_SCOPES`) plus whatever the host's own RBAC says about\n * the proxied endpoint — the point of bearer passthrough is that an agent\n * inherits the caller's permissions rather than holding its own. There is\n * no id for this package to contribute.\n * - **No `web` inventory**, though `./react` ships the whole AI-connect\n * onboarding flow. A `surface` contribution is a `createWeb*` FACTORY —\n * one config object in, an object of component types out, memoised once by\n * the binder. `./react` has no such factory: it exports components a host\n * mounts with its own props (`AiIntegrationOnboarding` takes the store,\n * the endpoint URL and the live connection at the call site). Inventing a\n * factory here to have something to declare would freeze a props table\n * three hosts pass differently, which is the opposite of what a surface\n * contribution is for. When the flow grows a real bound surface, the\n * inventory grows with it.\n * - **No `env`.** Three helpers here DO read `process.env`\n * (`trustedOriginsFromEnv`, and `loadSigningKeyFromEnv` for the key and its\n * id) — this used to say \"the package reads nothing itself\", which was\n * simply untrue and is the kind of sentence that makes a narrowing unfalsifiable.\n * The accurate reason is narrower and still holds: nothing is read at import\n * time or unconditionally, so a host that never calls these helpers has no\n * environment dependency on this package at all; and where they ARE called,\n * the variable's NAME is the caller's — passed as an argument by\n * `trustedOriginsFromEnv`, and defaulted but overridable per call by\n * `loadSigningKeyFromEnv`. `env` declares variables a host must answer for.\n * Declaring these would oblige every host to answer for names it may\n * legitimately have spelled differently, or never reads at all.\n * - **`e2e` IS declared** (`../e2e`). It was narrowed away with \"this package\n * packages no journeys\", which was true as a statement of fact and circular\n * as an argument: it packaged none because nobody had written any, while\n * `./react` shipped the entire AI-connect walkthrough — landing, assistant\n * picker, endpoint copy, configure, connect, confirm — behind twenty test\n * ids that NO suite touched. Not this package's (its two React tests cover\n * the status board and the step primitives), and not the origin host's,\n * whose `ai.e2e.ts` drives its own plan lock and upsell modal and only\n * passes `ai-onboarding` on the way. A flow every adopter's owner has to\n * walk was covered in neither repo, which is exactly the omission this\n * capability exists to turn into a declaration.\n * - **No `jobs`.** Nothing here sweeps: authorization codes are stateless\n * signed blobs (the partial's header says so — there is no `oauth_codes`\n * table and nothing to expire), and refresh-token revocation happens on\n * the rotation path rather than on a clock.\n *\n * `@12-apps/wiring` is a TYPE-ONLY devDependency (the report-builder move):\n * the manifest is a plain `satisfies`-checked value, and the producer\n * factories' runtime assertions run in this package's own test suite.\n */\n\nimport type { PackageManifest } from \"@12-apps/wiring\";\n\nexport const mcpManifest = {\n name: \"@12-apps/mcp\",\n contract: 1,\n db: { partial: \"prisma/mcp.prisma\", migrations: \"prisma/migrations\" },\n /**\n * A refused token grant, an unresolvable signing key or a rejected\n * redirect URI files under `mcp` rather than under whichever host mounted\n * the authorization server. Mandatory for runtime manifests since wiring\n * 1.3.0, and this is the surface that most needs it: every failure here is\n * a caller who cannot connect, reported to them as an opaque OAuth error\n * code by specification.\n */\n observability: { namespace: \"mcp\" },\n server: [\"http\"],\n e2e: { entry: \"@12-apps/mcp/e2e\", world: { factory: \"defineMcpConnectWorld\" } },\n} as const satisfies PackageManifest;\n"],"mappings":";;;AA0FO,IAAM,cAAc;AAAA,EACzB,MAAM;AAAA,EACN,UAAU;AAAA,EACV,IAAI,EAAE,SAAS,qBAAqB,YAAY,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASpE,eAAe,EAAE,WAAW,MAAM;AAAA,EAClC,QAAQ,CAAC,MAAM;AAAA,EACf,KAAK,EAAE,OAAO,oBAAoB,OAAO,EAAE,SAAS,wBAAwB,EAAE;AAChF;","names":[]}
|
package/dist/oauth/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { b as McpSigningKeyProvider, R as RefreshTokenStore, N as NewOAuthClient, S as StoredOAuthClient, c as NewRefreshToken, d as StoredRefreshToken, e as McpOauthStores, f as McpConnectionStore } from '../create-api-mcp-oauth-CsC0jlH7.js';
|
|
2
2
|
export { g as ACCESS_TOKEN_TTL_SECONDS, h as AccessTokenError, i as AccessTokenErrorCode, A as ApiMcpOauth, C as CodeReplayStore, D as DEFAULT_MCP_RESOURCE_PATH, j as DEFAULT_OAUTH_PATHS, k as DEFAULT_PROVIDER_ROOTS, l as DEFAULT_SIGNING_KEY_ENV, m as DEFAULT_SIGNING_KEY_ID_ENV, n as MCP_SUPPORTED_SCOPES, o as McpConnectionRecording, M as McpOauthConfig, p as McpOauthContext, q as McpOauthHandlers, r as McpOauthPaths, a as McpOauthRoute, s as McpOauthSession, t as McpScope, u as McpSigningKey, O as OAuthClientStore, P as ProviderAttributionRule, v as PublicSigningJwk, w as RegisterClientInput, x as RegisteredClient, y as SIGNING_ALG, z as SignAccessTokenInput, B as StoredMcpConnection, T as TokenEndpointAuthMethod, V as VerifiedAccessToken, E as VerifyAccessTokenOptions, F as createApiMcpOauth, G as hashSecret, H as inProcessCodeReplayStore, I as issuer, J as loadSigningKeyFromEnv, K as matchesRedirectUri, L as originFromRequest, Q as providerFromRedirectUris, U as registerClient, W as resolveMcpOauthConfig, X as resolveTrustedOrigin, Y as resourceAudience, Z as signAccessToken, _ as signingKeyProvider, $ as trustedOriginsFromEnv, a0 as verifyAccessToken } from '../create-api-mcp-oauth-CsC0jlH7.js';
|
|
3
|
-
import {
|
|
3
|
+
import { d as AiProvider } from '../guide-CrzdsdNf.js';
|
|
4
4
|
import 'jose';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ConfirmActionCopy } from '@12-apps/ui/copy';
|
|
2
2
|
import { OnboardingStore, OnboardingStateSnapshot } from '@12-apps/onboarding';
|
|
3
|
-
import {
|
|
4
|
-
export {
|
|
5
|
-
export { A as AI_CAPABILITIES, a as AI_CONNECT_PROMPT, b as AI_HOST_GUIDES, c as AI_PERMISSION_MODEL, E as EN_US_AI_CAPABILITIES, d as EN_US_AI_CONNECT_PROMPT, e as EN_US_AI_HOST_GUIDES, f as EN_US_AI_PERMISSION_MODEL, P as PT_BR_AI_CAPABILITIES, g as PT_BR_AI_CONNECT_PROMPT, h as PT_BR_AI_HOST_GUIDES, i as PT_BR_AI_PERMISSION_MODEL } from '../locales-
|
|
3
|
+
import { a as AiHostGuide, d as AiProvider, b as AiCapability, f as AiHostBrand } from '../guide-CrzdsdNf.js';
|
|
4
|
+
export { A as AiConnectPromptSpec, g as AiHostConfigureStage, h as AiHostLink, i as aiConnectPrompt, p as providerForHostId } from '../guide-CrzdsdNf.js';
|
|
5
|
+
export { A as AI_CAPABILITIES, a as AI_CONNECT_PROMPT, b as AI_HOST_GUIDES, c as AI_PERMISSION_MODEL, E as EN_US_AI_CAPABILITIES, d as EN_US_AI_CONNECT_PROMPT, e as EN_US_AI_HOST_GUIDES, f as EN_US_AI_PERMISSION_MODEL, P as PT_BR_AI_CAPABILITIES, g as PT_BR_AI_CONNECT_PROMPT, h as PT_BR_AI_HOST_GUIDES, i as PT_BR_AI_PERMISSION_MODEL } from '../locales-Cv0Pecvu.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Every word the AI-integration screens render, as REQUIRED host config
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Feature: Connecting an AI assistant to the store
|
|
2
|
+
|
|
3
|
+
The walkthrough a store owner follows to let an assistant reach their data.
|
|
4
|
+
Every step below is this package's own component; what an assistant is
|
|
5
|
+
called, and which ones are offered, is the host's configuration.
|
|
6
|
+
|
|
7
|
+
Background:
|
|
8
|
+
Given I am signed in as somebody who may connect an assistant
|
|
9
|
+
When I open the AI integration screen
|
|
10
|
+
|
|
11
|
+
Scenario: The landing says what an assistant will be able to do
|
|
12
|
+
Then the landing explains the permission model before anything is connected
|
|
13
|
+
And it shows examples of what an assistant can be asked
|
|
14
|
+
|
|
15
|
+
Scenario: Choosing an assistant starts the manual walkthrough
|
|
16
|
+
When I start the walkthrough
|
|
17
|
+
And I choose an assistant that has no one-click install
|
|
18
|
+
Then I am asked to copy the store's endpoint
|
|
19
|
+
|
|
20
|
+
Scenario: Copying the endpoint is what advances the wizard
|
|
21
|
+
When I start the walkthrough
|
|
22
|
+
And I choose an assistant that has no one-click install
|
|
23
|
+
And I copy the endpoint
|
|
24
|
+
Then the walkthrough has moved on to configuring the connector
|
|
25
|
+
|
|
26
|
+
Scenario: Configuring will not advance until the connector page is opened
|
|
27
|
+
When I start the walkthrough
|
|
28
|
+
And I choose an assistant that has no one-click install
|
|
29
|
+
And I copy the endpoint
|
|
30
|
+
Then continuing is refused until I open the connector page
|
|
31
|
+
And once opened, continuing reaches the connect step
|
|
32
|
+
|
|
33
|
+
Scenario: Going back returns to the previous step
|
|
34
|
+
When I start the walkthrough
|
|
35
|
+
And I choose an assistant that has no one-click install
|
|
36
|
+
And I copy the endpoint
|
|
37
|
+
And I go back a step
|
|
38
|
+
Then I am asked to copy the store's endpoint
|
|
39
|
+
|
|
40
|
+
Scenario: The confirmation waits for the assistant, and can be re-tested
|
|
41
|
+
When I start the walkthrough
|
|
42
|
+
And I choose an assistant that has no one-click install
|
|
43
|
+
And I copy the endpoint
|
|
44
|
+
And I work through configuring and connecting
|
|
45
|
+
Then the confirmation is still waiting for the assistant
|
|
46
|
+
And it offers to test the connection again
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"sideEffects":
|
|
5
|
+
"sideEffects": [
|
|
6
|
+
"**/e2e/**"
|
|
7
|
+
],
|
|
6
8
|
"description": "App-agnostic MCP server core: generate one MCP tool per OpenAPI operation and proxy each call carrying the caller's bearer token (permission passthrough). Also ships the OAuth 2.1 authorization server (./oauth, ./hono: register/authorize/token, JWKS and both .well-known documents), the package-owned Prisma partial + migration for its three tables, the mcp:generate/mcp:check (./generate) and mcp:coverage (./coverage) gates, and the reusable AI-connect onboarding UI (./react).",
|
|
7
9
|
"exports": {
|
|
8
10
|
".": {
|
|
@@ -37,7 +39,11 @@
|
|
|
37
39
|
"types": "./dist/manifest/server.d.ts",
|
|
38
40
|
"default": "./dist/manifest/server.js"
|
|
39
41
|
},
|
|
40
|
-
"./package.json": "./package.json"
|
|
42
|
+
"./package.json": "./package.json",
|
|
43
|
+
"./e2e": {
|
|
44
|
+
"types": "./dist/e2e/index.d.ts",
|
|
45
|
+
"default": "./dist/e2e/index.js"
|
|
46
|
+
}
|
|
41
47
|
},
|
|
42
48
|
"scripts": {
|
|
43
49
|
"build": "rm -rf dist && tsup",
|
|
@@ -53,14 +59,16 @@
|
|
|
53
59
|
"dependencies": {
|
|
54
60
|
"@12-apps/onboarding": "^2.7.0",
|
|
55
61
|
"@12-apps/rbac": "^4.11.0",
|
|
56
|
-
"@12-apps/ui": "^6.10.
|
|
62
|
+
"@12-apps/ui": "^6.10.1",
|
|
57
63
|
"@mui/icons-material": "^6.5.0",
|
|
58
64
|
"jose": "^6.1.3",
|
|
59
65
|
"react": "^19.2.0"
|
|
60
66
|
},
|
|
61
67
|
"peerDependencies": {
|
|
62
68
|
"@12-apps/wiring": ">=1.9.0",
|
|
69
|
+
"@playwright/test": ">=1.55.0",
|
|
63
70
|
"hono": ">=4.0.0",
|
|
71
|
+
"playwright-bdd": ">=8.3.0",
|
|
64
72
|
"react": ">=19.0.0",
|
|
65
73
|
"zod": ">=4.0.0"
|
|
66
74
|
},
|
|
@@ -68,9 +76,15 @@
|
|
|
68
76
|
"@12-apps/wiring": {
|
|
69
77
|
"optional": true
|
|
70
78
|
},
|
|
79
|
+
"@playwright/test": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
71
82
|
"hono": {
|
|
72
83
|
"optional": true
|
|
73
84
|
},
|
|
85
|
+
"playwright-bdd": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
74
88
|
"zod": {
|
|
75
89
|
"optional": true
|
|
76
90
|
}
|
|
@@ -81,6 +95,7 @@
|
|
|
81
95
|
"@12-apps/typescript-config": "^1.21.0",
|
|
82
96
|
"@12-apps/wiring": "^1.14.0",
|
|
83
97
|
"@mui/material": "^6.5.0",
|
|
98
|
+
"@playwright/test": "^1.61.1",
|
|
84
99
|
"@testing-library/react": "^16.1.0",
|
|
85
100
|
"@types/node": "^22.15.3",
|
|
86
101
|
"@types/react": "19.2.2",
|
|
@@ -88,6 +103,7 @@
|
|
|
88
103
|
"eslint-plugin-test-flakiness": "^1.4.0",
|
|
89
104
|
"hono": "^4.6.0",
|
|
90
105
|
"jsdom": "^25.0.1",
|
|
106
|
+
"playwright-bdd": "^9.2.0",
|
|
91
107
|
"react-dom": "^19.2.0",
|
|
92
108
|
"tsup": "^8.0.0",
|
|
93
109
|
"typescript": "^5.8.2",
|
|
@@ -129,6 +145,7 @@
|
|
|
129
145
|
"!**/*.spec.*",
|
|
130
146
|
"!**/*.stories.*",
|
|
131
147
|
"!**/*.test-story.*",
|
|
132
|
-
"!**/test-helpers.*"
|
|
148
|
+
"!**/test-helpers.*",
|
|
149
|
+
"features"
|
|
133
150
|
]
|
|
134
151
|
}
|
package/src/e2e/globs.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where this package's journeys and their steps live, ON DISK, in the consumer's
|
|
6
|
+
* `node_modules`.
|
|
7
|
+
*
|
|
8
|
+
* `defineBddConfig` takes filesystem globs, not module specifiers — it hands
|
|
9
|
+
* them to a glob matcher, so `'@12-apps/mcp/features/**'` matches
|
|
10
|
+
* nothing and, worse, matches nothing SILENTLY: bddgen compiles the features it
|
|
11
|
+
* found, finds none, and the run is green with zero journeys.
|
|
12
|
+
*
|
|
13
|
+
* So the paths are RESOLVED here instead of written down by every consumer. A
|
|
14
|
+
* host that hard-codes `node_modules/@12-apps/mcp/...` is broken by
|
|
15
|
+
* pnpm's nested store, by a workspace link, and by this package's own layout
|
|
16
|
+
* changing; resolving from the package's own entry point is correct under all
|
|
17
|
+
* three.
|
|
18
|
+
*/
|
|
19
|
+
const require_ = createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The package root, found from a file this package definitely exports.
|
|
23
|
+
* `./package.json` is exported precisely so this lookup needs no guess about
|
|
24
|
+
* directory depth.
|
|
25
|
+
*/
|
|
26
|
+
function packageRoot(): string {
|
|
27
|
+
return dirname(require_.resolve('@12-apps/mcp/package.json'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Every packaged `.feature`, for `defineBddConfig({ features })`. */
|
|
31
|
+
export const mcpFeatures: string = join(packageRoot(), 'features/**/*.feature');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The base `defineBddConfig({ featuresRoot })` must be given, and it is NOT
|
|
35
|
+
* optional decoration.
|
|
36
|
+
*
|
|
37
|
+
* bddgen mirrors each feature's path RELATIVE TO `featuresRoot` under
|
|
38
|
+
* `outputDir`. Left unset it defaults to the config's own directory, so a
|
|
39
|
+
* feature living under `node_modules/...` compiles to a path Playwright then
|
|
40
|
+
* IGNORES, because its default `testIgnore` excludes `**\/node_modules/**`.
|
|
41
|
+
*
|
|
42
|
+
* The result is the worst kind of green: bddgen reports the features compiled,
|
|
43
|
+
* Playwright collects zero specs from them, and the run passes with the whole
|
|
44
|
+
* packaged suite silently absent.
|
|
45
|
+
*
|
|
46
|
+
* A host running SEVERAL packaged suites gives `featuresRoot` the directory they
|
|
47
|
+
* all sit under, since bddgen takes exactly one — and that is safe to do,
|
|
48
|
+
* because a feature outside it is a hard exit from bddgen, never a quiet
|
|
49
|
+
* omission.
|
|
50
|
+
*/
|
|
51
|
+
export const mcpFeaturesRoot: string = join(packageRoot(), 'features');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every packaged step definition, for `defineBddConfig({ steps })`.
|
|
55
|
+
*
|
|
56
|
+
* COMPILED JavaScript, and that is the reason this package has a build step at
|
|
57
|
+
* all while everything else it exports is raw `.ts` through the `exports` map.
|
|
58
|
+
* Those entries are consumed by an application's BUNDLER, which transpiles
|
|
59
|
+
* whatever it is pointed at. These are loaded by NODE — `playwright.config.ts`
|
|
60
|
+
* imports this module, and bddgen imports the step files — and Node refuses to
|
|
61
|
+
* strip types from anything under `node_modules`
|
|
62
|
+
* (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). Playwright's own TS transform
|
|
63
|
+
* does not rescue them either; it skips `node_modules` by design.
|
|
64
|
+
*
|
|
65
|
+
* The host's own steps glob must ALSO be listed — that is where its
|
|
66
|
+
* `defineMcpConnectWorld` call lives, and playwright-bdd imports every step
|
|
67
|
+
* file before any scenario runs, which is what makes the registration land in
|
|
68
|
+
* time in every worker.
|
|
69
|
+
*/
|
|
70
|
+
export const mcpSteps: string = join(packageRoot(), 'dist/e2e/steps/**/*.js');
|
package/src/e2e/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@12-apps/mcp/e2e` — the packaged AI-connect journeys and the port a host
|
|
3
|
+
* implements to run them.
|
|
4
|
+
*
|
|
5
|
+
* The `e2e` capability the manifest declares. A host adds the three globs to
|
|
6
|
+
* its bdd config and calls `defineMcpConnectWorld` from inside its own steps
|
|
7
|
+
* glob; every scenario this package ships then runs in that host, including the
|
|
8
|
+
* ones added after it integrated. Nothing is copied, so nothing can rot.
|
|
9
|
+
*/
|
|
10
|
+
export { mcpFeatures, mcpFeaturesRoot, mcpSteps } from './globs.js';
|
|
11
|
+
export {
|
|
12
|
+
defineMcpConnectWorld,
|
|
13
|
+
mcpConnectWorld,
|
|
14
|
+
type McpConnectFixtures,
|
|
15
|
+
type McpConnectWorld,
|
|
16
|
+
} from './world.js';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { expect, type Page } from '@playwright/test';
|
|
2
|
+
import { createBdd } from 'playwright-bdd';
|
|
3
|
+
|
|
4
|
+
import { mcpConnectWorld } from '../world.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The packaged AI-connect journeys' step definitions.
|
|
8
|
+
*
|
|
9
|
+
* Every locator is a test id THIS package's own components render —
|
|
10
|
+
* `ai-landing`, `ai-host-select`, `ai-copy-step`, `ai-configure-step`,
|
|
11
|
+
* `ai-connect-step`, `ai-confirm-waiting`, `mcp-endpoint-url` — which is what
|
|
12
|
+
* makes the scenarios portable: they mean the same thing in any app mounting
|
|
13
|
+
* `AiIntegrationOnboarding`. Everything a host owns (sign-in, where the flow is
|
|
14
|
+
* mounted, which assistants it offers) arrives through the world port.
|
|
15
|
+
*
|
|
16
|
+
* ## Why `package.json` declares this file under `sideEffects`
|
|
17
|
+
*
|
|
18
|
+
* Every Given/When/Then below runs at IMPORT time — registering a step is the
|
|
19
|
+
* whole point of the module, and it exports nothing anybody imports by name. To
|
|
20
|
+
* a bundler doing tree-shaking that reads as dead weight, and dropping it is
|
|
21
|
+
* licensed: the result is a suite where bddgen reports the features compiled
|
|
22
|
+
* and every scenario then fails on an undefined step.
|
|
23
|
+
*
|
|
24
|
+
* Not one step asserts a SENTENCE. The assistants' names, the button words and
|
|
25
|
+
* the permission prose are REQUIRED host config (FUT-760) — this package ships
|
|
26
|
+
* no assistant and no copy of its own — so a spec written against them could
|
|
27
|
+
* only ever have run in one adopter.
|
|
28
|
+
*/
|
|
29
|
+
const { Given, When, Then } = createBdd();
|
|
30
|
+
|
|
31
|
+
Given('I am signed in as somebody who may connect an assistant', async ({ page }) => {
|
|
32
|
+
await mcpConnectWorld().signInAsOwner(page);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
When('I open the AI integration screen', async ({ page }) => {
|
|
36
|
+
await mcpConnectWorld().openAiIntegrationScreen(page);
|
|
37
|
+
await expect(page.getByTestId('ai-onboarding')).toBeVisible();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
Then('the landing explains the permission model before anything is connected', async ({ page }) => {
|
|
41
|
+
await expect(page.getByTestId('ai-landing')).toBeVisible();
|
|
42
|
+
// The callout is the point of the landing: an owner is about to hand a third
|
|
43
|
+
// party a key to their store, and this is the screen that says what the key
|
|
44
|
+
// opens. It renders BEFORE any assistant is chosen, which is the only moment
|
|
45
|
+
// the answer is still "nothing".
|
|
46
|
+
await expect(page.getByTestId('ai-permission-callout')).toBeVisible();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
Then('it shows examples of what an assistant can be asked', async ({ page }) => {
|
|
50
|
+
await expect(page.getByTestId('ai-capability-examples')).toBeVisible();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
When('I start the walkthrough', async ({ page }) => {
|
|
54
|
+
await page.getByTestId('ai-landing-start').click();
|
|
55
|
+
await expect(page.getByTestId('ai-host-select')).toBeVisible();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
When('I choose an assistant that has no one-click install', async ({ page }) => {
|
|
59
|
+
// The flow BRANCHES on the guide's `pluginUrl`: with one it becomes
|
|
60
|
+
// Escolher → Instalar → Confirmar and there is no endpoint to copy at all.
|
|
61
|
+
// The host names a guide that takes the manual path, because which
|
|
62
|
+
// assistants exist is its configuration and not this package's.
|
|
63
|
+
await page.getByTestId(`ai-host-card-${mcpConnectWorld().fixtures.manualHostId}`).click();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
Then("I am asked to copy the store's endpoint", async ({ page }) => {
|
|
67
|
+
await expect(page.getByTestId('ai-copy-step')).toBeVisible();
|
|
68
|
+
await expect(page.getByTestId('mcp-endpoint-url')).toContainText(
|
|
69
|
+
mcpConnectWorld().fixtures.endpointUrl,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** Copying is the ACTION that advances this step — there is no next button. */
|
|
74
|
+
async function copyEndpoint(page: Page): Promise<void> {
|
|
75
|
+
await page.getByTestId('ai-copy-endpoint').click();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Open the assistant's connector page — the one action that unlocks "next".
|
|
80
|
+
*
|
|
81
|
+
* By its OWN id, not `getByRole('link')`: this control is a Button calling
|
|
82
|
+
* `window.open`, and the only real `<a>` in the step is the DOCS link, which
|
|
83
|
+
* deliberately does not unlock anything. A role query would have taken the
|
|
84
|
+
* docs link and then failed on an assertion about the button.
|
|
85
|
+
*/
|
|
86
|
+
async function openConnectorPage(page: Page): Promise<void> {
|
|
87
|
+
await page.getByTestId(`ai-host-link-${mcpConnectWorld().fixtures.manualHostId}`).click();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
When('I copy the endpoint', async ({ page }) => {
|
|
91
|
+
await copyEndpoint(page);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
Then('the walkthrough has moved on to configuring the connector', async ({ page }) => {
|
|
95
|
+
// The claim is that COPYING advanced it. The copy step offers no "next", so
|
|
96
|
+
// reaching the configure step at all proves the copy handler drove the
|
|
97
|
+
// wizard rather than some button the operator happened to press.
|
|
98
|
+
await expect(page.getByTestId('ai-configure-step')).toBeVisible();
|
|
99
|
+
await expect(page.getByTestId('ai-copy-step')).toHaveCount(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
Then('continuing is refused until I open the connector page', async ({ page }) => {
|
|
103
|
+
// The guard that stops an owner walking past the one step that does the
|
|
104
|
+
// actual work: the connector page has to be opened before "next" unlocks.
|
|
105
|
+
await expect(page.getByTestId('ai-configure-next')).toBeDisabled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
Then('once opened, continuing reaches the connect step', async ({ page }) => {
|
|
109
|
+
await openConnectorPage(page);
|
|
110
|
+
await expect(page.getByTestId('ai-configure-next')).toBeEnabled();
|
|
111
|
+
await page.getByTestId('ai-configure-next').click();
|
|
112
|
+
await expect(page.getByTestId('ai-connect-step')).toBeVisible();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
When('I go back a step', async ({ page }) => {
|
|
116
|
+
await page.getByTestId('ai-step-back').click();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
When('I work through configuring and connecting', async ({ page }) => {
|
|
120
|
+
await openConnectorPage(page);
|
|
121
|
+
await page.getByTestId('ai-configure-next').click();
|
|
122
|
+
await expect(page.getByTestId('ai-connect-step')).toBeVisible();
|
|
123
|
+
await page.getByTestId('ai-connect-done').click();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
Then('the confirmation is still waiting for the assistant', async ({ page }) => {
|
|
127
|
+
// Waiting rather than connected, because nothing has actually connected: the
|
|
128
|
+
// wizard reaching its last step is not evidence of a live connection, and
|
|
129
|
+
// this is the step that refuses to claim otherwise.
|
|
130
|
+
await expect(page.getByTestId('ai-confirm-waiting')).toBeVisible();
|
|
131
|
+
await expect(page.getByTestId('ai-confirm-connected')).toHaveCount(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
Then('it offers to test the connection again', async ({ page }) => {
|
|
135
|
+
await expect(page.getByTestId('ai-confirm-retest')).toBeVisible();
|
|
136
|
+
});
|
package/src/e2e/world.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The port a HOST implements to run the packaged AI-connect journeys.
|
|
5
|
+
*
|
|
6
|
+
* These journeys were written because the walkthrough they cover was tested
|
|
7
|
+
* NOWHERE. `./react` ships the whole flow — the landing, the assistant picker,
|
|
8
|
+
* the endpoint to copy, the configure and connect steps, the confirmation and
|
|
9
|
+
* its re-test — and twenty test ids go with it. Not one of them appeared in
|
|
10
|
+
* this package's own suite, and not one appeared in the origin host's specs
|
|
11
|
+
* either: that host's `ai.e2e.ts` drives its OWN plan lock and upsell modal,
|
|
12
|
+
* reaching only `ai-onboarding` and the status board on the way past.
|
|
13
|
+
*
|
|
14
|
+
* So the flow a store owner actually walks was covered by nothing, in either
|
|
15
|
+
* repo. That is the gap the `e2e` capability exists to convert into a
|
|
16
|
+
* declaration rather than an omission nobody can see.
|
|
17
|
+
*
|
|
18
|
+
* What stays the host's: how it signs an owner in, where it mounts the flow,
|
|
19
|
+
* and which assistant its `hosts` config offers — the guides are REQUIRED
|
|
20
|
+
* config (FUT-760), so the package has no assistant of its own to name.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Facts about the host that the assertions have to name. */
|
|
24
|
+
export interface McpConnectFixtures {
|
|
25
|
+
/**
|
|
26
|
+
* The id of an assistant the host offers whose guide has NO `pluginUrl`.
|
|
27
|
+
*
|
|
28
|
+
* The flow BRANCHES on that field: a guide carrying one gets `InstallStep`
|
|
29
|
+
* (a single "open the install link" button), and a guide without one gets
|
|
30
|
+
* the three-step manual path — copy the endpoint, configure, connect. These
|
|
31
|
+
* journeys walk the MANUAL path, so the host has to point at a guide that
|
|
32
|
+
* takes it. Naming the branch here rather than guessing keeps the scenario
|
|
33
|
+
* from failing in a host whose first assistant happens to ship a plugin.
|
|
34
|
+
*/
|
|
35
|
+
manualHostId: string;
|
|
36
|
+
/** The endpoint URL that host serves, as it is rendered for copying. */
|
|
37
|
+
endpointUrl: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What a host must be able to do for these journeys to run in it. */
|
|
41
|
+
export interface McpConnectWorld {
|
|
42
|
+
/**
|
|
43
|
+
* Put the browser in a known signed-in state as somebody who may connect an
|
|
44
|
+
* assistant, with the flow's progress RESET to its first run.
|
|
45
|
+
*
|
|
46
|
+
* The reset is load-bearing rather than hygiene: the wizard persists its step
|
|
47
|
+
* through `@12-apps/onboarding`, so a scenario that advanced it would hand
|
|
48
|
+
* the next one a flow resuming from the middle — and the landing step, which
|
|
49
|
+
* two of these scenarios assert, would never render.
|
|
50
|
+
*/
|
|
51
|
+
signInAsOwner(page: Page): Promise<void>;
|
|
52
|
+
/** Land on the screen that mounts `AiIntegrationOnboarding`. */
|
|
53
|
+
openAiIntegrationScreen(page: Page): Promise<void>;
|
|
54
|
+
fixtures: McpConnectFixtures;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let installed: McpConnectWorld | null = null;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Install the host's implementation. Call this from a module inside the host's
|
|
61
|
+
* OWN steps glob — playwright-bdd imports every step file before any scenario
|
|
62
|
+
* runs, so a top-level call there is registered in time, in every worker.
|
|
63
|
+
*/
|
|
64
|
+
export function defineMcpConnectWorld(world: McpConnectWorld): void {
|
|
65
|
+
installed = world;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The installed world, or a refusal naming the fix.
|
|
70
|
+
*
|
|
71
|
+
* Throws rather than returning null: a step that ran against an absent world
|
|
72
|
+
* would fail on whatever it touched next, somewhere unrelated to the actual
|
|
73
|
+
* mistake, which is a diagnosis nobody should have to make twice.
|
|
74
|
+
*/
|
|
75
|
+
export function mcpConnectWorld(): McpConnectWorld {
|
|
76
|
+
if (!installed) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'No mcp e2e world is installed. Call defineMcpConnectWorld({ … }) from a module inside ' +
|
|
79
|
+
"your own `steps` glob — playwright-bdd imports those before any scenario runs, " +
|
|
80
|
+
'which is what makes the registration land in every worker.',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return installed;
|
|
84
|
+
}
|
package/src/manifest/index.ts
CHANGED
|
@@ -53,13 +53,29 @@
|
|
|
53
53
|
* three hosts pass differently, which is the opposite of what a surface
|
|
54
54
|
* contribution is for. When the flow grows a real bound surface, the
|
|
55
55
|
* inventory grows with it.
|
|
56
|
-
* - **No `env`.**
|
|
57
|
-
* `
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
56
|
+
* - **No `env`.** Three helpers here DO read `process.env`
|
|
57
|
+
* (`trustedOriginsFromEnv`, and `loadSigningKeyFromEnv` for the key and its
|
|
58
|
+
* id) — this used to say "the package reads nothing itself", which was
|
|
59
|
+
* simply untrue and is the kind of sentence that makes a narrowing unfalsifiable.
|
|
60
|
+
* The accurate reason is narrower and still holds: nothing is read at import
|
|
61
|
+
* time or unconditionally, so a host that never calls these helpers has no
|
|
62
|
+
* environment dependency on this package at all; and where they ARE called,
|
|
63
|
+
* the variable's NAME is the caller's — passed as an argument by
|
|
64
|
+
* `trustedOriginsFromEnv`, and defaulted but overridable per call by
|
|
65
|
+
* `loadSigningKeyFromEnv`. `env` declares variables a host must answer for.
|
|
66
|
+
* Declaring these would oblige every host to answer for names it may
|
|
67
|
+
* legitimately have spelled differently, or never reads at all.
|
|
68
|
+
* - **`e2e` IS declared** (`../e2e`). It was narrowed away with "this package
|
|
69
|
+
* packages no journeys", which was true as a statement of fact and circular
|
|
70
|
+
* as an argument: it packaged none because nobody had written any, while
|
|
71
|
+
* `./react` shipped the entire AI-connect walkthrough — landing, assistant
|
|
72
|
+
* picker, endpoint copy, configure, connect, confirm — behind twenty test
|
|
73
|
+
* ids that NO suite touched. Not this package's (its two React tests cover
|
|
74
|
+
* the status board and the step primitives), and not the origin host's,
|
|
75
|
+
* whose `ai.e2e.ts` drives its own plan lock and upsell modal and only
|
|
76
|
+
* passes `ai-onboarding` on the way. A flow every adopter's owner has to
|
|
77
|
+
* walk was covered in neither repo, which is exactly the omission this
|
|
78
|
+
* capability exists to turn into a declaration.
|
|
63
79
|
* - **No `jobs`.** Nothing here sweeps: authorization codes are stateless
|
|
64
80
|
* signed blobs (the partial's header says so — there is no `oauth_codes`
|
|
65
81
|
* table and nothing to expire), and refresh-token revocation happens on
|
|
@@ -86,4 +102,5 @@ export const mcpManifest = {
|
|
|
86
102
|
*/
|
|
87
103
|
observability: { namespace: "mcp" },
|
|
88
104
|
server: ["http"],
|
|
105
|
+
e2e: { entry: "@12-apps/mcp/e2e", world: { factory: "defineMcpConnectWorld" } },
|
|
89
106
|
} as const satisfies PackageManifest;
|