Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -12,7 +12,7 @@
{
"registryType": "npm",
"identifier": "@developerz.ai/ui-debugger-mcp",
"version": "1.8.0",
"version": "1.9.0",
"transport": {
"type": "stdio"
},
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
33 changes: 31 additions & 2 deletions src/services/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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 () => [],
Expand Down Expand Up @@ -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);
Expand Down
29 changes: 28 additions & 1 deletion src/services/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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(
Expand Down
Loading