@bugmole/cli 0.4.0 → 0.6.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/LICENSE +18 -4
- package/package.json +7 -4
- package/scripts/bugmole-admin.mjs +194 -0
- package/scripts/bugmole-admin.test.mjs +62 -0
- package/scripts/bugmole.test.ts +94 -2
- package/scripts/bugmole.ts +290 -43
- package/scripts/ensure-playwright.cjs +87 -0
- package/scripts/ensure-playwright.test.ts +36 -0
- package/scripts/sync-byok.d.mts +2 -0
- package/scripts/sync-byok.mjs +18 -0
- package/scripts/sync-launcher-version.cjs +24 -0
- package/scripts/sync-launcher-version.test.ts +28 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -6
- package/spec/domain_rules.yaml +139 -0
- package/spec/roles.yaml +20 -0
- package/spec/test-case-results.schema.json +20 -4
- package/spec/test-cases.schema.json +131 -16
- package/src/billing/plan-catalog.test.ts +82 -2
- package/src/billing/plan-catalog.ts +212 -9
- package/src/integrations/jira.ts +264 -0
- package/src/mcp/roles-and-review.test.ts +111 -0
- package/src/mcp/server.ts +358 -22
- package/src/mcp/write-test-cases.test.ts +68 -0
- package/src/registry/control-plane-client.ts +121 -6
- package/src/registry/migrations/0034_signup_attribution.sql +19 -0
- package/src/registry/migrations/0035_domain_verify_file.sql +6 -0
- package/src/registry/migrations/0036_task_approval.sql +11 -0
- package/src/registry/migrations/0037_spec_proposals.sql +27 -0
- package/src/registry/migrations/0038_local_worker_seen.sql +5 -0
- package/src/registry/migrations/0039_project_secrets.sql +37 -0
- package/src/registry/migrations/0040_roles_and_task_review.sql +31 -0
- package/src/registry/migrations/0041_jira_integration.sql +75 -0
- package/src/registry/migrations/0042_subscription_gaps.sql +10 -0
- package/src/registry/migrations/0043_workspace_feature_overrides.sql +17 -0
- package/src/registry/migrations/0044_project_identity.sql +31 -0
- package/src/registry/project-identity.test.ts +99 -0
- package/src/registry/project-identity.ts +206 -0
- package/src/registry/roles.test.ts +66 -0
- package/src/registry/roles.ts +201 -0
- package/src/registry/task-scheduling.test.ts +94 -0
- package/src/registry/task-scheduling.ts +199 -2
- package/src/registry/test-case-revisions.test.ts +57 -0
- package/src/registry/test-case-revisions.ts +132 -0
- package/src/registry-worker/ai/routes.ts +3 -3
- package/src/registry-worker/artifacts.ts +23 -4
- package/src/registry-worker/billing/billing-core.test.ts +1 -1
- package/src/registry-worker/billing/checkout-routes.ts +53 -4
- package/src/registry-worker/billing/enforcement.ts +67 -12
- package/src/registry-worker/billing/paypal/api.ts +14 -0
- package/src/registry-worker/billing/paypal/client.ts +9 -0
- package/src/registry-worker/billing/paypal/provider.ts +10 -1
- package/src/registry-worker/billing/paypal.test.ts +108 -1
- package/src/registry-worker/billing/plan-gaps.test.ts +216 -0
- package/src/registry-worker/billing/provider.ts +8 -0
- package/src/registry-worker/billing/routes.ts +2 -1
- package/src/registry-worker/billing/subscriptions.ts +114 -4
- package/src/registry-worker/core.ts +24 -0
- package/src/registry-worker/devices/policy.ts +4 -5
- package/src/registry-worker/devices/routes.ts +8 -8
- package/src/registry-worker/domains/domains.test.ts +64 -1
- package/src/registry-worker/domains/routes.ts +32 -15
- package/src/registry-worker/domains.ts +40 -2
- package/src/registry-worker/feature-access.ts +165 -0
- package/src/registry-worker/feature-flags/admin.ts +300 -0
- package/src/registry-worker/feature-flags/feature-flags.test.ts +270 -0
- package/src/registry-worker/feature-flags/routes.ts +102 -0
- package/src/registry-worker/features.ts +11 -0
- package/src/registry-worker/feedback/feedback.test.ts +47 -0
- package/src/registry-worker/feedback/routes.ts +2 -2
- package/src/registry-worker/feedback/website.ts +77 -0
- package/src/registry-worker/flags.ts +80 -18
- package/src/registry-worker/github/checks.ts +4 -4
- package/src/registry-worker/hooks.ts +9 -0
- package/src/registry-worker/identity/oidc.test.ts +103 -0
- package/src/registry-worker/identity/oidc.ts +176 -0
- package/src/registry-worker/index.ts +494 -173
- package/src/registry-worker/jira/connection.ts +111 -0
- package/src/registry-worker/jira/jira.test.ts +404 -0
- package/src/registry-worker/jira/routes.ts +432 -0
- package/src/registry-worker/jira/workflow.ts +577 -0
- package/src/registry-worker/jobs/retention.ts +21 -5
- package/src/registry-worker/mcp/tools.ts +2 -1
- package/src/registry-worker/notifications/alerts.ts +4 -4
- package/src/registry-worker/notifications/notifications.test.ts +11 -0
- package/src/registry-worker/notifications/routes.ts +9 -10
- package/src/registry-worker/notifications/teams.ts +6 -2
- package/src/registry-worker/org/routes.test.ts +24 -1
- package/src/registry-worker/org/routes.ts +9 -8
- package/src/registry-worker/projects/identity.ts +194 -0
- package/src/registry-worker/projects/inactivity.ts +162 -0
- package/src/registry-worker/projects/projects.test.ts +283 -0
- package/src/registry-worker/proposals/proposals.test.ts +80 -0
- package/src/registry-worker/proposals/routes.ts +183 -0
- package/src/registry-worker/roles/roles.test.ts +84 -0
- package/src/registry-worker/roles/routes.ts +141 -0
- package/src/registry-worker/runner/dispatch.ts +1 -0
- package/src/registry-worker/runner/routes.ts +22 -5
- package/src/registry-worker/runner/runner.test.ts +59 -0
- package/src/registry-worker/runner/tokens.ts +12 -1
- package/src/registry-worker/secrets/crypto.ts +135 -0
- package/src/registry-worker/secrets/routes.ts +296 -0
- package/src/registry-worker/secrets/secrets.test.ts +237 -0
- package/src/registry-worker/signup/attribution.test.ts +147 -0
- package/src/registry-worker/signup/attribution.ts +194 -0
- package/src/registry-worker/signup/policy.ts +2 -2
- package/src/registry-worker/signup/routes.ts +12 -3
- package/src/registry-worker/sso/membership.ts +4 -2
- package/src/registry-worker/sso/routes.ts +24 -8
- package/src/registry-worker/sso/sso.test.ts +3 -2
- package/src/registry-worker/task-approval.test.ts +67 -0
- package/src/registry-worker/task-resume.test.ts +196 -0
- package/src/registry-worker/task-review.test.ts +121 -0
- package/src/runtime/ai-exploration.test.ts +34 -1
- package/src/runtime/ai-exploration.ts +97 -3
- package/src/runtime/appium-driver.ts +7 -1
- package/src/runtime/apply-proposals.test.ts +74 -0
- package/src/runtime/apply-proposals.ts +41 -0
- package/src/runtime/cloud-secrets.test.ts +201 -0
- package/src/runtime/cloud-secrets.ts +210 -0
- package/src/runtime/config-validate.ts +12 -9
- package/src/runtime/cursor-driver-run.ts +14 -3
- package/src/runtime/discovery-task.test.ts +26 -1
- package/src/runtime/discovery-task.ts +87 -7
- package/src/runtime/executor.ts +18 -10
- package/src/runtime/explorer.test.ts +32 -0
- package/src/runtime/explorer.ts +48 -0
- package/src/runtime/flow-language.ts +43 -8
- package/src/runtime/init-wizard.ts +112 -6
- package/src/runtime/journey-editor.ts +78 -2
- package/src/runtime/journey-graph.test.ts +60 -0
- package/src/runtime/local-registry-stub.test.ts +64 -0
- package/src/runtime/local-registry-stub.ts +255 -7
- package/src/runtime/local-vault.ts +65 -0
- package/src/runtime/pipeline.test.ts +23 -0
- package/src/runtime/pipeline.ts +61 -20
- package/src/runtime/planner.test.ts +24 -1
- package/src/runtime/planner.ts +88 -23
- package/src/runtime/playwright-driver.test.ts +164 -0
- package/src/runtime/playwright-driver.ts +286 -15
- package/src/runtime/project-identity.test.ts +161 -0
- package/src/runtime/project-identity.ts +232 -0
- package/src/runtime/project-roles.test.ts +107 -0
- package/src/runtime/project-roles.ts +158 -0
- package/src/runtime/propose-cli.ts +66 -0
- package/src/runtime/record-run-verdicts.test.ts +58 -0
- package/src/runtime/record-run-verdicts.ts +38 -4
- package/src/runtime/reporter.ts +1 -1
- package/src/runtime/reset.ts +2 -0
- package/src/runtime/roles-cli.test.ts +47 -0
- package/src/runtime/roles-cli.ts +60 -0
- package/src/runtime/run-job.test.ts +21 -0
- package/src/runtime/run-job.ts +8 -0
- package/src/runtime/run-once.ts +68 -5
- package/src/runtime/scenario-matrix.test.ts +41 -0
- package/src/runtime/scenario-matrix.ts +98 -0
- package/src/runtime/secret-driver.ts +77 -0
- package/src/runtime/secret-redaction.ts +69 -0
- package/src/runtime/secret-sources.test.ts +442 -0
- package/src/runtime/secret-sources.ts +176 -0
- package/src/runtime/secrets-cli.ts +146 -0
- package/src/runtime/serve-worker.ts +64 -6
- package/src/runtime/site-discovery.test.ts +181 -4
- package/src/runtime/site-discovery.ts +248 -29
- package/src/runtime/vault-federation.test.ts +37 -0
- package/src/runtime/vault-federation.ts +128 -0
- package/src/runtime/web-suite.test.ts +65 -0
- package/src/runtime/web-suite.ts +8 -2
- package/src/storage/create-object-store.ts +5 -1
- package/src/storage/object-store.ts +12 -1
- package/src/storage/test-case-results.test.ts +64 -0
- package/src/storage/test-case-results.ts +49 -0
- package/src/vendor/byok.ts +371 -0
|
@@ -5,16 +5,16 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import { chromium } from "playwright";
|
|
8
|
-
import { discoverableRoute, discoverSite, flowsFor, slugify } from "./site-discovery.js";
|
|
9
|
-
import { discoveryUrl, runDiscoveryTask } from "./discovery-task.js";
|
|
8
|
+
import { discoverableRoute, discoverSite, discoveryLimitsFromEnv, flowsFor, slugify, stoppedAtSignIn } from "./site-discovery.js";
|
|
9
|
+
import { discoveryUrl, progressOf, runDiscoveryTask } from "./discovery-task.js";
|
|
10
10
|
import { PlaywrightDriver } from "./playwright-driver.js";
|
|
11
11
|
import { startLocalRegistryStub } from "./local-registry-stub.js";
|
|
12
12
|
import { ControlPlaneClient } from "../registry/control-plane-client.js";
|
|
13
13
|
|
|
14
14
|
const pages: Record<string, string> = {
|
|
15
|
-
"/": `<title>Acme</title><nav><a href="/pricing">Pricing</a> <a href="/signup">Sign up</a> <a href="/logout">Log out</a> <a href="https://elsewhere.example/">Blog</a
|
|
15
|
+
"/": `<title>Acme</title><nav><a href="/pricing">Pricing</a> <a href="/signup">Sign up</a> <a href="/logout">Log out</a> <a href="https://elsewhere.example/">Blog</a> </nav><h1>Welcome to Acme</h1>`,
|
|
16
16
|
"/pricing": `<title>Pricing · Acme</title><a href="/">Home</a><h1>Plans for every team</h1>`,
|
|
17
|
-
"/signup": `<title>Sign up · Acme</title><h1>Create your account</h1>
|
|
17
|
+
"/signup": `<title>Sign up · Acme</title><h1>Create your account</h1><a href="/">Home</a> <a href="/logout">Log out</a> <a href="/api/export">Export</a> <a href="/report.csv" download>Download report</a> <a href="/data">Raw data</a>
|
|
18
18
|
<form aria-label="Registration"><label>Email <input type="email" required></label><label>Password <input type="password" required></label><button type="submit">Create account</button></form>`,
|
|
19
19
|
"/logout": `<title>Bye</title><h1>You are signed out</h1>`,
|
|
20
20
|
};
|
|
@@ -24,6 +24,11 @@ async function withSite(run: (baseUrl: string, hits: string[]) => Promise<void>)
|
|
|
24
24
|
const server = http.createServer((request, response) => {
|
|
25
25
|
const route = new URL(request.url ?? "/", "http://x").pathname;
|
|
26
26
|
hits.push(route);
|
|
27
|
+
if (route === "/data" || route === "/api/export" || route === "/report.csv") {
|
|
28
|
+
response.writeHead(200, { "content-type": route === "/report.csv" ? "text/csv" : "application/json" });
|
|
29
|
+
response.end(route === "/report.csv" ? "a,b\n" : JSON.stringify({ rows: [] }));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
27
32
|
const body = pages[route];
|
|
28
33
|
response.writeHead(body ? 200 : 404, { "content-type": "text/html" });
|
|
29
34
|
response.end(`<!doctype html><html><body>${body ?? "Not found"}</body></html>`);
|
|
@@ -86,6 +91,10 @@ test("explores a site from its URL like a visitor, and the flows it writes pass"
|
|
|
86
91
|
const result = await discoverSite(baseUrl, { dwellMs: 0, onEvent: (event) => { events.push(event.text); } });
|
|
87
92
|
assert.deepEqual(result.pages.map((page) => page.route).sort(), ["/", "/pricing", "/signup"]);
|
|
88
93
|
assert.ok(!hits.includes("/logout"), "never opens a sign-out link");
|
|
94
|
+
assert.ok(!hits.includes("/api/export") && !hits.includes("/report.csv"), "never opens an API or download link");
|
|
95
|
+
assert.ok(events.some((text) => text.startsWith("Skipping “Export” (it downloads a file or calls the API)")));
|
|
96
|
+
assert.ok(events.some((text) => /“Raw data” opens a file \(application\/json\), not a page/.test(text)), "a link that opens JSON isn't a page");
|
|
97
|
+
assert.equal(events.filter((text) => text.startsWith("Skipping “Log out”")).length, 1, "a skipped link is reported once, not on every page that has it");
|
|
89
98
|
assert.ok(events.includes("Visiting home page"));
|
|
90
99
|
assert.ok(events.includes("Clicking “Pricing”"));
|
|
91
100
|
assert.ok(events.some((text) => text.startsWith("Skipping “Log out”")));
|
|
@@ -118,6 +127,114 @@ test("explores a site from its URL like a visitor, and the flows it writes pass"
|
|
|
118
127
|
});
|
|
119
128
|
});
|
|
120
129
|
|
|
130
|
+
test("a crawl cut short by its limits says which linked pages it never opened", async (t) => {
|
|
131
|
+
if (!(await chromiumInstalled())) {
|
|
132
|
+
t.skip("Chromium is not installed");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await withSite(async (baseUrl) => {
|
|
136
|
+
const events: Array<{ kind: string; text: string }> = [];
|
|
137
|
+
const capped = await discoverSite(baseUrl, { dwellMs: 0, maxPages: 2, onEvent: (event) => { events.push(event); } });
|
|
138
|
+
assert.equal(capped.pages.length, 2);
|
|
139
|
+
assert.equal(capped.limits?.maxPages, 2);
|
|
140
|
+
assert.equal(capped.limits?.stoppedAtPageLimit, true);
|
|
141
|
+
assert.equal(capped.limits?.notFollowed.length, 1);
|
|
142
|
+
assert.ok(!capped.pages.some((page) => page.route === capped.limits?.notFollowed[0]));
|
|
143
|
+
assert.match(events.find((event) => event.kind === "limit")?.text ?? "", /Stopped at the 2-page limit; 1 linked page was not opened/);
|
|
144
|
+
const progress = progressOf(baseUrl, capped);
|
|
145
|
+
assert.equal(progress.limits?.notFollowed, 1);
|
|
146
|
+
assert.deepEqual(progress.limits?.notFollowedRoutes, capped.limits?.notFollowed);
|
|
147
|
+
|
|
148
|
+
const shallow = await discoverSite(baseUrl, { dwellMs: 0, maxDepth: 0 });
|
|
149
|
+
assert.deepEqual(shallow.pages.map((page) => page.route), ["/"]);
|
|
150
|
+
assert.equal(shallow.limits?.stoppedAtPageLimit, false);
|
|
151
|
+
assert.deepEqual([...(shallow.limits?.notFollowed ?? [])].sort(), ["/pricing", "/signup"]);
|
|
152
|
+
|
|
153
|
+
const whole = await discoverSite(baseUrl, { dwellMs: 0 });
|
|
154
|
+
assert.deepEqual(whole.limits, { maxPages: 12, maxDepth: 2, stoppedAtPageLimit: false, notFollowed: [] });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a project's own worker reads its exploration limits from the environment", () => {
|
|
159
|
+
assert.deepEqual(discoveryLimitsFromEnv({ BUGMOLE_DISCOVERY_MAX_PAGES: "60", BUGMOLE_DISCOVERY_MAX_DEPTH: "4" }), { maxPages: 60, maxDepth: 4 });
|
|
160
|
+
assert.deepEqual(discoveryLimitsFromEnv({ BUGMOLE_DISCOVERY_MAX_PAGES: "0", BUGMOLE_DISCOVERY_MAX_DEPTH: "two" }), {});
|
|
161
|
+
assert.deepEqual(discoveryLimitsFromEnv({}), {});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("explores a signed-in app from a saved session, and stays inside it", async (t) => {
|
|
165
|
+
if (!(await chromiumInstalled())) {
|
|
166
|
+
t.skip("Chromium is not installed");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const hits: string[] = [];
|
|
170
|
+
const signedIn: Record<string, string> = {
|
|
171
|
+
// "Test plans" routes on the client, like a single-page app: the address
|
|
172
|
+
// and the page change some time after the click. "Billing" and "Sign out"
|
|
173
|
+
// only exist once the account menu is opened.
|
|
174
|
+
"/": `<h1>Overview</h1><a href="/plans" id="plans">Test plans</a><a href="/account/sign-out">Account</a><a href="/connect">Connect GitHub</a>
|
|
175
|
+
<a href="/manage/domains"><strong>Domains</strong> <span>Verify the sites Bugmole Cloud is allowed to run against.</span></a>
|
|
176
|
+
<button aria-expanded="false" id="account"><span aria-hidden="true">A</span> <strong>Account</strong> <small>menu</small></button><div id="menu"></div>
|
|
177
|
+
<script>
|
|
178
|
+
document.getElementById("plans").addEventListener("click", (event) => {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
history.pushState(null, "", "/plans" + location.search);
|
|
182
|
+
document.querySelector("h1").textContent = "Test plans";
|
|
183
|
+
}, 600);
|
|
184
|
+
});
|
|
185
|
+
const account = document.getElementById("account");
|
|
186
|
+
account.addEventListener("click", () => {
|
|
187
|
+
const open = account.getAttribute("aria-expanded") !== "true";
|
|
188
|
+
account.setAttribute("aria-expanded", String(open));
|
|
189
|
+
document.getElementById("menu").innerHTML = open ? '<a href="/billing">Billing</a><a href="/sign-out">Sign out</a>' : "";
|
|
190
|
+
});
|
|
191
|
+
document.addEventListener("keydown", (event) => { if (event.key === "Escape" && account.getAttribute("aria-expanded") === "true") account.click(); });
|
|
192
|
+
</script>`,
|
|
193
|
+
"/plans": `<h1>Test plans</h1><a href="/">Overview</a>`,
|
|
194
|
+
"/billing": `<h1>Billing</h1><a href="/">Overview</a>`,
|
|
195
|
+
"/manage/domains": `<h1>Domains</h1><a href="/">Overview</a>`,
|
|
196
|
+
};
|
|
197
|
+
const server = http.createServer((request, response) => {
|
|
198
|
+
const route = new URL(request.url ?? "/", "http://x").pathname;
|
|
199
|
+
hits.push(route);
|
|
200
|
+
if (!/session=ok/.test(request.headers.cookie ?? "")) {
|
|
201
|
+
response.writeHead(307, { location: "/login" });
|
|
202
|
+
return response.end();
|
|
203
|
+
}
|
|
204
|
+
// Same-origin link that hands off to another site, like an install redirect.
|
|
205
|
+
if (route === "/connect") {
|
|
206
|
+
response.writeHead(302, { location: `http://localhost:${(server.address() as { port: number }).port}/elsewhere` });
|
|
207
|
+
return response.end();
|
|
208
|
+
}
|
|
209
|
+
const body = signedIn[route];
|
|
210
|
+
response.writeHead(body ? 200 : 404, { "content-type": "text/html" });
|
|
211
|
+
response.end(`<!doctype html><html><body>${body ?? "Not found"}</body></html>`);
|
|
212
|
+
});
|
|
213
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
214
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "bugmole-signed-in-"));
|
|
215
|
+
try {
|
|
216
|
+
const baseUrl = `http://127.0.0.1:${(server.address() as { port: number }).port}/`;
|
|
217
|
+
const storageState = path.join(directory, "state.json");
|
|
218
|
+
await fs.writeFile(storageState, JSON.stringify({
|
|
219
|
+
cookies: [{ name: "session", value: "ok", domain: "127.0.0.1", path: "/", expires: -1, httpOnly: true, secure: false, sameSite: "Lax" }],
|
|
220
|
+
origins: [],
|
|
221
|
+
}));
|
|
222
|
+
const result = await discoverSite(`${baseUrl}?project=p1`, { dwellMs: 0, storageState, openEachPage: true });
|
|
223
|
+
assert.deepEqual(result.pages.map((page) => page.route).sort(), ["/", "/billing", "/manage/domains", "/plans"]);
|
|
224
|
+
assert.equal(result.pages.find((page) => page.route === "/plans")?.heading, "Test plans", "waits for a client-side page to render");
|
|
225
|
+
assert.deepEqual(result.pages.find((page) => page.route === "/billing")?.path, ["Account menu", "Billing"]);
|
|
226
|
+
assert.ok(!hits.includes("/account/sign-out"), "a sign-out address is never opened, whatever its label");
|
|
227
|
+
assert.ok(!hits.includes("/sign-out"), "nor a sign-out link inside a menu");
|
|
228
|
+
assert.ok(result.events.some((event) => event.text === "“Connect GitHub” leaves the app"));
|
|
229
|
+
const plans = result.flows.find((flow) => flow.route === "/plans")!;
|
|
230
|
+
assert.match(plans.content, new RegExp(`^url: ${baseUrl}plans\\?project=p1\\n`), "the base URL's app context is kept");
|
|
231
|
+
assert.doesNotMatch(plans.content, /tapOn/, "each flow opens its page directly");
|
|
232
|
+
} finally {
|
|
233
|
+
server.close();
|
|
234
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
121
238
|
test("a discovery task streams progress, then publishes plans and a valid journey graph", async (t) => {
|
|
122
239
|
if (!(await chromiumInstalled())) {
|
|
123
240
|
t.skip("Chromium is not installed");
|
|
@@ -166,3 +283,63 @@ test("a discovery task streams progress, then publishes plans and a valid journe
|
|
|
166
283
|
}
|
|
167
284
|
});
|
|
168
285
|
});
|
|
286
|
+
|
|
287
|
+
test("a crawl that only reached sign-in screens is reported as stopped at sign-in", () => {
|
|
288
|
+
const page = (extra: Record<string, unknown>) => ({ route: "/login", url: "https://app.test/login", title: "App", path: [], links: [], forms: [], ...extra });
|
|
289
|
+
assert.equal(stoppedAtSignIn({ pages: [page({ heading: "Welcome back", controls: ["Continue with Aegis"] })] }), true);
|
|
290
|
+
assert.equal(stoppedAtSignIn({ pages: [page({ forms: [{ name: "Log in", fields: [{ label: "Email", type: "email", required: true }, { label: "Password", type: "password", required: true }] }] })] }), true);
|
|
291
|
+
assert.equal(stoppedAtSignIn({ pages: [page({ heading: "Pricing" })] }), false, "a page that isn't a sign-in screen");
|
|
292
|
+
assert.equal(stoppedAtSignIn({ pages: [] }), false, "nothing explored is a different failure");
|
|
293
|
+
assert.equal(
|
|
294
|
+
stoppedAtSignIn({ pages: [page({ heading: "Welcome back" }), page({ route: "/", heading: "Home" }), page({ route: "/a", heading: "A" })] }),
|
|
295
|
+
false,
|
|
296
|
+
"a public site that also has a sign-in page",
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("an app that opens on a sign-in button: discovery records the button, says it stopped, and skips AI", async (t) => {
|
|
301
|
+
if (!(await chromiumInstalled())) {
|
|
302
|
+
t.skip("Chromium is not installed");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const server = http.createServer((_request, response) => {
|
|
306
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
307
|
+
response.end("<!doctype html><html><head><title>Bugmole</title></head><body><h1>Welcome back</h1><p>Sign in or create your account.</p><button>Continue with Aegis →</button></body></html>");
|
|
308
|
+
});
|
|
309
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
310
|
+
const baseUrl = `http://127.0.0.1:${(server.address() as { port: number }).port}/`;
|
|
311
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "bugmole-sign-in-wall-"));
|
|
312
|
+
const stub = await startLocalRegistryStub({ directory: path.join(directory, "registry") });
|
|
313
|
+
try {
|
|
314
|
+
const seen = await discoverSite(baseUrl, { dwellMs: 0 });
|
|
315
|
+
assert.deepEqual(seen.pages[0].controls, ["Continue with Aegis →"]);
|
|
316
|
+
assert.equal(stoppedAtSignIn(seen), true);
|
|
317
|
+
|
|
318
|
+
const client = new ControlPlaneClient({ registryUrl: stub.origin, apiKey: "local", projectId: "demo" });
|
|
319
|
+
const { task } = await client.createTask({ title: "Explore with AI app", description: `Explore ${baseUrl} like a first-time visitor, then have AI write multi-page journeys.`, taskType: "discovery", idempotencyKey: "discover-wall" });
|
|
320
|
+
const cfg = {
|
|
321
|
+
project: { id: "demo" },
|
|
322
|
+
runtime: { spec_dir: path.join(directory, "spec"), artifacts_dir: path.join(directory, "artifacts") },
|
|
323
|
+
storage: { provider: "local" },
|
|
324
|
+
};
|
|
325
|
+
const updates: Array<Record<string, unknown>> = [];
|
|
326
|
+
await runDiscoveryTask(cfg, {
|
|
327
|
+
claimTask: client.claimTask.bind(client),
|
|
328
|
+
publishPlan: client.publishPlan.bind(client),
|
|
329
|
+
updateTask: async (id: string, update: Record<string, unknown>) => {
|
|
330
|
+
updates.push(update);
|
|
331
|
+
return client.updateTask(id, update);
|
|
332
|
+
},
|
|
333
|
+
}, task, "worker-1");
|
|
334
|
+
const final = updates.at(-1)!;
|
|
335
|
+
assert.equal(final.status, "completed", String(final.errorMessage));
|
|
336
|
+
assert.match(String(final.completionSummary), /Stopped at a sign-in page/);
|
|
337
|
+
const texts = (final.continuation as { events: Array<{ text: string }> }).events.map((event) => event.text);
|
|
338
|
+
assert.ok(texts.some((text) => text.startsWith("Stopped at a sign-in page")), texts.join("\n"));
|
|
339
|
+
assert.ok(!texts.some((text) => /AI/.test(text) && !text.startsWith("Stopped")), "the AI step never ran");
|
|
340
|
+
} finally {
|
|
341
|
+
await stub.close();
|
|
342
|
+
server.close();
|
|
343
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
344
|
+
}
|
|
345
|
+
});
|