@emseepea/testing 0.25.0 → 0.25.2

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/README.md CHANGED
@@ -643,8 +643,9 @@ days.
643
643
 
644
644
  Import `@emseepea/testing/chatgpt` only when you want to test the real ChatGPT
645
645
  web experience. Ordinary framework tests do not enable this adapter or install
646
- Playwright. This is an experimental browser
647
- adapter: ChatGPT controls can change, and live host qualification is pending.
646
+ Playwright. This is an experimental browser adapter: ChatGPT controls can
647
+ change, so adopters must qualify each capability they rely on against their
648
+ owned account and endpoint.
648
649
 
649
650
  On your own machine, install the optional browser tooling:
650
651
 
@@ -670,6 +671,63 @@ The command verifies a
670
671
  signed-in control, not the account's identity; the human confirms that identity.
671
672
  If that control changes, pass an account-specific `--authenticated-selector`.
672
673
 
674
+ ### Use an already-authorized browser
675
+
676
+ If the launched browser repeats a security challenge, stop setup. Complete
677
+ login, MFA, CAPTCHA, and consent yourself in an ordinary browser. The setup
678
+ helper keeps Chromium's sandbox enabled, but cannot guarantee that an
679
+ automation-launched browser will be accepted by the login service.
680
+
681
+ An existing Playwright integration can pass its owner-authorized `Page` directly
682
+ to `runNativeChatGptJourney`. The adapter does not need to launch that browser.
683
+ It cannot attach to an arbitrary ordinary Chrome window without a supported
684
+ browser control interface.
685
+
686
+ Chrome versions with GUI remote debugging expose it at
687
+ `chrome://inspect/#remote-debugging`. The browser owner can select "Allow remote
688
+ debugging for this browser instance" and approve the connection in Chrome.
689
+ This needs no command-line browser launch.
690
+
691
+ The runner must still execute on
692
+ the browser's host; enabling this option does not give a cloud runner access
693
+ to another computer. Use a dedicated test browser and disable debugging when
694
+ finished.
695
+
696
+ For a Chromium session that the adopter has already opened and exposed through
697
+ Chrome DevTools Protocol (CDP), the execution command can attach locally:
698
+
699
+ ```sh
700
+ npx emseepea-chatgpt --enable-native-chatgpt --scenario ./native-chatgpt.mjs \
701
+ --connect-cdp 'ws://127.0.0.1:9222/devtools/browser/<browser-id>' \
702
+ --browser-owner '<test-account-owner>'
703
+ ```
704
+
705
+ This is an alternative to the setup command, not a way to solve a CAPTCHA.
706
+ The browser owner must confirm the signed-in account and complete connection
707
+ consent before attaching. `--browser-owner` must match the scenario's account
708
+ owner. Use a dedicated synthetic-test browser with one browser context.
709
+
710
+ The command accepts only a browser WebSocket on literal `127.0.0.1`, an explicit
711
+ port from 1024 to 65535, and `/devtools/browser` with an optional browser ID.
712
+ Remote hosts,
713
+ credentials, query strings, redirects, HTTP discovery, and profile options are
714
+ rejected.
715
+
716
+ WebSocket messages and the initial send queue are capped at 16 MiB.
717
+ Keep the debugging interface and address private, bound to loopback, and
718
+ available only while testing; CDP grants browser control and is not an account
719
+ authorization mechanism.
720
+
721
+ Attachment opens a separate test tab in the authorized context. It does not
722
+ read or export cookies or storage state, inspect existing tabs, launch Chrome,
723
+ or alter login behavior or the owner's browser defaults.
724
+
725
+ After the scenario's required cleanup, the runner
726
+ closes its tab and disconnects. The owner browser and its existing tabs remain
727
+ open. Missing browser access or incomplete authentication blocks verification.
728
+
729
+ ### Connect and run the owned scenario
730
+
673
731
  Connect your authorized HTTPS MCP endpoint through ChatGPT's current plugin
674
732
  installation flow. Complete account consent yourself. A local server requires
675
733
  a supported HTTPS tunnel before ChatGPT can reach it. See the
@@ -728,6 +786,16 @@ Return ordered, run-scoped calls matching the declared method, target, outcome,
728
786
  and optional result hash. Each call includes request/result SHA-256 values,
729
787
  endpoint SHA-256, sequence, timestamp, and `nativeClient`.
730
788
 
789
+ Keep deterministic MCP qualification and native-host evidence separate.
790
+ Command-line qualification can prove a resource link's shape, catalogue
791
+ membership, exact `resources/read` result, and original bytes. A native ChatGPT
792
+ journey separately proves whether ChatGPT materializes or rejects that link.
793
+
794
+ Declare the expected `tools/call` and `resources/read` as distinct server calls.
795
+ If ChatGPT does not issue the expected read, record the checkpoint as
796
+ `incomplete` with category `server-observation-missing`; do not treat the
797
+ tool-returned URI as proof of a native resource read.
798
+
731
799
  Set
732
800
  `nativeClient: "verified-chatgpt"` only when your authenticated server boundary
733
801
  establishes that the request came through the dedicated ChatGPT connection;
package/chatgpt/run.mjs CHANGED
@@ -4,12 +4,15 @@ import { readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
+ import { attachOwnedBrowser, validateBrowserAttachment } from "./session.mjs";
7
8
 
8
9
  const help = `Usage: emseepea-chatgpt --enable-native-chatgpt --scenario <owned-module.mjs>
9
10
  [--profile <dedicated-directory>] [--output <private-evidence.json>]
11
+ Or attach: --connect-cdp <loopback-browser-WebSocket> --browser-owner <owner>
10
12
 
11
13
  The module exports authorization and createJourney({ page, runId }). It owns
12
- its assertions, server observations, and cleanup. Complete human setup first.
14
+ its assertions, server observations, and cleanup. Complete human setup first,
15
+ or explicitly confirm ownership of an already-authorized local CDP browser.
13
16
  This command never logs in, grants consent, or exports authentication state.
14
17
  `;
15
18
 
@@ -21,37 +24,49 @@ async function main() {
21
24
  for (let index = 0; index < args.length; index += 1) {
22
25
  const arg = args[index];
23
26
  if (arg === "--enable-native-chatgpt") continue;
24
- if (!["--scenario", "--profile", "--output"].includes(arg) || values.has(arg) || !args[index + 1] || args[index + 1].startsWith("--")) throw new Error("invalid-run-options");
27
+ if (!["--scenario", "--profile", "--output", "--connect-cdp", "--browser-owner"].includes(arg) || values.has(arg) || !args[index + 1] || args[index + 1].startsWith("--")) throw new Error("invalid-run-options");
25
28
  values.set(arg, args[++index]);
26
29
  }
27
30
  if (!values.get("--scenario")) throw new Error("owned-scenario-required");
28
31
  const scenario = await import(pathToFileURL(resolve(values.get("--scenario"))).href);
29
32
  if (typeof scenario.createJourney !== "function" || scenario.authorization?.approved !== true || typeof scenario.authorization.accountOwner !== "string") throw new Error("owned-scenario-required");
30
- const profile = resolve(values.get("--profile") ?? join(homedir(), ".emseepea", "chatgpt-browser"));
31
- const record = await readFile(join(profile, "emseepea-profile.json"), "utf8").then(JSON.parse).catch(() => undefined);
32
- const ownerSha256 = createHash("sha256").update(scenario.authorization.accountOwner).digest("hex");
33
- if (record?.schemaVersion !== 1 || record?.humanConfirmed !== true || record?.ownerSha256 !== ownerSha256 || !["chromium", "chrome", "msedge"].includes(record?.channel)) throw new Error("human-setup-required");
33
+ const attached = values.has("--connect-cdp");
34
+ if ((attached && values.has("--profile")) || (!attached && values.has("--browser-owner"))) throw new Error("invalid-run-options");
35
+ if (attached) validateBrowserAttachment(values.get("--connect-cdp"), values.get("--browser-owner"), scenario.authorization.accountOwner);
36
+ let profile;
37
+ let record;
38
+ if (!attached) {
39
+ profile = resolve(values.get("--profile") ?? join(homedir(), ".emseepea", "chatgpt-browser"));
40
+ record = await readFile(join(profile, "emseepea-profile.json"), "utf8").then(JSON.parse).catch(() => undefined);
41
+ const ownerSha256 = createHash("sha256").update(scenario.authorization.accountOwner).digest("hex");
42
+ if (record?.schemaVersion !== 1 || record?.humanConfirmed !== true || record?.ownerSha256 !== ownerSha256 || !["chromium", "chrome", "msedge"].includes(record?.channel)) throw new Error("human-setup-required");
43
+ }
34
44
  let chromium;
35
45
  try { ({ chromium } = await import("playwright")); }
36
46
  catch { throw new Error("optional-playwright-dependency-required"); }
37
47
  const { runNativeChatGptJourney } = await import("../dist/chatgpt.js");
38
- let context;
39
- try { context = await chromium.launchPersistentContext(profile, { headless: false, ...(record.channel === "chromium" ? {} : { channel: record.channel }) }); }
40
- catch { throw new Error("headed-browser-unavailable"); }
48
+ let session;
49
+ if (attached) session = await attachOwnedBrowser({ chromium, endpoint: values.get("--connect-cdp"), browserOwner: values.get("--browser-owner"), accountOwner: scenario.authorization.accountOwner });
50
+ else {
51
+ let context;
52
+ try { context = await chromium.launchPersistentContext(profile, { headless: false, chromiumSandbox: true, ...(record.channel === "chromium" ? {} : { channel: record.channel }) }); }
53
+ catch { throw new Error("headed-browser-unavailable"); }
54
+ session = { page: context.pages()[0] ?? await context.newPage(), close: () => context.close() };
55
+ }
41
56
  try {
42
- const page = context.pages()[0] ?? await context.newPage();
43
- await page.goto("https://chatgpt.com/", { waitUntil: "domcontentloaded" });
57
+ const page = session.page;
58
+ if (!attached) await page.goto("https://chatgpt.com/", { waitUntil: "domcontentloaded" });
44
59
  const runId = randomUUID();
45
60
  const declared = await scenario.createJourney({ page, runId });
46
61
  const evidence = await runNativeChatGptJourney({ ...declared, enabled: true, page, runId, authorization: scenario.authorization },
47
62
  resolve(values.get("--output") ?? "artifacts/native-chatgpt/evidence.json"));
48
63
  console.log(`Native ChatGPT journey ${evidence.status}. Evidence written privately.`);
49
64
  if (evidence.status !== "passed") process.exitCode = evidence.status === "failed" ? 1 : 2;
50
- } finally { await context.close(); }
65
+ } finally { await session.close(); }
51
66
  }
52
67
 
53
68
  main().catch((error) => {
54
- const categories = new Set(["explicit-opt-in-required", "invalid-run-options", "owned-scenario-required", "human-setup-required", "optional-playwright-dependency-required", "headed-browser-unavailable"]);
69
+ const categories = new Set(["explicit-opt-in-required", "invalid-run-options", "owned-scenario-required", "human-setup-required", "optional-playwright-dependency-required", "headed-browser-unavailable", "browser-owner-confirmation-required", "loopback-browser-endpoint-required", "owned-browser-attachment-unavailable"]);
55
70
  console.error(`Native ChatGPT journey blocked: ${categories.has(error.message) ? error.message : "journey-unavailable"}`);
56
71
  process.exitCode = 2;
57
72
  });
@@ -0,0 +1,83 @@
1
+ // The owner exposes this local browser interface; this module never launches,
2
+ // authenticates, exports storage, or bypasses a browser challenge.
3
+ export function validateBrowserAttachment(endpoint, browserOwner, accountOwner) {
4
+ if (typeof accountOwner !== "string" || !accountOwner.trim() || browserOwner !== accountOwner) throw new Error("browser-owner-confirmation-required");
5
+ if (typeof endpoint !== "string" || endpoint.length > 256 || !/^ws:\/\/127\.0\.0\.1:[0-9]+\/devtools\/browser(?:\/[A-Za-z0-9-]+)?$/.test(endpoint)) throw new Error("loopback-browser-endpoint-required");
6
+ let url;
7
+ try { url = new URL(endpoint); }
8
+ catch { throw new Error("loopback-browser-endpoint-required"); }
9
+ if (Number(url.port) < 1024 || Number(url.port) > 65535 || !url.port || url.pathname.length > 150) throw new Error("loopback-browser-endpoint-required");
10
+ return url.href;
11
+ }
12
+
13
+ export async function attachOwnedBrowser({ chromium, endpoint, browserOwner, accountOwner }) {
14
+ const address = validateBrowserAttachment(endpoint, browserOwner, accountOwner);
15
+ const transport = browserTransport(address);
16
+ let browser;
17
+ let page;
18
+ try {
19
+ // Supported Playwright transport seam: Node's WebSocket rejects redirects,
20
+ // while Playwright's URL connector follows them. Keep browser defaults.
21
+ transport.open();
22
+ browser = await chromium.connectOverCDP(transport, { timeout: 30_000, noDefaults: true });
23
+ const contexts = browser.contexts();
24
+ if (contexts.length !== 1) throw new Error("owned-browser-context-required");
25
+ // A new tab shares the owner's authorized session without selecting,
26
+ // navigating, closing, or retaining any pre-existing private tab.
27
+ page = await contexts[0].newPage();
28
+ await page.goto("https://chatgpt.com/", { waitUntil: "domcontentloaded", timeout: 30_000 });
29
+ return { page, async close() {
30
+ try { await page.close({ runBeforeUnload: false }); }
31
+ finally { await browser.close(); } // CDP-connected close disconnects, not browser termination.
32
+ } };
33
+ } catch {
34
+ try { await page?.close({ runBeforeUnload: false }); } catch { /* redacted */ }
35
+ try { await browser?.close(); } catch { /* redacted */ }
36
+ transport.close();
37
+ throw new Error("owned-browser-attachment-unavailable");
38
+ }
39
+ }
40
+
41
+ // Only JSON framing for Playwright's public ConnectOverCDPTransport interface.
42
+ // Playwright owns all CDP/browser semantics. No messages are retained or logged.
43
+ function browserTransport(address) {
44
+ let socket;
45
+ let ended = false;
46
+ const pending = [];
47
+ let pendingBytes = 0;
48
+ const finish = () => {
49
+ if (ended) return;
50
+ ended = true;
51
+ pending.length = 0;
52
+ pendingBytes = 0;
53
+ transport.onclose?.("browser-connection-closed");
54
+ };
55
+ const transport = {
56
+ open() {
57
+ socket = new WebSocket(address);
58
+ socket.onopen = () => {
59
+ try { for (const message of pending) socket.send(message); pending.length = 0; pendingBytes = 0; }
60
+ catch { transport.close(); finish(); }
61
+ };
62
+ socket.onmessage = (event) => {
63
+ try {
64
+ if (typeof event.data !== "string" || Buffer.byteLength(event.data) > 16 * 1024 * 1024) throw new Error("bounded-message-required");
65
+ transport.onmessage?.(JSON.parse(event.data));
66
+ } catch { transport.close(); finish(); }
67
+ };
68
+ socket.onerror = finish;
69
+ socket.onclose = finish;
70
+ },
71
+ send(message) {
72
+ const encoded = JSON.stringify(message);
73
+ if (ended || Buffer.byteLength(encoded) > 16 * 1024 * 1024) throw new Error("bounded-message-required");
74
+ if (socket.readyState === WebSocket.CONNECTING) {
75
+ if (pending.length >= 64 || pendingBytes + Buffer.byteLength(encoded) > 16 * 1024 * 1024) throw new Error("bounded-message-required");
76
+ pending.push(encoded);
77
+ pendingBytes += Buffer.byteLength(encoded);
78
+ } else socket.send(encoded);
79
+ },
80
+ close() { socket?.close(); },
81
+ };
82
+ return transport;
83
+ }
package/chatgpt/setup.mjs CHANGED
@@ -49,7 +49,7 @@ async function main() {
49
49
  }
50
50
  await chmod(profile, 0o700);
51
51
  let context;
52
- try { context = await chromium.launchPersistentContext(profile, { headless: false, ...(channel === "chromium" ? {} : { channel }) }); }
52
+ try { context = await chromium.launchPersistentContext(profile, { headless: false, chromiumSandbox: true, ...(channel === "chromium" ? {} : { channel }) }); }
53
53
  catch { throw new Error("headed-browser-unavailable"); }
54
54
  const terminal = createInterface({ input: process.stdin, output: process.stdout });
55
55
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/testing",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
4
4
  "description": "MCP integration and semantic testing helpers",
5
5
  "license": "MIT",
6
6
  "repository": {