From 45abb1a8418eab9e5746c73548962e0691136664 Mon Sep 17 00:00:00 2001 From: ivndev001 Date: Sat, 5 Sep 2026 11:03:53 +0000 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20release=20v1.9.0=20=E2=80=94=20.dz?= =?UTF-8?q?/=20dual-read=20config=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Bumps the package to 1.9.0 across all four places that carry the version (`package.json`, `server.json` top-level + `packages[0]`, and the exported `VERSION` in `src/index.ts`), and adds the 1.9.0 CHANGELOG entry covering everything merged since v1.8.0. ## Why v1.8.0 (2026-07-31) is still `dist-tags.latest` on npm and knows only `CONFIG_FILENAME = '.ui-debugger-mcp.json'` — it throws when that file is absent. The dual-read landed on `main` in #65 on 2026-09-02 and has never been published, so nothing that installs `@latest` can read a `.dz/` config. That unpublished release is the explicit, currently-unmet precondition on developerz-ai/developerz.ai#3239 (closes its #2964, epic #2958): merging the platform side first would write the config only to `.dz/`, which 1.8.0 never opens, breaking `/ui-sweep` and any customer repo declaring the ui-debugger tool service. Cutting this release is what unblocks that merge. ## Changes - `package.json` 1.8.0 -> 1.9.0 - `server.json` — both `version` fields (top-level and `packages[0].version`), which PUBLISHING.md requires be bumped alongside package.json because the MCP registry resolves the npm package by the version named here - `src/index.ts` `VERSION` 1.8.0 -> 1.9.0 — caught by the repo's own "VERSION matches package.json" test, which failed until this line moved - `CHANGELOG.md` — 1.9.0 entry: the `.dz/` dual-read (#65), the `ActResult.navigated` full-document-load signal (#62), and the biome absolute-path prune fix (#63) Minor, not patch: the dual-read is a backwards-compatible feature — the root file still resolves, and `.dz/` only wins where it exists. ## Verification Run on the branch, after `bun install --frozen-lockfile`: - `bun run lint` — 157 files checked, clean - `bun run typecheck` — clean - `bun run build` — clean - `bun test` — 1171 pass / 10 skip / 0 fail (1181 across 75 files) - `node -e "JSON.parse(...)"` on both `server.json` and `package.json` The one failure this change had to fix was found by the gate, not by reading: `bun test` reported `(fail) VERSION matches package.json` until `src/index.ts` was bumped. No test was modified. ## Post-merge Publishing is a human step and deliberately not automated here: cut a GitHub Release `v1.9.0` (or Actions -> release -> Run workflow). `release.yml` publishes to npm over OIDC trusted publishing — no token — and then registers the version with the MCP registry in the same job. Confirm `npm view @developerz.ai/ui-debugger-mcp dist-tags` reads 1.9.0 and that the published `src/config/load.ts` carries `CONFIG_CANDIDATES` before merging developerz-ai/developerz.ai#3239. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NCugazR85MEY7B2KVpcf7F --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ package.json | 2 +- server.json | 4 ++-- src/index.ts | 2 +- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1873539..7a83809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,39 @@ 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. + ## [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'; From e032d66676451ad851daefe74490dc25e139fe74 Mon Sep 17 00:00:00 2001 From: ivndev001 Date: Sat, 5 Sep 2026 11:24:44 +0000 Subject: [PATCH 2/2] fix(auth): a rejected login reports in seconds, not after the whole 30s budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What `assertSignedIn`'s no-`expect` path gives its post-submit settle wait a bounded `NAVIGATION_SETTLE_MS` (5s) slice instead of the entire remaining login budget. `capWait` still shortens it further for a caller with less left, so this is a ceiling and never a floor. ## Why The settle wait's own result is DISCARDED (`.catch(() => undefined)`) — it exists only to let a redirect land before the URL that actually decides is read. And a page that never reaches network-idle is not an edge case: it is the NORMAL shape of a rejected login. The credentials POST comes back 401, the app renders an error in place, nothing navigates. So the one case this check exists to catch was also the slowest to report, at 30 seconds of silence per wrong password. ## This is why CI is red on main `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 `STORY_TIMEOUT_MS` (30_000) — the SAME number as `LOGIN_TIMEOUT_MS`. On a runner where the idle wait ran to term, the `AuthError` therefore arrived after the harness had already given up: the test was structurally unable to observe the behaviour it asserts, on any machine slow enough to reach the timeout. That is not a flake. Run 33647731476 on `main` (commit 06ec3a07, the commit that landed the `.dz/` dual-read) fails on exactly this test at exactly 30,002ms, and PR #67 inherited it. It passes locally because a fast box reaches network-idle in milliseconds and never touches the ceiling — the test's own timing margin was zero, and which side of it you land on is a property of the runner. No test was modified to make this pass. The prod timing was wrong; the test was right and could not run. ## Changes - `src/services/login.ts`: `NAVIGATION_SETTLE_MS = 5_000`, applied through `capWait`. The constant's docstring records the reasoning and the failing run so the next reader does not re-derive it. - `src/services/login.test.ts`: the fake adapter records `opts.timeout` on each `waitFor`, and two new tests pin the slice — one that the settle wait is 5s and not the budget, one that a 1.2s caller still shortens it to 1.2s. - `CHANGELOG.md`: under 1.9.0. ## Verification - Mutation-proven: restoring `timeout: budget` reddens the new slice test (22 pass / 1 fail); restored, 23/23. - Timing proof against a promise that never settles: the bounded wait returns in ~5.0s, inside the 30s harness ceiling; the old path spent the full 30s. - `bun run lint` clean, `bun run typecheck` clean. - `bun test`: 1173 pass / 10 skip / **0 fail** (1183 across 75 files). The wrong-credentials story now runs; the previous run on this branch was 1171/0 with that test never reaching its assertion on CI. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 17 +++++++++++++++++ src/services/login.test.ts | 33 +++++++++++++++++++++++++++++++-- src/services/login.ts | 29 ++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a83809..ba4c0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `~/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/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(