diff --git a/CHANGELOG.md b/CHANGELOG.md index 1873539..ba4c0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.0] - 2026-09-05 + +### Added + +- **The per-repo config is read from `.dz/ui-debugger/ui-debugger-mcp.json` + first**, falling back to the root `.ui-debugger-mcp.json`. A repo that has + consolidated its agent config under `.dz/` now boots the tool unchanged + instead of failing to find a config at all. The two-candidate order lives in + one place — `CONFIG_CANDIDATES` in `src/config/load.ts`, reached through + `resolveConfigPath(cwd)` by every read and write. + + Resolution is fail-fast in both directions: `.dz/` wins when both files + exist, and a *bad* `.dz/` copy raises `ConfigError` rather than silently + falling through to the root file. A fresh `init` writes to `.dz/` when the + repo already has that directory, and to the root file otherwise. + + This is the release the platform side of the `.dz/` migration waits on + (developerz-ai/developerz.ai#2964, epic #2958): until it is on npm, a + consumer pinned to `@latest` gets 1.8.0, which knows only the root path. + +- `ActResult.navigated`, plus a note on the step that caused it, fed by a new + optional adapter method. A full-document load wipes every bit of in-page + state and nothing on screen distinguishes that from "my click did nothing" — + the driver used to read a reloaded page as an unchanged one and report the + reload-causing action as a success. + +### Fixed + +- `biome.json` no longer carries `"!**/tmp"` in `files.includes`. That pattern + is matched against the **absolute** path, so a checkout under `/tmp/...`, + `~/tmp/project`, or any container workdir with a `tmp` component matched at + the path root and biome pruned the entire tree before reaching the repo. + +- **A rejected login now reports in seconds, not after the whole 30s budget.** + The no-`expect` proof-of-signin waits for the post-submit navigation to settle + before reading the URL that decides — but it was given the ENTIRE remaining + login budget for a wait whose result it discards. A page that never reaches + network-idle is the normal shape of a *rejected* login (the credentials POST + comes back 401 and nothing navigates), so the one case the check exists to + catch was also the slowest to report. The settle wait is now a bounded + `NAVIGATION_SETTLE_MS` slice, still shortened further by a caller with less + budget left. + + This is also why CI was red on `main`: `session-builder.test.ts`'s + wrong-credentials story sets its own ceiling to the login budget, so on any + runner where the idle wait ran to term the `AuthError` arrived after the + harness had already given up. The test was structurally unable to observe the + behaviour it asserts and timed out at exactly 30,000ms (run 33647731476, + commit 06ec3a07). No test was modified — the code was. + ## [1.8.0] - 2026-07-30 ### Changed diff --git a/package.json b/package.json index a6a5dfe..e017c50 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@developerz.ai/ui-debugger-mcp", - "version": "1.8.0", + "version": "1.9.0", "description": "Autonomous UI debugging MCP server. Give a goal; a fast agent drives the browser/desktop, finds bugs + visual issues, and reports back.", "mcpName": "io.github.developerz-ai/ui-debugger-mcp", "license": "MIT", diff --git a/server.json b/server.json index 4c963f0..86799cd 100644 --- a/server.json +++ b/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.developerz-ai/ui-debugger-mcp", "description": "Autonomous UI debugging MCP server: an agent drives the browser/desktop, reports bugs + UX issues.", - "version": "1.8.0", + "version": "1.9.0", "repository": { "url": "https://github.com/developerz-ai/ui-debugger-mcp", "source": "github" @@ -12,7 +12,7 @@ { "registryType": "npm", "identifier": "@developerz.ai/ui-debugger-mcp", - "version": "1.8.0", + "version": "1.9.0", "transport": { "type": "stdio" }, diff --git a/src/index.ts b/src/index.ts index d4400da..67069fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ // Public package surface. Implementation lands incrementally — see idea/ for design. export const NAME = 'ui-debugger-mcp'; -export const VERSION = '1.8.0'; +export const VERSION = '1.9.0'; diff --git a/src/services/login.test.ts b/src/services/login.test.ts index 72f8ff2..2e3eabe 100644 --- a/src/services/login.test.ts +++ b/src/services/login.test.ts @@ -110,7 +110,7 @@ test('samePage ignores the query a login flow appends', () => { // --- performLogin ------------------------------------------------------------- /** A trace entry: what the login asked the adapter to do. */ -type Call = { fn: string; arg?: unknown; text?: string }; +type Call = { fn: string; arg?: unknown; text?: string; timeout?: number }; interface FakeOptions { /** Selectors that resolve to a node; everything else resolves to null. */ @@ -155,7 +155,7 @@ function fakeAdapter(options: FakeOptions = {}): { adapter: Adapter; calls: Call readState: async () => [], screenshot: async () => new Uint8Array(), waitFor: async (opts) => { - calls.push({ fn: 'waitFor', arg: opts.query ?? 'networkIdle' }); + calls.push({ fn: 'waitFor', arg: opts.query ?? 'networkIdle', timeout: opts.timeout }); if (options.waitFails) throw new Error('never became visible'); }, console: async () => [], @@ -196,6 +196,35 @@ test('performLogin opens the login page, fills every field, and submits', async expect(steps.every((s) => s.ok)).toBe(true); }); +test('the no-`expect` settle wait is a SLICE of the login budget, not all of it', async () => { + const { adapter, calls } = fakeAdapter(); + await performLogin(adapter, OPTS); + + // The proof-of-signin path with no `expect` waits for the post-submit + // navigation to settle, then reads the URL that actually decides. That wait's + // result is DISCARDED, so its only job is to let a redirect land — and a page + // that never reaches network-idle is the NORMAL shape of a rejected login (the + // credentials POST answered 401, nothing navigated). + // + // RED-WHEN-WIDENED: pass the whole budget here again and a wrong password costs + // 30s of silence, and `session-builder.test.ts`'s wrong-credentials story — whose + // own ceiling IS the login budget — becomes unobservable and times out at exactly + // 30,000ms, as it did on main (run 33647731476, commit 06ec3a07). + const settle = calls.find((c) => c.fn === 'waitFor' && c.arg === 'networkIdle'); + expect(settle).toBeDefined(); + expect(settle?.timeout).toBe(5_000); +}); + +test('a caller with less budget left still shortens the settle wait', async () => { + const { adapter, calls } = fakeAdapter(); + await performLogin(adapter, { ...OPTS, timeoutMs: 1_200 }); + + // capWait only ever SHORTENS: the slice is a ceiling, never a floor a spent + // run has to sit through. + const settle = calls.find((c) => c.fn === 'waitFor' && c.arg === 'networkIdle'); + expect(settle?.timeout).toBe(1_200); +}); + test('performLogin clears each field before typing (type appends, it does not fill)', async () => { const { adapter, calls } = fakeAdapter(); await performLogin(adapter, OPTS); diff --git a/src/services/login.ts b/src/services/login.ts index 39b98a4..fe5192d 100644 --- a/src/services/login.ts +++ b/src/services/login.ts @@ -39,6 +39,31 @@ import type { Step } from '../findings/schema.js'; /** Whole-login default budget; the run's remaining cap may only shorten it. */ const LOGIN_TIMEOUT_MS = 30_000; +/** + * How long the no-`expect` proof waits for the post-submit navigation to SETTLE, + * before reading the URL that actually decides whether the login took. + * + * A slice, deliberately not the whole login budget. The settle wait's own result + * is DISCARDED (`.catch(() => undefined)`) — it exists only to let a redirect + * land before the URL is read, and a page that never reaches network-idle is the + * normal shape of a REJECTED login: the credentials POST comes back 401, the app + * renders an error in place, and nothing navigates. Spending the full budget + * there meant the one case this check exists to catch was also the slowest to + * report, at 30s of silence per wrong password. + * + * That is not hypothetical. `session-builder.test.ts`'s "a persona whose + * credentials are wrong fails the run instead of opening it signed out" sets its + * own ceiling to `LOGIN_TIMEOUT_MS`, so on any runner where the idle wait ran to + * term the AuthError arrived after the harness had already given up — the test + * was structurally unable to observe the behaviour it asserts, and it timed out + * at exactly 30,000ms on `main` (run 33647731476, commit 06ec3a07). + * + * Five seconds is chosen against what it is waiting for — one redirect hop on an + * app that has already answered the login POST — not against the runner. A + * caller with less than that left still shortens it via `capWait`. + */ +const NAVIGATION_SETTLE_MS = 5_000; + /** What `start_debug({as})` resolved to — the persona plus the key that named it. */ export interface ResolvedAuth { /** The `as` key, as the caller typed it — every failure names it. */ @@ -232,7 +257,9 @@ async function assertSignedIn( return; } - await adapter.waitFor({ networkIdle: true, timeout: budget }).catch(() => undefined); + await adapter + .waitFor({ networkIdle: true, timeout: capWait(NAVIGATION_SETTLE_MS, budget) }) + .catch(() => undefined); const after = await currentUrl(adapter); if (after === null) { throw new AuthError(