174 lines
7.7 KiB
TypeScript
174 lines
7.7 KiB
TypeScript
import { expect, test } from "vitest";
|
|
import { chrome, desktop } from "@openwork/hosts";
|
|
import { createVisualEvidence, screenshot, validate } from "@openwork/test-evidence";
|
|
import {
|
|
assignPluginToMarketplace,
|
|
captureOpenedUrls,
|
|
clickButton,
|
|
completeDesktopHandoff,
|
|
createMarketplace,
|
|
createPluginWithSkill,
|
|
enabledButtons,
|
|
ensureMemberSession,
|
|
go,
|
|
grantMarketplaceAccess,
|
|
readHandoffDeepLink,
|
|
readResolvedMarketplace,
|
|
signIn,
|
|
signInInBrowser,
|
|
visibleText,
|
|
waitForText,
|
|
waitUntilInteractive,
|
|
} from "@openwork/behaviors";
|
|
|
|
/**
|
|
* CORE JOURNEY: a person opens the app for the first time, signs in to OpenWork
|
|
* Cloud — which hands off to a real browser and comes back — then shares a skill
|
|
* with a colleague by authoring it inside a plugin and putting that plugin on a
|
|
* marketplace the colleague can use.
|
|
*
|
|
* Faithfulness notes:
|
|
* - The browser hop is real: we capture the URL the app asks the OS to open
|
|
* (PATH shim over xdg-open, which is what shell.openExternal calls on Linux)
|
|
* and drive that page in a real Chrome.
|
|
* - Only the OS protocol dispatch of `openwork://den-auth?grant=…` is bridged,
|
|
* because a container registers no protocol handler. The grant is the real one
|
|
* the app generated and the browser approved; it is handed to the product's own
|
|
* documented entry point for this case.
|
|
*/
|
|
|
|
const e2eTestsEnabled = process.env.OPENWORK_EVAL_E2E_TESTS === "1";
|
|
const denApiUrl = process.env.OPENWORK_EVAL_DEN_API_URL?.trim().replace(/\/+$/, "") ?? "";
|
|
const denWebUrl = (process.env.OPENWORK_EVAL_DEN_WEB_URL?.trim() || denApiUrl.replace("127.0.0.1", "localhost")).replace(/\/+$/, "");
|
|
const password = process.env.OPENWORK_EVAL_DEMO_PASSWORD?.trim() || "OpenWorkDemo123!";
|
|
const adminEmail = process.env.OPENWORK_EVAL_DEMO_EMAIL?.trim() || "alex@acme.test";
|
|
const colleagueEmail = process.env.OPENWORK_EVAL_MEMBER_EMAIL?.trim() || "jordan.demo@acme.test";
|
|
|
|
const title = !e2eTestsEnabled
|
|
? "first-run cloud sharing skipped: set OPENWORK_EVAL_E2E_TESTS=1 to opt in"
|
|
: !denApiUrl
|
|
? "first-run cloud sharing skipped: set OPENWORK_EVAL_DEN_API_URL to a running Den"
|
|
: "first run signs in through the browser, then shares a skill with a colleague via a marketplace";
|
|
|
|
test.skipIf(!e2eTestsEnabled || !denApiUrl)(title, async () => {
|
|
const den = { apiUrl: denApiUrl, webUrl: denWebUrl };
|
|
const capture = await captureOpenedUrls();
|
|
|
|
// The app must find our xdg-open shim first, so we can see where it points.
|
|
await using app = await desktop({
|
|
name: "first-run-cloud-share",
|
|
bootstrap: { baseUrl: den.webUrl, requireSignin: false },
|
|
env: { PATH: `${capture.binDir}:${process.env.PATH ?? ""}` },
|
|
});
|
|
await using visualEvidence = createVisualEvidence("first-run-cloud-share");
|
|
|
|
{
|
|
const shot = await screenshot(app);
|
|
const seen = await validate(shot, [
|
|
"A fresh OpenWork app is visible offering to sign in to OpenWork Cloud",
|
|
"No error or 'Something went wrong' message is visible",
|
|
]);
|
|
expect(seen.ok, seen.why).toBe(true);
|
|
await visualEvidence.recordScreenshot(shot, seen);
|
|
}
|
|
|
|
// 1. Sign in from inside the app: this must hand off to the browser.
|
|
const buttons = await enabledButtons(app);
|
|
const signInLabel = ["Sign in to OpenWork Cloud", "Sign in"].find((label) => buttons.includes(label));
|
|
expect(signInLabel, `no sign-in control. Buttons: ${buttons.join(" | ")}`).toBeDefined();
|
|
if (!signInLabel) throw new Error("unreachable");
|
|
await clickButton(app, signInLabel, { timeoutMs: 60_000 });
|
|
|
|
// The real handoff URL carries desktopAuth/desktopScheme; the grant is issued
|
|
// by Den only after the person signs in in the browser.
|
|
const handoffUrl = await capture.waitForUrl(
|
|
(url) => url.includes("desktopAuth=1") || url.includes("desktopScheme=") || url.includes("grant="),
|
|
{ timeoutMs: 90_000 },
|
|
);
|
|
expect(handoffUrl.startsWith(den.webUrl), `the app opened an unexpected origin: ${handoffUrl}`).toBe(true);
|
|
|
|
// 2. Finish in a real browser. Same host as the app here; `chrome({ host })`
|
|
// is what a split topology would use.
|
|
await using browser = await chrome({ name: "cloud-signin", startUrl: "about:blank" });
|
|
await signInInBrowser(browser, handoffUrl, { email: adminEmail, password });
|
|
{
|
|
const shot = await screenshot(browser);
|
|
const seen = await validate(shot, [
|
|
"A browser page shows an OpenWork Cloud sign-in result or dashboard, not a sign-in form error",
|
|
"No 'invalid credentials' or error banner is visible",
|
|
]);
|
|
expect(seen.ok, seen.why).toBe(true);
|
|
await visualEvidence.recordScreenshot(shot, seen);
|
|
}
|
|
|
|
// 3. Back to the app with the grant Den issued for this browser session.
|
|
const deepLink = await readHandoffDeepLink(browser, { timeoutMs: 120_000 });
|
|
expect(deepLink.startsWith("openwork://"), `unexpected deep link: ${deepLink}`).toBe(true);
|
|
await completeDesktopHandoff(app, deepLink, den.webUrl);
|
|
await waitUntilInteractive(app, { timeoutMs: 180_000 });
|
|
const signedInText = await visibleText(app);
|
|
expect(
|
|
/acme|signed in|account/i.test(signedInText),
|
|
`the app does not look signed in. Visible text: ${signedInText.slice(0, 400)}`,
|
|
).toBe(true);
|
|
{
|
|
const shot = await screenshot(app);
|
|
const seen = await validate(shot, [
|
|
"The app is back in focus and no longer offers a bare 'Sign in to OpenWork Cloud' as the only action",
|
|
"No sign-in failure message is visible",
|
|
]);
|
|
expect(seen.ok, seen.why).toBe(true);
|
|
await visualEvidence.recordScreenshot(shot, seen);
|
|
}
|
|
|
|
// 4. Author a skill inside a plugin and share it via a marketplace.
|
|
const admin = await signIn(den, { email: adminEmail, password });
|
|
const colleague = await ensureMemberSession(den, admin, {
|
|
email: colleagueEmail,
|
|
password,
|
|
name: "Jordan Demo",
|
|
markVerifiedCmd: process.env.OPENWORK_EVAL_MARK_VERIFIED_CMD?.trim(),
|
|
});
|
|
|
|
const stamp = Date.now();
|
|
const skillName = `shared-standup-${stamp}`;
|
|
const marketplace = await createMarketplace(admin, { name: `Team Marketplace ${stamp}` });
|
|
const plugin = await createPluginWithSkill(admin, {
|
|
name: `Standup Kit ${stamp}`,
|
|
skillName,
|
|
skillBody: "Summarise yesterday, today, and blockers in three short bullets.",
|
|
marketplaceId: marketplace.id,
|
|
});
|
|
await assignPluginToMarketplace(admin, marketplace.id, plugin.id).catch(async (error: unknown) => {
|
|
// Creating the plugin with marketplaceId may already have published it; only
|
|
// a genuine failure should surface.
|
|
const resolved = await readResolvedMarketplace(admin, marketplace.id);
|
|
if (!resolved.pluginNames.includes(plugin.name)) throw error;
|
|
});
|
|
await grantMarketplaceAccess(admin, marketplace.id, { orgWide: true });
|
|
|
|
// 5. The colleague can actually see the shared skill.
|
|
const asColleague = await readResolvedMarketplace(colleague, marketplace.id);
|
|
expect(
|
|
asColleague.pluginNames,
|
|
`the colleague cannot see the plugin. Saw: ${JSON.stringify(asColleague.pluginNames)}`,
|
|
).toContain(plugin.name);
|
|
expect(
|
|
asColleague.skillNames.some((name) => name.includes(skillName)),
|
|
`the colleague cannot see the shared skill. Saw: ${JSON.stringify(asColleague.skillNames)}`,
|
|
).toBe(true);
|
|
|
|
// 6. And it is visible in the app's own extensions surface.
|
|
await go(app, `/workspace/${app.readiness.workspaceId ?? ""}/settings/extensions`).catch(() => undefined);
|
|
await waitForText(app, "Library", { timeoutMs: 60_000 }).catch(() => undefined);
|
|
{
|
|
const shot = await screenshot(app);
|
|
const seen = await validate(shot, [
|
|
"An OpenWork surface listing extensions, skills or connections is visible",
|
|
"No 'Something went wrong' crash message is visible",
|
|
]);
|
|
expect(seen.ok, seen.why).toBe(true);
|
|
await visualEvidence.recordScreenshot(shot, seen);
|
|
}
|
|
});
|