Skip to content

ci: detection proof - poison corpus counter-evidence for the matrix - #15145

Closed
benjcooley wants to merge 160 commits into
benjcooley/ecosystem-matrixfrom
benjcooley/matrix-detection-proof
Closed

ci: detection proof - poison corpus counter-evidence for the matrix#15145
benjcooley wants to merge 160 commits into
benjcooley/ecosystem-matrixfrom
benjcooley/matrix-detection-proof

Conversation

@benjcooley

Copy link
Copy Markdown
Contributor

Counter-evidence for #15110: a green weekly verdict is only meaningful if poison reliably reds it. detection-proof/corpus/ holds seven synthetic packs, each broken in exactly one measured way, plus a clean control; the new matrix-detection-proof job (no corpus/cache dependency, fixture checked in, ~2 min) passes only if:

  1. verify_detection.py sees every channel fire with its exact poison message — 12 assertions, including 7/7 rows written despite universal breakage and the clean control staying fully clean (specificity), and
  2. summarize_matrix.py exits FAIL on the poison corpus (loads 85.7% < 95% floor; worst op 85.7% < 99% floor).
poison pack breaks detected as
poison-load-throw throws at import loadedOk 0 + message (gated)
poison-regdef-throw beforeRegisterNodeDef throws hookErrors
poison-customnodes-throw registerCustomNodes throws hookErrors
poison-op-break onNodeCreated throws load/addNode op errs (gated)
poison-serialize-throw onSerialize throws serialize op err (gated)
poison-desync pushes an unregistered widget signature drift (wn, counts)
clean-control nothing fully clean row

Building the proof surfaced two blind channels, handled honestly rather than papered over:

  • Throwing extension hooks are invisible to registration: extensionService.invokeExtensions* catches per-extension and console.errors, so hook throws could never fail the registerNodeDef field (proven: the hook ran — its prototype side effects fired later — while its throw was swallowed). The runner now records the containment signature as a hookErrors row field (deduped, capped 20, radar not gate) and the summary gains a contained hook errors line.
  • The store-vs-live desync comparator is dormant in this harness (needs Vue widget-store wiring; r/st read n/a even under MATRIX_VUE=1). Widget mutations still surface through the serialized signature, which is what the proof pins.

Also: the poison fixture is corpus data, not repo code (eslint/oxlint/knip ignores), and generated src/__ecs_matrix__ build output is now excluded from vue-tsc so a locally built fixture cannot fail pnpm typecheck.

Local evidence: DETECTION PROOF PASSED: every channel fired, control clean, 7/7 rows; summarizer exit 1 with VERDICT: FAIL - entry JS loads clean 85.7% < 95%; every operation clean (worst: load) 85.7% < 99%. The PR run's matrix-detection-proof job is the CI evidence.

Stacked on #15110.

dante01yoon and others added 24 commits August 11, 2026 22:27
## Summary

Ports the repeated-query regression coverage identified in superseded PR
#15022 after #15026 merged the legacy `/login` redirect. Original
coverage and test approach are attributed to #15022 / @dante01yoon.

## Changes

- **What**: Extend the existing real-router redirect test to verify
`/login?campaign=one&campaign=two` reaches `cloud-login` with
`query.campaign` equal to `['one', 'two']`.

## Review Focus

Confirm the assertion covers Vue Router's repeated-query parsing while
retaining the existing query, hash, and no-loop coverage.

## Validation

- `pnpm test:unit
src/platform/cloud/onboarding/onboardingCloudRoutes.test.ts` — 11 tests
passed
- `pnpm exec oxfmt --check
src/platform/cloud/onboarding/onboardingCloudRoutes.test.ts` — passed
- `git diff --check origin/main...HEAD` — passed
- Commit hooks: typecheck, Oxlint, ESLint, and formatting passed; push
hook Knip passed

## Screenshots (if applicable)

Not applicable; this PR is test-only.

Co-authored-by: Amp <amp@ampcode.com>
Automatic SHA bump — `cursor-review.yml` was updated in
`Comfy-Org/github-workflows` at
[`0096e1a`](Comfy-Org/github-workflows@0096e1a).
_Opened by the `bump-cursor-review-callers` workflow._

Co-authored-by: cloud-code-bot[bot] <234529496+cloud-code-bot[bot]@users.noreply.github.com>
## Summary

Child of #15050. Give every root Vitest test the same named fake-clock
baseline and express scenario time as relative movement from that
baseline.

```ts
const TEST_SYSTEM_TIME = Date.parse('2024-06-15T12:00:00Z')
```

- reduces test-local `vi.setSystemTime()` calls from 44 to 7
- replaces arbitrary Unix-adjacent timestamps and unrelated calendar
dates with `Date.now()`-relative fixtures
- uses `vi.advanceTimersByTime()` when elapsed time, rather than a
wall-clock jump, is the behavior under test
- retains six non-firing wall-clock jumps where advancing timers would
execute scheduled work and change the scenario
- retains one explicit July 2026 clock for calendar-boundary filtering
- makes a template-freshness fixture relative to the baseline instead of
depending on the actual current date
- aligns a module-scoped workspace-token expiry with the deterministic
test clock

## Baseline choice

The most frequent exact timestamp was `1_000_000` ms (five calls),
equivalent to `1970-01-01T00:16:40Z`. That value was common but
arbitrary.

June 15, 2024 was the most common meaningful calendar anchor: four
date-oriented setups used that day. Noon UTC is explicit, stable, and
compatible with the root project's existing `TZ=UTC` configuration.

## Results

| Metric | Before | After | Change |
|---|---:|---:|---:|
| `vi.setSystemTime()` calls | 44 | 7 | −37 (−84%) |
| Files containing those calls | 14 | 3 | −11 (−79%) |
| Root tests passed | 15,892 | 15,892 | No regression |
| Root tests skipped | 8 | 8 | No change |

The first full run exposed one hidden real-date dependency in
`templateRankingStore.test.ts`: a hardcoded January 2024 template was
only “very old” relative to the present day. It is now represented as
three years before `Date.now()`, matching the behavior the assertion
describes.

## Validation

- `pnpm test:unit`: 1,159 files passed; 15,892 tests passed; 8 skipped
- `pnpm lint`
- `pnpm typecheck`
- oxfmt and type-aware Oxlint on all changed files
- pre-push `pnpm knip --cache`

Co-authored-by: Amp <amp@ampcode.com>
## Summary

Child of #15052. Give every test a clean localStorage baseline through
the shared Vitest setup.

```ts
beforeEach(() => {
  globalThis.localStorage?.clear()
})
```

The optional access keeps the shared setup compatible with the website
and object-parser projects, which use Vitest's Node environment and do
not expose localStorage.

## Audit results

| Classification | Calls | Outcome |
|---|---:|---|
| Test-boundary setup/teardown | 50 | Removed as globally redundant |
| Intra-test fast-check run isolation | 1 | Retained |
| **Total audited** | **51** | **30 test files** |

The retained call separates generated command sequences and shrink
attempts inside a single Vitest test. A global `beforeEach` cannot
replace it.

The cleanup also removes empty hooks, stale comments, and unused
lifecycle imports. Net diff: 10 additions and 106 deletions across 31
files.

## Validation

| Project | Test files | Passed | Skipped | Failed |
|---|---:|---:|---:|---:|
| Root | 1,159 | 15,896 | 8 | 0 |
| Desktop UI | 16 | 125 | 0 | 0 |
| Website | 41 | 387 | 0 | 0 |
| Object info parser | 4 | 33 | 0 | 0 |

Also passed:

- `pnpm typecheck`
- `pnpm lint` (six pre-existing warnings, zero errors)
- oxfmt and type-aware Oxlint on all changed files
- pre-push `pnpm knip --cache`

Co-authored-by: Amp <amp@ampcode.com>
…s '10') (#15043)

## Summary

Once a node's price badge evaluation fails on a stringified widget
value, correcting the value to its numeric equivalent cannot recover the
badge, because `safeValueForSig` gives `10` and `'10'` the same cache
signature and the failed-empty label keeps being served.

## Changes

- **What**: `safeValueForSig` tags primitives with their runtime type
(`number:10` vs `string:10`) and prefixes the JSON/fallback paths
(`json:`/`fallback:`), so a type change is a signature change and
triggers re-evaluation. The null/undefined representation, failure
caching, and scheduling are unchanged; an unchanged value+type still
hits the cache (pinned by a guard test).

## Review Focus

Tests go through the public `useNodePricing` surface only. Mutation
proof: reverting the signature change re-reds exactly the recovery test
while the retry-spam guard stays green.

## Red-green proof

| Phase | Commit | Unit |
| ----- | ------ | ---- |
| Red |
[`45e9510480`](45e9510)
`test: add failing coverage for price badge cache signature type
collision` | [failed (1 failed / 15821 passed, the new recovery
test)](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/31477496409/job/93734556154)
|
| Green |
[`8267022f15`](8267022)
`fix: include value type in price badge cache signature` |
[passed](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/31479991708/job/93742543093)
|

The red run proves the test fails without the fix; the green run proves
the fix resolves it. Same tests, unmodified, in both runs. E2E
intentionally omitted: price badges require cloud API node definitions
with no existing E2E fixture; the defect is cache-layer logic fully
observable in unit tests.

Fixes #14540
…type mode (#14848)

## Summary

Fixes a bug where a legacy bare-tagged model asset silently disappears
from the model library sidebar once the backend advertises `model_type:`
support, instead of falling back to legacy bare-tag grouping.

_Recreated from #14217 — same change, opened directly under my own
account for tracking. The High finding from Cursor's panel on that PR is
already folded into this diff._

## Changes

- **What**: `modelFolderFromTag` returned `undefined` for any tag that
didn't carry the `model_type:` prefix as soon as `modelTypeMode` was
`true`. `buildModelBuckets` treats an asset whose tags resolve to no
folder as "uncategorized" and drops it with a `console.warn`, so a model
still carrying a legacy bare tag (e.g. `checkpoints` instead of
`model_type:checkpoints`) vanished from the sidebar entirely on a
`model_type:`-capable backend. Changed in
`src/platform/assets/services/assetService.ts`.
- **Breaking**: none

## How

The fix deletes the local `modelFolderFromTag` helper and buckets via
`getAssetCategories` from `assetMetadataUtils`, which already implements
exactly the intended semantics and is already used by the other asset
surfaces: `model_type:*` values are authoritative when present, and an
asset with no `model_type:` tag still routes by its bare tags (with
namespace residue filtered out). This makes the sidebar consistent with
the rest of the asset code rather than carrying a second, subtly
different grouping rule.

A side effect worth calling out: because `getAssetCategories` returns
`model_type:` values *alone* when an asset has any, an asset covered by
`model_type:` tags can no longer be cross-listed into a second folder by
a leftover bare-tag twin from a partial re-tagging. The e2e suite covers
that case (the "mid-retag twin" scenario).

Added an e2e scenario, `Model library sidebar - asset mode with a legacy
bare tag`, in
`browser_tests/tests/sidebar/modelLibraryAssetMode.spec.ts`, plus a
matching fixture (`MODEL_TYPE_CHECKPOINT_LEGACY_TAG` in
`browser_tests/fixtures/data/assetFixtures.ts`): a bare-tagged asset
that still carries a `loader_path`, walked with
`supports_model_type_tags: true`, asserting it still renders in the
`checkpoints` folder rather than being dropped.

## Review Focus

- This targets `main` directly. The stack it was originally written on
top of (#13574) has since merged, so the diff here is just the three
files.
- Coverage for the regression lives in the e2e spec rather than a unit
test — the unit-level version was dropped as duplicating it. Flagging
that explicitly since it is a judgement call about test placement.
- Verified against `main` that the bug is still live:
`modelFolderFromTag` on `main` still returns `undefined` for bare tags
whenever `modelTypeMode` is true.
- Checked that the existing `assetService.test.ts` cases on `main`
remain valid under the new path: `getBareTagCategories` filters out the
reserved `models` tag, so the "drops uncategorized model assets with a
warning" case still drops its `tags: ['models']` asset; the bare-tag
grouping cases all run with `supports_model_type_tags = false`, an
unchanged path.

## Test plan

- [ ] `pnpm typecheck` — **not run**: no Node/pnpm toolchain provisioned
on the host I authored this on. Relying on CI.
- [ ] `pnpm exec vitest run
src/platform/assets/services/assetService.test.ts` — **not run**, same
reason. Compatibility with the existing cases was verified by reading
them (see Review Focus).
- [x] Static check that the refactor is complete:
`MODEL_TYPE_TAG_PREFIX` import removed with no remaining uses in the
file, `modelFolderFromTag` has zero references repo-wide,
`getAssetCategories` is exported from `assetMetadataUtils` on `main`.
- [x] The identical three-file diff passed a full CI run on #14217
(Playwright 1759 passed / 0 failed / 1 flaky; Codecov reported all
modified lines covered).

---

**Review coverage note:** no Cursor panel has run on this PR — verified
by querying for the panel review itself rather than inferring from an
empty findings list. That is a *did-not-run*, not a clean result; the
checks API reports success either way. CodeRabbit was rate-limited.
Codex did review it, and its one finding was addressed in `f16560c`.

_(An earlier version of this note blamed an org-wide Cursor outage
running to 2026-08-22. That attribution was wrong and is withdrawn —
panels have since been seen running normally elsewhere. The measured
fact, that none ran here, is unchanged.)_

---------

Co-authored-by: Austin Mroz <austin@comfy.org>
## ELI5
Think of gated model access like a locked door with one clear sign:
everyone should learn why it is locked, and the control should honestly
say where it leads. This change makes that sign work for screen readers
and keyboards, keeps unsafe destinations from appearing, and uses clear
singular or plural wording without changing the visual design.

## Motivation
Five public follow-up issues identified gaps that remained after gated
metadata was centralized: asynchronously discovered gating could be
missed by screen readers, explanation text was hover-dependent for
sighted keyboard users, external navigation exposed the wrong role,
Download could repeat the same description, and the copy hardcoded a
plural. Without this follow-up, people could receive incomplete or
misleading guidance depending on input method and runtime. Publishing it
after the parent refactor merged keeps the patch focused on
accessibility and wording while preserving the existing visual design.

## Provenance
- **Authored by:** agent-work loop
- **From:** #14360 — Gated-model hint appears asynchronously with no
screen-reader announcement; #14361 — Gated-model explanation reaches
sighted keyboard-only users through title only; #14362 — Gated-repo lock
control navigates externally but is announced as a button; #14363 —
Gated Download button carries the same description via both title and
aria-describedby; #14368 — Gated-model copy uses three phrasings of one
action, and hardcodes the plural
- **Verified:** pnpm test:unit on AccessibleTooltip, MissingModelCard,
MissingModelRow, and useMissingModelDownload: 57 passed; pnpm test:unit
on PaidTemplateBadge and LogoOverlay: 12 passed; pnpm typecheck: passed;
pnpm lint: passed; pnpm format:check: 4,616 files passed; pnpm knip:
passed; git diff origin/main...HEAD --check: passed
- **Deviations:** None

## Reviewer context
- **Type:** bug fix, covering gated-model accessibility semantics,
trusted navigation, and copy
- **Slots into:** Follow-up to #14982 — refactor(missing-model):
centralize metadata state. Because it merged before publication, this PR
targets main and contains only follow-up accessibility and copy work.

## Summary
- Politely announce gated guidance only when metadata transitions from
not gated to gated, keep warm-cache mounts silent, and clear the live
region so a later appearance can announce again.
- Reveal the gated explanation on keyboard focus, expose browser and
legacy Electron navigation as safe links with a new-tab warning, and
keep Desktop2 navigation as a button with honest fallback labeling.
- Require a trusted Hugging Face URL before showing gated controls or
descriptions, and give Download one accessible description instead of
duplicating title and aria-describedby.
- Use consistent sign-in wording and pluralize guidance from the gated
model count.

## Changes
- **What:** Updates the shared accessible-tooltip trigger pattern,
missing-model card and row behavior, English locale copy, and focused
behavioral tests.

## Review Focus
Please focus on live-region transition behavior, the trusted URL
boundary, link-versus-button semantics across runtimes, and the
single-description behavior for Download.

## Test plan
- [ ] Discover gating after mount and verify one polite announcement;
hide and re-show it to verify the status clears and the same guidance
can announce again.
- [ ] Mount with warm cached gating and verify the visible pluralized
note does not trigger the live region.
- [ ] Focus the gated access control and verify its explanation is
available without hover.
- [ ] Verify browser and legacy Electron use trusted new-tab links,
Desktop2 uses a fallback-labeled button, and untrusted URLs produce no
gated UI or Download description.
- [ ] Verify singular and plural sign-in wording and confirm Download
receives exactly one gated explanation.
- [ ] Confirm the accessibility fixes are active without a feature flag;
they correct the existing gated-model flow and preserve its visual
design.

## Screenshots (if applicable)
Not included; visual design is preserved.

Fixes #14360
Fixes #14361
Fixes #14362
Fixes #14363
Fixes #14368
…14789)

Follow-up to #14574, reviving the work from the closed #14580 rebased
onto current main.

#14574 landed the escape-then-sanitize fix so Cloud could be patched
without waiting. It left three things behind, all of which lived in
#14580 and are in no other open PR:

- the `v-html` sink in both search components
- the `sanitize` parameter, which is a complete bypass when false
- a lint guardrail to stop the sink coming back

## Changes

`highlightQuery` now returns `{ text, highlighted }[]` instead of an
HTML string, and the new `HighlightedText.vue` renders those segments as
Vue text nodes. There is no markup to escape, no sanitizer to bypass,
and no `v-html` in either the legacy or v2 search UI.

`vue/no-v-html` is set to `error` for `src/components/searchbox/**`.
Verified it fires: a probe file with `v-html` produces `'v-html'
directive can lead to XSS attack`.

Removing the string API also drops the `dompurify` dependency from
`shared-frontend-utils`.

## Why this rather than keeping the escaping

The original defect was not a missing sanitizer — `highlightQuery`
already called DOMPurify. It was an early return above the call.
Escaping correctly is a property that has to hold at every future edit;
not building HTML is a property that holds structurally.

## Testing

- 204 tests pass across `formatUtil` and `src/components/searchbox`
- eslint clean, oxfmt clean
- rebase conflict in `formatUtil.ts` resolved in favour of the segment
API, which supersedes the escape-and-sanitize string it replaces

## Note on scope

This closes the searchbox sink. Two sibling sinks of the same class are
handled in #14772 (property-name `innerHTML`, and Open Image blob
typing) — neither is Vue, so this lint rule does not cover them.

Original work by @huang47 in #14580.

---------

Co-authored-by: ShihChi Huang <shh@theonlyperson.com>
Co-authored-by: t <t@t.t>
…state readers (#15044)

## Summary

Reuse Vue Router's existing query parser for `?ff=` instead of parsing
`window.location.search` independently with `URLSearchParams`.

## Changes

- Replace the local `URLSearchParams` reader in
`sessionFeatureFlagOverride.ts` with `parseQuery`.
- Preserve pre-router synchronous access, repeated `?ff=` parameters,
nameless clearing, typed values, persistence, and employee gating.
- No new modules, router hooks, state, dependencies, or tests.

Fixes FE-1551

## Verification

- `pnpm test:unit src/utils/sessionFeatureFlagOverride.test.ts`
- `pnpm typecheck`
- `pnpm lint`
- `pnpm knip`
- `pnpm exec oxfmt --check src/utils/sessionFeatureFlagOverride.ts`

Co-authored-by: ShihChi Huang <shh@theonlyperson.com>
## Summary

A workflow whose only multiline text box is titled `Anti-prompt` had the
first-run tour's _"Describe what you want — your image gets built from
this description"_ callout spotlit on its **negative** box. The user
types "a cat" and gets everything except a cat.

Closes #14623

## Changes

- **What**: `heuristicRoles.ts` now disqualifies a prompt candidate
whose label reads as an anti-prompt, alongside the existing
`negative`/`neg`/`system`/`undesired`/`avoid` stems.

### Why not just add `anti` to the stem list

The issue suggests one more stem. That does not work as written:
`DISQUALIFYING` is matched against **normalised label words**, and
`labelWords('anti_aliasing')` is `['anti', 'aliasing']` — so a bare
`anti` stem disqualifies `anti_aliasing`, which the existing carve-out
test at `heuristicRoles.test.ts` explicitly requires to remain a valid
prompt (its message already anticipates this: _"a prefix match on
'neg'/'anti' drops the prompt step for ordinary words"_).
`startsWith('anti')` is worse — it also eats `antique`.

`anti` only names the negative box next to the noun, so the check
matches the pair off the normalised label rather than its individual
words:

```ts
const ANTI_PROMPT = /\banti ?prompts?\b/
```

Because it runs on the label after camelCase-splitting and separator
normalisation, one expression covers `Anti-prompt`, `anti prompt`,
`AntiPrompt`, `anti_prompts` and `ANTIPROMPT`, while `antique`,
`anti_aliasing` and `antialiasing strength` still qualify. It is applied
to the same label set as the existing check — node title, widget name,
subgraph port name, and the input name the node feeds — so a widget
named `anti_prompt` is caught too.

## Review Focus

- The regex is deliberately noun-adjacent rather than a stem. If you
would rather lose `anti_aliasing` as a prompt than carry a regex, say so
and I will swap it for the stem plus a deleted test case — but that
trade seems clearly worse.
- Bare `anti` as an entire label (no noun) is _not_ disqualified. It is
ambiguous and unattested; the existing `anti_aliasing` carve-out points
the same way.

## Verification

- `pnpm test:unit
src/renderer/extensions/firstRunTour/roles/heuristicRoles.test.ts` — 47
passed.
- Red-green checked: with the new guard line deleted and the new cases
kept, exactly the 6 new assertions fail (`Anti-prompt`, `anti prompt`,
`AntiPrompt`, `ANTIPROMPT`, `anti_prompts`, and the `anti_prompt`
widget); restoring it turns them green with no other test moving.
- `pnpm format:check`, `pnpm lint`, `pnpm typecheck` — all clean.

Not verified: no browser run. This is pure label-matching logic with
unit coverage on both the disqualifying and the must-not-disqualify
side, so a live tour run would not add signal beyond what the tests
assert.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Unit CI intermittently fails with **every test passing**:

```
 Test Files  1094 passed (1097)
      Tests  14943 passed | 8 skipped (14988)
     Errors  21 errors
```

```
⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯
Vitest caught 21 unhandled errors during the test run.
⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
EnvironmentTeardownError: [vitest-worker]: Closing rpc while "onUserConsoleLog" was pending
This error originated in "src/workbench/extensions/manager/composables/nodePack/usePacksSelection.test.ts"
```

plus a wall of unattributed `connect ECONNREFUSED ::1:3000` on stderr.
Vitest
exits non-zero on unhandled errors even when no test failed, which
evicts PRs
from the merge queue while their own checks read green.

This is instance two of the class fixed in #14648 (a late `console.warn`
from
`TabErrors.test.ts` firing after teardown).

## Where `localhost:3000` actually comes from

Not from any mock. `http://localhost:3000` is **vitest's default URL for
the
happy-dom environment** (`vitest/dist/chunks/index…js`: `url:
happyDOM.url ||
"http://localhost:3000"`). Every relative `fetch('/api/…')` in a unit
test
resolves against it and becomes a real TCP connection to a port nothing
is
listening on.

An earlier guess pinned this on `src/stores/assetsStore.test.ts`, which
mocks
`internalURL`/`apiURL` to the same string. That file is a red herring -
run on
its own it issues zero requests. Instrumenting `globalThis.fetch` across
the
whole suite shows exactly **41** real requests, all to
`http://localhost:3000/api/system_stats`, from three files:

| file | requests |
| --- | --- |
| `usePacksSelection.test.ts` | **21** |
| `TabErrors.test.ts` | 15 |
| `ErrorGroupList.test.ts` | 5 |

21 requests from `usePacksSelection.test.ts`, 21 unhandled errors
attributed to
`usePacksSelection.test.ts`. One per test in the file.

## The chain

`usePacksSelection.test.ts` has no network code in it. The request comes
from
three levels down, off a single `useComfyManagerStore()` in
`beforeEach`:

```
usePacksSelection.test.ts:43   useComfyManagerStore()
  comfyManagerStore.ts:199     whenever(...) fires immediately
  comfyManagerStore.ts:191     refreshInstalledList()
  comfyManagerService.ts:59    isManagerServiceAvailable()
  useManagerState.ts:40        useSystemStatsStore()
  systemStatsStore.ts:24       useAsyncState(fetchSystemStatsData, null, { immediate: true })
  api.ts:1239                  getSystemStats() -> fetch('/api/system_stats')
```

Constructing the Pinia store performs I/O. Nothing awaits it and nothing
can -
it is a side effect of store construction. The test finishes; the socket
is
still open; the connection eventually fails; `systemStatsStore` logs
`console.error('Error fetching system stats:', err)`; that console call
has to
cross the worker RPC, and by then the RPC is closing. Hence
`EnvironmentTeardownError`, once per leaked request.

## Why it survived this long

Two layers of misdirection:

1. **The blamed file contains nothing suspicious.**
`usePacksSelection.test.ts`
   is a pure composable test. Vitest is explicit that its attribution is
positional - *"It doesn't mean the error was thrown inside the file
itself,
but while it was running"* - so a late emission gets pinned on whatever
file
the worker is on at that moment. With three files leaking requests, the
name
on the failure moves between branches, and each occurrence reads as an
   unrelated one-off in someone else's test.
2. **It needs the CI runner's network to reproduce.** Locally `::1:3000`
gets an
immediate RST, so the rejection lands well inside the test. On the
runner the
connect hangs (note the `AggregateError` with both `::1` and
`127.0.0.1`),
so all 21 settle at once during teardown. See "What I could not
reproduce".

## The fix

**`vitest.setup.ts` rejects `http(s)` fetches instead of dialling out**,
and
happy-dom is configured not to load remote iframes/scripts/stylesheets.

I went for the guard rather than mocking the store in the three files,
and the
choice is worth defending because "add a global thing in test setup" is
usually
the wrong answer:

- It is **fail-closed, not fail-open.** It does not swallow an unhandled
rejection, silence output, or relax a vitest flag. It removes the
*input* to
  the race - a request that can outlive its test - and turns it into an
  immediate, in-band, attributable rejection carrying the offending URL.
- **Per-file mocks cannot hold this line.** The request is three layers
below
the call the test makes. Nothing in `usePacksSelection.test.ts` hints
that
`useComfyManagerStore()` reaches the network, so the next test to touch
that
store reintroduces the leak silently. #14648 fixed one file this way;
this is
  the same defect surfacing in a different file two weeks later.
- **Blast radius is measured, not assumed:** the entire suite makes 41
real
requests, all accidental, all from the three files above. Audited with a
  passthrough counter - 45 blocked (41 + 4 from the new guard test), **0
  escaped**. Nothing in the suite legitimately uses the network.

Tests that want network behaviour still stub `fetch` themselves; a local
`vi.stubGlobal('fetch', …)` replaces the guard.

`src/vitestSetup.test.ts` covers the guard so it cannot be dropped
silently, and
`docs/guidance/vitest.md` (globbed onto `**/*.test.ts`) explains the
failure mode
to whoever hits the error next.

The iframe/CSS/script settings close the same hole for subresources -
`ManagerSurveyDialog.test.ts` was making real requests to
`us.posthog.com` and
`useTypeformEmbed` to `embed.typeform.com`, both aborted at teardown.
Those now
fail immediately and locally.

## Evidence

- Instrumented `globalThis.fetch` over the full suite: **41 → 0** real
requests.
- 6 full runs on `main` before the change and 5 after, node 25 +
`CI=true`
(retry 2, as CI runs it): 0 unhandled errors after, all 15,000 tests
passing.
- `pnpm typecheck`, `pnpm lint`, `oxfmt` clean.

### What I could not reproduce

**The unhandled-error mode does not reproduce on my machine, before or
after.**
6 pre-change full runs produced 0 of them. I tried delaying the
rejection
(100/200/400/1500 ms), routing the requests at a blackholed address so
the
connect hangs the way it does on the runner, and node 25 - none of it
opened the
window between "console.error emitted" and "RPC closed".

So the post-change green runs are *not* the proof, and I am not claiming
a
red-to-green. The proof is upstream of the race: the suite no longer
issues a
request that can outlive its test, verified by instrumentation, so there
is
nothing left to settle late. The count and file identity (21 requests
from
`usePacksSelection.test.ts`, 21 errors attributed to it) tie the CI
failure to
the requests this PR removes.

The three test files still construct a live `systemStatsStore`; they now
get an
immediate rejection instead of a socket. Making store construction not
perform
I/O is the real cleanup, but that is a production behaviour change for
eight
consumers and does not belong in a CI-stability PR.


## How often this actually fires

Measured across the last 400 `ci-tests-unit` runs (all branches, ~27 h):

| | count |
| --- | --- |
| success | 323 |
| cancelled (concurrency) | 54 |
| failure | 11 |

Grepping each failing job log for the signature, **2 of the 11 carry
it**, both with exactly 21 errors:

-
[`30929957273`](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/30929957273)
— `1096 passed (1096)`, **zero failed test files**, red purely from 21
`EnvironmentTeardownError`. This is the pure form of the failure.
-
[`30882752230`](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/30882752230)
— 3 real failures *plus* 21 teardown errors.

So roughly **0.5 % of runs, ~0.25 % for the all-green-but-red case** —
one or two a day at current volume. Rare per run, continuous at repo
scale, and a re-run clears it, which is why it has never accumulated on
`main` and why nobody has chased it.

One correction to the earlier writeup: I asserted this evicts PRs from
the merge queue. In this window there were 4 `gh-readonly-queue/*` unit
failures and **none** of them were this mode — all were genuine
`app.test.ts` failures. The merge-queue impact is plausible but not
demonstrated; the observed hits were on ordinary branch runs.

The subresource half is directly observable. A full local run on `main`
before the change emits, among others:

```
DOMException [NetworkError]: Failed to execute "fetch()" on "Window" with URL
"https://us.posthog.com/external_surveys/survey-123": The operation was aborted.
```

That is `ManagerSurveyDialog.test.ts` reaching the real PostHog host and
being aborted at teardown — the same shape as the `/api/system_stats`
leak, via happy-dom subresource loading rather than an explicit `fetch`.
The `disableIframePageLoading` / `disableCSSFileLoading` /
`disableJavaScriptFileLoading` settings in this PR close that path.

## Interaction with #14836

#14836 enables `mockReset` / `restoreMocks` / `unstubEnvs` /
`unstubGlobals`
globally. It makes this guard more necessary rather than redundant.

`unstubGlobals` deletes a module-scope `vi.stubGlobal('fetch', ...)`
before every
test, so a file that stubbed fetch once at module scope silently falls
back to
the real one. #14836 fixes the current instances by moving them into
`beforeEach`, but nothing stops the next one from being written the old
way, and
the failure is silent: the stub is simply gone, and any negative
assertion on
the mock passes forever.

Reviewing #14836 turned up exactly that case still live in it:
`src/platform/telemetry/initHostTelemetry.test.ts` keeps its stub at
module
scope, so `expect(fetchMock).not.toHaveBeenCalled()` is vacuously true.
This
guard is what converts that class into `Blocked a real network request
to <url>`
at the call site, attributed to the test that caused it.

Either PR can land first. The guidance in `docs/guidance/vitest.md` now
points at
`beforeEach` or the test body rather than module scope, which is correct
under
both configs.

---------

Co-authored-by: t <t@t.t>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Patch version increment to 1.51.2

**Base branch:** `main`

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
## Summary

Restyles the workspace partner node governance panel to the new Partner
Models design (workspace settings parity with the platform.comfy.org
governance UX).

## Changes

- **What**:
- Replace the Unrestricted/Restricted segmented control with an **Allow
all partner models** toggle card; while restricted, the helper line
carries a warning icon and explains that later-published partner models
stay blocked until a workspace owner enables them
  - Rename the settings tab **Allowlist → Partner Models**
- Table: **Models** column is a static `{n} models` count in both modes;
new sortable **State** column with per-provider toggles rendered only
while restricted (width always reserved, so columns never shift); pinned
header and footer with rows scrolling between them; table is no longer
dimmed/inert while allow-all is on (rows stay explorable)
- Replace the Enable all / Disable all buttons with a **Disable all ▾**
menu whose items carry scope counts (`Disable all 36 providers` /
`Enable 4 matching providers`); with a search active, bulk actions apply
to matching providers only; no-op items are disabled
- Add an allowed-models summary under the table (`All partner models
allowed` / `214 partner models allowed`)
- Search: providers match by name or model name, nothing auto-expands
(chevron is always user-controlled); rows narrowed by the query show `3
of 7 match` in the Models column, full-relevance rows keep the plain
count
- Static table geometry: sticky in-scroller header (fixes a pre-existing
scrollbar-width misalignment from #13951), full-height card, reserved
scrollbar gutter — no layout shift on expand/collapse/mode change
- New `setProvidersEnabled(providerIds, enabled)` store action for
search-scoped bulk edits
- Partner-models vocabulary sweep across `workspacePanel.partnerNodes`
strings
- **Enterprise upsell (gated state)**: workspaces whose policy access
403s but whose catalog loads render the tab read-only with a muted crown
on the nav item, an Enterprise pill + Contact us on the access card, and
a confirm-dialog upsell when the locked toggle is clicked. Store keeps
the provider catalog when only the policy fetch is forbidden; `NavItem`
gains an optional `suffixIcon`.
- **Breaking**: none — policy API payloads and enforcement semantics
unchanged

## Review Focus

- Unfiltered *Disable all N providers* still confirms via dialog;
search-scoped bulk applies immediately (scope is explicit in the item
label)
- Access-mode flips now **remember provider states** (per design
annotation): first-ever restriction defaults all providers on;
thereafter, disabling BFL, allowing all, and restricting again leaves
BFL disabled. Returning to allow-all no longer resets stored states —
behavior change vs the shipped store, FE-only (the full policy is
persisted either way)
- Analytics in #14273 instruments the previous control set and will need
a rebase
- Confirm-dialog components (`ConfirmHeader/Body/Footer`,
`showConfirmDialog`) restyled to the Modal/Small design spec — 512px
width, rounded-2xl, `border-default`, 14px regular title, muted Cancel,
16px section padding. **This affects every `showConfirmDialog` caller
app-wide** (visual only; behavior unchanged)

Design: [Team Plan / Workspaces — Partner Models
settings](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=5732-53942)

Linear: Part of DES-685 and DES-641 · implementation counterpart FE-1525

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Summary

- create a fresh `createTestingPinia({ stubActions: false })` before
each root and desktop UI test
- remove 207 redundant per-suite activations
- retain 11 explicit activations needed for module initialization,
mid-test resets, and property-based iteration isolation
- leave specialized suites free to replace the active Pinia with real
Pinia, stubbed actions, plugins, or custom spies

Stacked on #15053.

## Verification

- root Vitest suite: 15,896 passed, 8 skipped across 1,159 files
- desktop UI Vitest suite: 125 passed across 16 files
- `pnpm lint`
- `pnpm typecheck`
- formatting and commit hooks

Co-authored-by: Amp <amp@ampcode.com>
## Summary

Combine the test-isolation cleanup and shared component automocks from
the stacked PR series into one merge. The aggregate removes redundant
per-suite setup while preserving within-test resets and
behavior-specific cleanup.

## Constituent changes

- #15072 — remove 249 redundant `mockReset()`, `mockClear()`, and
`vi.clearAllMocks()` calls across 120 files, relying on Vitest's
configured `mockReset` and `restoreMocks` behavior.
- #15074 — clear `sessionStorage` in the shared Vitest `beforeEach` and
remove 28 redundant cleanup calls across 21 suites, retaining the
fast-check iteration reset.
- #15075 — rely on the shared testing Pinia and remove 67 redundant
`setActivePinia(createPinia())` calls across 60 files, retaining
deliberate within-test and independent-store resets.
- #15082 — clear `document.body` in the shared Vitest setup and remove
28 redundant body resets across 26 suites, preserving behavior-specific
DOM replacement.
- #15084 — centralize Testing Library cleanup, including scoped website
cleanup for its `globals: false` project, and remove 23 redundant
lifecycle registrations while preserving mid-test cleanup.
- #15090 — reset browser history to `/` in the shared Vitest setup and
remove five suite-local URL resets while retaining URL mutations used as
test inputs.
- #15100 — replace three inline Select test doubles with opt-in
colocated Vitest automocks for the Select component family and include
Vue manual mocks in Knip's Vitest entry pattern.
- #15101 — replace five inline Slider factories with an opt-in colocated
range-input automock that preserves accessibility, numeric conversion,
bounds, step, and array-valued `v-model` behavior.
- #15104 — replace two queue-menu Popover factories with an opt-in
colocated automock and shared close spy, reset by Vitest's configured
`mockReset` behavior.

## Verification

- root unit suite: 1,160 files passed; 15,926 tests passed, 8 skipped
- `pnpm lint`
- `pnpm typecheck`
- `pnpm format:check`
- `pnpm knip`

---------

Co-authored-by: Amp <amp@ampcode.com>
## Summary

Regenerate translations for backported locale keys on the cloud release
line, and bake them into a per-deploy patch bump commit on the cloud
branch. Decision: #frontend-releases (Christian Byrne + Austin Mroz).

## Root cause

New localization keys land on `main` (e.g. #13984) and are backported to
`cloud/x.y`, but `i18n-update-core.yaml` only triggers on `pull_request`
to `main` and its job only runs for `version-bump-*` heads. Two gaps
followed:

1. i18n never listened to PRs whose base is `cloud/x.y`, so even a
`version-bump-*` PR on the cloud branch did not regenerate translations.
2. Nothing drove version bumps on the cloud line on a cadence.

That is why Austin's manual patch bump on `cloud/1.47` (#14066, still
open) was **inert**: it was just a `package.json` bump on `cloud/1.47`
with no i18n run behind it, so no `Update locales` commit was ever
added. `cloud/1.47`'s history confirms it -- its bumps (`1.47.7`,
`1.47.6`, ...) carry no accompanying locale commits.

## Changes

- **What**:
- `i18n-update-core.yaml`: extend `on.pull_request.branches` to `[main,
core/**, cloud/**]`. The job condition is unchanged (same-repo
`version-bump-*` head). This reuses the exact mechanism `main` already
relies on: the i18n job runs `collect-i18n` + `locale` + `format` and
commits `Update locales` **into the version-bump PR branch**, so the
patch bump and regenerated locale JSON merge to the cloud branch
together as one trackable commit.
- `cloud-release-version-bump.yaml` (new): daily cron (`0 7 * * *`) +
`workflow_dispatch`. Auto-detects the newest `cloud/x.y` branch (or
takes an explicit `branch` input) and dispatches the existing
`release-version-bump.yaml` with `version_type=patch` against it --
which opens the `version-bump-*` PR that i18n then completes.
- **Breaking**: none. `main`'s path is unchanged; `core/**` gains the
same (previously missing) behavior symmetrically.

## How stability / determinism is guaranteed (Austin's requirement)

`lobe-i18n` only translates **missing** target-locale keys. Once a
backported key is translated and committed to `cloud/x.y` (via the
merged version-bump PR), later runs produce no locale diff, so `git diff
--staged --quiet || git commit` adds no further commit. Translations are
baked once into a committed bump and never re-translated on subsequent
deploys.

## Idempotency + multi-branch

- **No stacking** (gate A): skip if a `version-bump-*` PR is already
open against the branch.
- **No empty churn** (gate B): skip when nothing merged since the last
version bump (detected via the last commit that changed the `version`
field); `force` dispatch input overrides. Fails open if the last bump
can't be located, so translations are never silently skipped.
- **Multi-branch**: auto-follows to `cloud/1.48` etc. -- no branch is
hardcoded. Defaults to the single latest cloud line (older cloud
branches are EOL); dispatch `branch` targets any specific branch.

## Review focus

- **Design tension for Christian to resolve**: gate B makes an empty
deploy (no new backports) a **no-op** -- no bump, no tracking commit.
That optimizes for "don't churn" over "one bump per deploy for
tracking." If you want a bump on *every* deploy regardless of content,
drop gate B (or have the cron always pass `force`). Left conservative
(content-gated) by default. Austin's "bake once, don't re-translate"
holds either way.
- **Token**: the cadence workflow uses `PR_GH_TOKEN` so the dispatched
`release-version-bump` run (and the PR it opens) actually trigger
downstream workflows -- `GITHUB_TOKEN`-triggered events do not start new
runs.
- **Not merging bump PRs automatically**: the cloud bump PR is left open
for the existing human/deploy merge step (unlike #14061's main
auto-merge). Enabling auto-merge on the cloud line can be a follow-up if
desired.
- **Relationship to #14061**: separate concern and separate files --
#14061 automates the weekly `main` minor bump + `core/x.y`/`cloud/x.y`
branch *cut*; this PR keeps already-cut cloud branches' translations
fresh via patch bumps. No file overlap (it does not touch
`i18n-update-core.yaml`); this reuses its `release-version-bump.yaml` at
runtime only.

Once merged, #14066 is superseded and can be closed (re-running the
cadence, or dispatching this workflow for `cloud/1.47`, produces the
correct bump + translation PR).

Refs #14066, #13984, #14061.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary

Adds `/ltx-2.5` and `/zh-CN/ltx-2.5` on the existing model-launch
template, data-only, and wires the primary "RUN LTX 2.5" CTA to the
launch template on Cloud.

## Changes

- **What**: New `data/ltx.ts` config + two page stubs, wired through the
existing `ModelLaunchPage` template (no new components). Route + `ltx.*`
translation keys (en / zh-CN). The primary hero CTA and every gallery
card deep-link `https://cloud.comfy.org/?template=video_ltx2_5_i2v`, the
way Seedance opens its run workflow. Secondary CTA keeps the workflows
hub; the pricing banner keeps the free-tier Cloud entry. Four hero
labels (Open Source, Image to Video, Text to Video, Partner Node), six
gallery cards (four free, two premium). Media served from
`media.comfy.org/website/ltx-2.5/` (`hero.mp4` + `card-1..6.webm`, all
confirmed live).
- **Dependencies**: none

## Review Focus

- The one behavioral choice worth a look: the CTA target.
`video_ltx2_5_i2v` now exists, so the primary CTA **and the gallery
cards** point at it (matching Seedance). If you want only the hero
button repointed and the cards left on the workflows hub, that is a
one-line-per-card change.
- Gallery cards follow the `media: { kind, src }` model; this page ships
no prompt boxes, so it reuses the existing template untouched.
- No posters yet for the gallery clips, so cards open on an empty frame
until each webm loads (same as `/flux-3`); can follow up when posters
reach the CDN.

## Verification

- `astro check`: 0 errors
- `vitest`: 38 tests pass (route + every-launch-page config checks,
incl. the new LTX config: unique card ids, both-locale copy, absolute
hrefs, media URL shape)
- `oxfmt --check` and `oxlint`: clean
- e2e (`e2e/ltx-2.5.spec.ts`): pins the "RUN LTX 2.5" CTA to the
template URL (literal + config-derived), secondary → workflows,
breadcrumb → catalog, MCP highlight → /mcp
<!-- ccr-slack-attribution -->
_Requested by **Christian Byrne** · [Slack
thread](https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1785357549411849?thread_ts=1785296212.634389&cid=C0A4XMHANP3)_

This adds a passing regression-guard test confirming that, for a real
QwenVL-shaped asset, the reverse (asset to node) resolution path already
correctly prefers the most specific, deepest matching category over a
shallower one.

Several other small PRs opened alongside this one document a
currently-broken forward direction, node type to which assets to list,
where only the first of several registered directories is ever
considered. This PR is the deliberate counterexample: the reverse
direction, asset to node, already implements exactly the
try-multiple-candidates-most-specific-first behavior the forward
direction is missing. This test exists to make that contrast concrete
and pin down that the working pattern doesn't regress, since it's the
natural template for eventually fixing the forward direction.

How to verify: pnpm test:unit
src/platform/assets/utils/assetMetadataUtils.test.ts


---
_Generated by [Claude
Code](https://claude.ai/code/session_01P6mHd4AJXxneJFqt8y3ozA)_

---

### Status note — 2026-08-03

Re-verified against `main` on this date.

`AILab_QwenVL` and eight other model-id-based node families were
**de-registered from `MODEL_NODE_MAPPINGS` by #14489** (merged
2026-08-01, backported to `cloud/1.48` via #14527). That fixed the
*reported symptom* in BE-5071 — those widgets now fall back to the
node's own `/api/object_info` combo.

**The bug class documented here is not fixed.** The asset browser still
resolves a node type to exactly one category. As of 2026-08-03 it
remains live on `main` for five node/input pairs:

| Node | Input | Directories (only the first is reachable) |
|---|---|---|
| `LS_LoadSegformerModel` | `model_name` | `segformer_b2_clothes`,
`segformer_b3_clothes`, `segformer_b3_fashion` |
| `UpscaleModelLoader` | `model_name` | `upscale_models`, `onnx` |
| `CLIPVisionLoader` | `clip_name` | `clip_vision`, `clip` |
| `FlashVSRNode` | *(auto)* | `FlashVSR`, `FlashVSR-v1.1` |
| `UltralyticsDetectorProvider` | `model_name` | `ultralytics/bbox`,
`ultralytics/segm` |

Tracked by FE-1177 (query `model_type:` rather than a single directory),
FE-1181 (generate the mapping from `object_info`), FE-1080
(child-directory tags) and BE-5255 (backend subtype-carrying tag).

**Why this is a merged test rather than a Slack thread:** `it.fails`
inverts the report — the assertion genuinely fails (proving the bug is
real) but CI stays green. It turns **red only when someone fixes the bug
without updating the test**, which is exactly the alarm we want. As an
unmerged draft it provides none of that, which is why these are being
taken out of draft.

Co-authored-by: Claude <noreply@anthropic.com>
…sters (#15116)

## Summary

Follow-up to #15109. Links the new `/ltx-2.5` page from the site, serves
posters for its clips, and points the secondary hero CTA at a page that
actually surfaces LTX workflows.

Stacked on `deepme987/website/ltx-2.5-launch`; retarget to `main` once
#15109 merges.

## Changes

- **Nav + footer**: the Products dropdown featured card moves from
MiniMax H3 to `NEW RELEASE: LTX 2.5`, and `/ltx-2.5` joins the footer
Products column after Wan Animate 2. The featured-card keys are named
after the nav item, not the card content, so only the values change.
Card art is `nav/ltx-card.webp`, cropped to the slot's 4:3 at 744x558 to
match `minimax-card.webp`.
- **Posters**: `hero-poster.webp` and `card-1..6.webp` were already on
the CDN but unused, so the hero and all six gallery cards painted as
empty boxes until ~8MB of video arrived. Wired as `posterSrc`, per the
`ModelLaunchMedia` guidance that only `/flux-3` should omit them.
- **Try Workflows**: the secondary CTA pointed at the generic hub, where
LTX 2.5 is not findable. Now `comfy.org/workflows/model/ltx`, matching
the `hubSlug` pattern in `model-metadata.ts`. The LTX 2.5 workflow slugs
still 404 on the hub, so this is the live target that also picks them up
automatically once the hub redeploys.

## Review Focus

- The hub target is the model page, not an individual workflow.
`/workflows/video_ltx2_5_i2v` is still a 404, so linking it directly
would ship a dead CTA.
- Restores e2e coverage the launch PR did not carry: the zh-CN page had
none.

## Verification

- `astro check`: 0 errors
- `vitest`: 394 pass
- `playwright e2e/ltx-2.5.spec.ts`: 12 pass; `navigation.spec.ts` +
`wan-animate-2.spec.ts`: 27 pass
- Mutation-checked the two new assertions: dropping one card's
`posterSrc` and reverting the CTA fails exactly those two tests
- Built output carries 7 `poster` attributes and no remaining
`minimax-card.webp`
## Automated Ingest API Type Update

This PR updates the Ingest API TypeScript types and Zod schemas from the
latest cloud OpenAPI specification.

- Cloud commit: 12db084
- Generated using @hey-api/openapi-ts with Zod plugin

These types cover the FE-facing ingest API (workspaces, billing,
secrets, assets, tasks, etc.).
Cloud's internal / machine-to-machine surface — `x-internal` operations
and the
`/admin/`, `/api/internal/`, `/api/webhooks/` rails — is stripped before
generation
(BE-2669), so it is not code-generated into this public package.

---------

Co-authored-by: synap5e <2515062+synap5e@users.noreply.github.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
#15113)

Root cause of the long-session "the app suddenly stops responding until
I refresh" reports (FE-1594).

## What breaks

`reka-ui` <= 2.6.1 `DismissableLayer` saves the pre-lock
`document.body.style.pointerEvents` in a **per-component-instance**
variable, assigned only by the layer that observes an empty layer set:

```js
let originalBodyPointerEvents;                     // per instance, undefined by default
if (context.layersWithOutsidePointerEventsDisabled.size === 0) {
  originalBodyPointerEvents = body.style.pointerEvents;   // only the FIRST layer assigns
  body.style.pointerEvents = "none";
}
...
cleanupFn(() => {
  if (props.disableOutsidePointerEvents && context.layersWithOutsidePointerEventsDisabled.size === 1)
    body.style.pointerEvents = originalBodyPointerEvents;  // whichever layer teardown sees size === 1
});
```

With two layers, the one that assigned the value is not the one that
restores it, so the restore assigns `undefined`.
`body.style.pointerEvents = undefined` stringifies to `"undefined"`,
which is not valid CSS — and per CSSOM an invalid value is a **no-op**,
so the declaration keeps its previous value. Verified in Chromium 149:

```
body.style.pointerEvents = 'none'       -> "none"
body.style.pointerEvents = undefined    -> "none"   (computed: "none")
body.style.pointerEvents = ''           -> ""
```

`document.body { pointer-events: none }` then survives for the rest of
the session. Nothing in the app ever clears it; only a reload does.

## What the user sees

Measured against the running app: with `body { pointer-events: none }`,
`canvas#graph-canvas` computes to `pointer-events: none` and stops being
the hit target (`document.elementFromPoint` over the graph returns
`<html>`). Every node and every widget drawn on the canvas goes dead —
including the LoadAudio "choose file to upload" button, which is why
this surfaces as "it suddenly doesn't let me upload audio files".

The left sidebar is unaffected: `.side-bar-panel` sets
`pointer-events-auto` (`LiteGraphCanvasSplitterOverlay.vue:38`), so it
keeps hit-testing. The separately reported "sidebar stops being
scrollable" symptom is therefore **not** this bug and is still open.

## Reachable how

Two layers with `disableOutsidePointerEvents` have to be alive at once,
and the first-registered one has to tear down first. Both tests here are
real flows:

- close a modal dialog while a menu/select/popover inside it is still
open (the nested layer's cleanup runs after the dialog's);
- `dialogStore` stacks up to 10 dialogs and `closeDialog({ key })`
closes any of them, so closing the lower one first is enough.

## Fix

Bump `reka-ui` 2.5.0 -> 2.6.2. Upstream fixed it in 2.6.2 by moving the
saved value onto the shared layer context, moving the restore into the
per-layer cleanup, and guarding it against a nullish value. 2.5.0 and
2.6.1 are affected; 2.6.2 through 2.10.3 are not.

## Verification

- Both new tests are red on 2.5.0 (`expected 'undefined' to be ''`) and
green on 2.6.2 — the first commit is test-only for that reason.
- Full unit suite on 2.6.2: 1161 files, 15928 passed / 8 skipped, 0
failed.

- Fixes FE-1594 (partially: the canvas/upload half)

### No bespoke e2e, deliberately

The unrecoverable part of this bug is real-browser CSSOM behaviour, so
an e2e would be the ideal home for it — but there is no stable handle on
`dialogStore` from `browser_tests` (`comfyAPIPlugin` only shims
`src/scripts` and `src/extensions/core`), and no existing spec opens a
modal dialog containing a menu. Rather than build a flaky bespoke flow,
the browser-side evidence is the Chromium measurement above, and the
regression guard is the unit test plus the existing 16-shard Playwright
suite, which exercises reka dialogs and menus broadly and runs against
the bumped version on this PR.

---------

Co-authored-by: Connor Byrne <c.byrne@comfy.org>
@MaanilVerma — **your call entirely; close it if you'd rather not.** I
owe you
context on why it's arriving as a PR eight days late rather than as a
question.

## The gap

`OnboardingTourNudgeMetadata` is `{ tour }` only, so `nudge_shown` and
`explore_templates_clicked` cannot be split by how the tour ended.

That matters more here than it would elsewhere, because **every ending
arms the
nudge** — deliberately, per your call in #14144: *"a user who saw no
tour is the
one who most needs somewhere to go next."* So the nudge's audience is
deliberately a mix of *finished the tour* and *never saw one*, and right
now the
funnel cannot tell those two apart. A conversion from someone who
completed the
walkthrough and one from someone the tour never started for land in the
same
bucket.

## The change

`tourWasCompleted` is already in scope in the component — it picks the
copy one
line above the telemetry call. This carries the same value onto both
events:

```ts
telemetry?.trackOnboardingTour('nudge_shown', {
  tour: 'firstRun',
  tour_completed: tourWasCompleted.value
})
```

Optional field, no new event, **no visibility rule changed**, nothing
added to
any other event.

## Verification

`FirstRunTourNudge.test.ts` **12/12**. The two existing assertions
pinned the
exact metadata shape, so they failed until updated — which is the suite
working.
Added one case asserting both events carry `false` when the tour did not
complete.

Mutation-checked: hard-coding `tour_completed: true` fails exactly that
new
case, 11 others still pass. `typecheck` and `format:check` clean.

## Why it's late

I drafted this as part of a Slack reply to you on **2026-08-04** and
never sent
it, so you have never actually seen the offer. That's the failure, not
the
telemetry. It was the one item in that draft still worth acting on — the
rest
of it has since been overtaken (the nudge copy landed in #14677, and
you've
since settled the OG image on #14957).

If you'd rather have the *full* ending instead of a boolean —
`completed` vs
`skipped` with its `skipReason` — say so and I'll rework it; the
controller has
`engine.lastEnding` and only exposes the boolean today. I went with the
boolean
because it needed no change to the controller's surface.

Co-authored-by: t <t@t.t>
…#14907)

## Summary

Adds the new **Forward Deployed Creatives** marketing page in both
locales — `/forward-deployed-creatives` and
`/zh-CN/forward-deployed-creatives` — built from the Figma design
([desktop](https://www.figma.com/design/11vkE4FAn4plEYpawd57zS/Comfy----Website-Design?node-id=10268-33788),
[mobile
hero](https://www.figma.com/design/11vkE4FAn4plEYpawd57zS/Comfy----Website-Design?node-id=10373-39106)).
The route is registered in `config/routes.ts` (`fdct`) and linked from
the main nav (under **Programs**, with a `new` badge) and the site
footer.

**Page sections, in order:**
1. **Hero** — parallax collage of FDC work on desktop (`useParallax`,
per-item scroll depth); collapses to a static stacked collage below
`xl`. Single **Contact us** CTA → `/contact`. Decorative autoplay videos
are paused under `prefers-reduced-motion`.
2. **Builders** — wire-node diagram (`WireNodeLayout`).
3. **Client logo marquee** (`SocialProofBarSection`).
4. **How it works** — numbered steps (`BenefitsGrid01`).
5. **What you get** — checklist (`ChecklistSplit01`).
6. **Featured technologists** — Doug Hogan, Chris V., Rob Losch, and
Robert Paige ("Bert"), each with a bio dialog surfacing their real
workflows.
7. **Featured projects** — tagged workflow cards linking to real
published workflows on `comfy.org/workflows`.
8. **Closing CTA** — "Build with the people that build Comfy." →
`/contact`.
9. **Q&A** — 5-item accordion, backed by a single data source
(`fdctFaqs`) that also emits the `FAQPage` json-ld so structured data
always matches on-page copy per locale.
10. **Enterprise CTA band** (`CtaBands01`) → `/contact`.

## New reusable blocks (each with a Storybook story)

- `TeamGrid01` + `TeamMemberDialog01`, built on new `ui/dialog`
primitives (`Dialog`, `DialogTrigger`, `DialogContent`, `DialogOverlay`,
`DialogTitle`, `DialogDescription`, `DialogClose`).
- `CtaBands01` — enterprise CTA band.
- `CardWorkflow01` + `CardWorkflowGallery01` — tagged workflow cards for
the featured-projects gallery.

## Backward-compatible block extensions

`WireNodeLayout`, `BenefitsGrid01`, `ChecklistSplit01`, `HeroCentered01`
(e.g. `eyebrowClass`), and `CardArticle01` / `CardArticleGallery01`
gained opt-in props for this page — existing call sites are unaffected.
Storybook stories were added for `BenefitsGrid01`, `ChecklistSplit01`,
and `CardArticleGallery01`.

## Drafted copy needing review

- **FAQ answers** (`fdct.faq.a1`–`a5`) are **drafted from the page's own
claims**, not provided copy — please review the five Q&A pairs for
accuracy of commitment.
- All **zh-CN translations** in the `fdct.*` namespace are drafted.
- Enterprise CTA band and closing-CTA copy are drafted.

## Test plan

- [x] `e2e/fdct.spec.ts` — ~19 tests across three suites: `FDCT page
@smoke` (en desktop), `FDCT hero @mobile`, and `FDCT page (zh-CN)
@smoke`. Covers each section, the technologist bio dialog, Q&A expand,
`FAQPage` json-ld pairs, and locale-prefixed CTA hrefs.
- [x] `config/routes.test.ts` — canonical `/forward-deployed-creatives`
for en and `/zh-CN/forward-deployed-creatives` for zh-CN.
- [x] `pnpm --filter @comfyorg/website typecheck`, eslint, stylelint,
oxfmt via pre-commit hooks.
- [x] Visually matched against Figma at desktop and mobile widths in a
real browser.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Seven synthetic packs, each breaking one measurement channel, plus a clean
control. The matrix-detection-proof job passes only if every channel fires
with its exact poison message (verify_detection.py) and the combined
verdict FAILs on the poison corpus.

Building it surfaced two blind channels, now handled honestly: throwing
extension hooks are contained by extensionService and could never fail
registration - the runner now records the containment signature as
hookErrors row data - and the store-vs-live desync comparator needs Vue
widget-store wiring absent in this harness, so widget mutations surface
via the serialized signature instead.

The poison fixture is corpus data, not repo code (eslint/oxlint/knip
ignores), and the generated src/__ecs_matrix__ build output is excluded
from vue-tsc so a locally built fixture cannot fail typecheck.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 55f18fac-18a5-40f7-b5d3-aedb51da3274

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

🎭 Playwright: ✅ 1828 passed, 0 failed · 3 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1807 / ❌ 0 / ⚠️ 3 / ⏭️ 5)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 18 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • New-test walkthrough (chromium, recorded video): View Report

📦 Bundle Size

⏳ Size data collection in progress…

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 67.2 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 56.2 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.6 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 50.8 MB heap
large-graph-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 63.3 MB heap
large-graph-pan: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 53.0 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 73.8 MB heap
minimap-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 65.2 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 51.3 MB heap
subgraph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 55.4 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 51.9 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 152ms TBT · 93.4 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 74.2 MB heap
vue-large-graph-idle: · 55.4 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 161.7 MB heap
vue-large-graph-pan: · 55.4 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 443ms TBT · 153.0 MB heap
workflow-execution: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 65.4 MB heap

ℹ️ No baseline found — significance unavailable.

Absolute values
Metric Value
canvas-idle: avg frame time 17ms
canvas-idle: p95 frame time 17ms
canvas-idle: layout duration 0ms
canvas-idle: style recalc duration 9ms
canvas-idle: layout count 0
canvas-idle: style recalc count 8
canvas-idle: task duration 614ms
canvas-idle: script duration 26ms
canvas-idle: TBT 0ms
canvas-idle: heap used 67.2 MB
canvas-idle: DOM nodes 16
canvas-idle: event listeners 5
canvas-mouse-sweep: avg frame time 17ms
canvas-mouse-sweep: p95 frame time 17ms
canvas-mouse-sweep: layout duration 4ms
canvas-mouse-sweep: style recalc duration 44ms
canvas-mouse-sweep: layout count 12
canvas-mouse-sweep: style recalc count 78
canvas-mouse-sweep: task duration 1038ms
canvas-mouse-sweep: script duration 141ms
canvas-mouse-sweep: TBT 0ms
canvas-mouse-sweep: heap used 56.2 MB
canvas-mouse-sweep: DOM nodes -112
canvas-mouse-sweep: event listeners -74
canvas-zoom-sweep: avg frame time 17ms
canvas-zoom-sweep: p95 frame time 17ms
canvas-zoom-sweep: layout duration 1ms
canvas-zoom-sweep: style recalc duration 20ms
canvas-zoom-sweep: layout count 6
canvas-zoom-sweep: style recalc count 31
canvas-zoom-sweep: task duration 477ms
canvas-zoom-sweep: script duration 28ms
canvas-zoom-sweep: TBT 0ms
canvas-zoom-sweep: heap used 69.6 MB
canvas-zoom-sweep: DOM nodes 77
canvas-zoom-sweep: event listeners 19
dom-widget-clipping: avg frame time 17ms
dom-widget-clipping: p95 frame time 17ms
dom-widget-clipping: layout duration 0ms
dom-widget-clipping: style recalc duration 9ms
dom-widget-clipping: layout count 0
dom-widget-clipping: style recalc count 11
dom-widget-clipping: task duration 483ms
dom-widget-clipping: script duration 75ms
dom-widget-clipping: TBT 0ms
dom-widget-clipping: heap used 50.8 MB
dom-widget-clipping: DOM nodes 18
dom-widget-clipping: event listeners 2
large-graph-idle: avg frame time 17ms
large-graph-idle: p95 frame time 17ms
large-graph-idle: layout duration 0ms
large-graph-idle: style recalc duration 8ms
large-graph-idle: layout count 0
large-graph-idle: style recalc count 8
large-graph-idle: task duration 865ms
large-graph-idle: script duration 130ms
large-graph-idle: TBT 0ms
large-graph-idle: heap used 63.3 MB
large-graph-idle: DOM nodes -283
large-graph-idle: event listeners -165
large-graph-pan: avg frame time 17ms
large-graph-pan: p95 frame time 17ms
large-graph-pan: layout duration 0ms
large-graph-pan: style recalc duration 15ms
large-graph-pan: layout count 0
large-graph-pan: style recalc count 69
large-graph-pan: task duration 1479ms
large-graph-pan: script duration 498ms
large-graph-pan: TBT 0ms
large-graph-pan: heap used 53.0 MB
large-graph-pan: DOM nodes -284
large-graph-pan: event listeners -180
large-graph-zoom: avg frame time 17ms
large-graph-zoom: p95 frame time 17ms
large-graph-zoom: layout duration 8ms
large-graph-zoom: style recalc duration 16ms
large-graph-zoom: layout count 60
large-graph-zoom: style recalc count 64
large-graph-zoom: task duration 1752ms
large-graph-zoom: script duration 617ms
large-graph-zoom: TBT 0ms
large-graph-zoom: heap used 73.8 MB
large-graph-zoom: DOM nodes -290
large-graph-zoom: event listeners -166
minimap-idle: avg frame time 17ms
minimap-idle: p95 frame time 17ms
minimap-idle: layout duration 0ms
minimap-idle: style recalc duration 7ms
minimap-idle: layout count 0
minimap-idle: style recalc count 7
minimap-idle: task duration 860ms
minimap-idle: script duration 132ms
minimap-idle: TBT 0ms
minimap-idle: heap used 65.2 MB
minimap-idle: DOM nodes -286
minimap-idle: event listeners -150
subgraph-dom-widget-clipping: avg frame time 17ms
subgraph-dom-widget-clipping: p95 frame time 17ms
subgraph-dom-widget-clipping: layout duration 0ms
subgraph-dom-widget-clipping: style recalc duration 12ms
subgraph-dom-widget-clipping: layout count 0
subgraph-dom-widget-clipping: style recalc count 47
subgraph-dom-widget-clipping: task duration 462ms
subgraph-dom-widget-clipping: script duration 135ms
subgraph-dom-widget-clipping: TBT 0ms
subgraph-dom-widget-clipping: heap used 51.3 MB
subgraph-dom-widget-clipping: DOM nodes 19
subgraph-dom-widget-clipping: event listeners 8
subgraph-idle: avg frame time 17ms
subgraph-idle: p95 frame time 17ms
subgraph-idle: layout duration 0ms
subgraph-idle: style recalc duration 9ms
subgraph-idle: layout count 0
subgraph-idle: style recalc count 9
subgraph-idle: task duration 609ms
subgraph-idle: script duration 22ms
subgraph-idle: TBT 0ms
subgraph-idle: heap used 55.4 MB
subgraph-idle: DOM nodes 9
subgraph-idle: event listeners -75
subgraph-mouse-sweep: avg frame time 17ms
subgraph-mouse-sweep: p95 frame time 17ms
subgraph-mouse-sweep: layout duration 5ms
subgraph-mouse-sweep: style recalc duration 42ms
subgraph-mouse-sweep: layout count 16
subgraph-mouse-sweep: style recalc count 76
subgraph-mouse-sweep: task duration 903ms
subgraph-mouse-sweep: script duration 106ms
subgraph-mouse-sweep: TBT 0ms
subgraph-mouse-sweep: heap used 51.9 MB
subgraph-mouse-sweep: DOM nodes -111
subgraph-mouse-sweep: event listeners -75
subgraph-transition-enter: avg frame time 17ms
subgraph-transition-enter: p95 frame time 17ms
subgraph-transition-enter: layout duration 13ms
subgraph-transition-enter: style recalc duration 33ms
subgraph-transition-enter: layout count 13
subgraph-transition-enter: style recalc count 17
subgraph-transition-enter: task duration 1079ms
subgraph-transition-enter: script duration 44ms
subgraph-transition-enter: TBT 152ms
subgraph-transition-enter: heap used 93.4 MB
subgraph-transition-enter: DOM nodes 13673
subgraph-transition-enter: event listeners 2375
viewport-pan-sweep: avg frame time 17ms
viewport-pan-sweep: p95 frame time 17ms
viewport-pan-sweep: layout duration 0ms
viewport-pan-sweep: style recalc duration 39ms
viewport-pan-sweep: layout count 0
viewport-pan-sweep: style recalc count 249
viewport-pan-sweep: task duration 5317ms
viewport-pan-sweep: script duration 1684ms
viewport-pan-sweep: TBT 0ms
viewport-pan-sweep: heap used 74.2 MB
viewport-pan-sweep: DOM nodes -282
viewport-pan-sweep: event listeners -161
vue-large-graph-idle: avg frame time 18ms
vue-large-graph-idle: p95 frame time 17ms
vue-large-graph-idle: layout duration 0ms
vue-large-graph-idle: style recalc duration 0ms
vue-large-graph-idle: layout count 0
vue-large-graph-idle: style recalc count 0
vue-large-graph-idle: task duration 17898ms
vue-large-graph-idle: script duration 641ms
vue-large-graph-idle: TBT 0ms
vue-large-graph-idle: heap used 161.7 MB
vue-large-graph-idle: DOM nodes -8312
vue-large-graph-idle: event listeners -16390
vue-large-graph-pan: avg frame time 18ms
vue-large-graph-pan: p95 frame time 17ms
vue-large-graph-pan: layout duration 0ms
vue-large-graph-pan: style recalc duration 22ms
vue-large-graph-pan: layout count 0
vue-large-graph-pan: style recalc count 158
vue-large-graph-pan: task duration 21976ms
vue-large-graph-pan: script duration 966ms
vue-large-graph-pan: TBT 443ms
vue-large-graph-pan: heap used 153.0 MB
vue-large-graph-pan: DOM nodes -8312
vue-large-graph-pan: event listeners -16382
workflow-execution: avg frame time 17ms
workflow-execution: p95 frame time 17ms
workflow-execution: layout duration 1ms
workflow-execution: style recalc duration 21ms
workflow-execution: layout count 3
workflow-execution: style recalc count 11
workflow-execution: task duration 123ms
workflow-execution: script duration 11ms
workflow-execution: TBT 0ms
workflow-execution: heap used 65.4 MB
workflow-execution: DOM nodes 121
workflow-execution: event listeners 99
Raw data
{
  "timestamp": "2026-08-12T17:31:21.150Z",
  "gitSha": "cf652febfbbb67d8e02471169b22f6edc2b8c30c",
  "branch": "benjcooley/matrix-detection-proof",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2028.229999999951,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 8.642999999999997,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 606.3100000000001,
      "heapDeltaBytes": 5808552,
      "heapUsedBytes": 70252048,
      "domNodes": 16,
      "jsHeapTotalBytes": 24641536,
      "scriptDurationMs": 25.217,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-idle",
      "durationMs": 2057.495000000017,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 8.422000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 621.269,
      "heapDeltaBytes": 5896040,
      "heapUsedBytes": 70651696,
      "domNodes": 16,
      "jsHeapTotalBytes": 24379392,
      "scriptDurationMs": 26.453999999999997,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2123.1300000000033,
      "styleRecalcs": 79,
      "styleRecalcDurationMs": 45.251,
      "layouts": 12,
      "layoutDurationMs": 3.8269999999999995,
      "taskDurationMs": 1115.1180000000002,
      "heapDeltaBytes": -10911024,
      "heapUsedBytes": 53241564,
      "domNodes": -283,
      "jsHeapTotalBytes": 24092672,
      "scriptDurationMs": 146.07799999999997,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1840.5059999998912,
      "styleRecalcs": 77,
      "styleRecalcDurationMs": 43.685,
      "layouts": 12,
      "layoutDurationMs": 3.6470000000000002,
      "taskDurationMs": 961.8070000000001,
      "heapDeltaBytes": 103580,
      "heapUsedBytes": 64596704,
      "domNodes": 60,
      "jsHeapTotalBytes": 25690112,
      "scriptDurationMs": 136.30599999999998,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1751.7090000000053,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 20.461,
      "layouts": 6,
      "layoutDurationMs": 0.722,
      "taskDurationMs": 468.609,
      "heapDeltaBytes": 8577328,
      "heapUsedBytes": 72846468,
      "domNodes": 76,
      "jsHeapTotalBytes": 24641536,
      "scriptDurationMs": 27.241999999999997,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1743.4019999999464,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 20.211999999999996,
      "layouts": 6,
      "layoutDurationMs": 0.745,
      "taskDurationMs": 485.021,
      "heapDeltaBytes": 8830720,
      "heapUsedBytes": 73197424,
      "domNodes": 77,
      "jsHeapTotalBytes": 24117248,
      "scriptDurationMs": 28.517000000000003,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 797.0160000000419,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 8.723,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 538.256,
      "heapDeltaBytes": -11175644,
      "heapUsedBytes": 53242436,
      "domNodes": 18,
      "jsHeapTotalBytes": 25690112,
      "scriptDurationMs": 78.768,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 715.8670000000029,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 9.207,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 428.35900000000004,
      "heapDeltaBytes": -11233116,
      "heapUsedBytes": 53280164,
      "domNodes": 18,
      "jsHeapTotalBytes": 25165824,
      "scriptDurationMs": 71.53,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2032.590999999968,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.639,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 852.9660000000001,
      "heapDeltaBytes": 5878616,
      "heapUsedBytes": 66349928,
      "domNodes": -283,
      "jsHeapTotalBytes": 3510272,
      "scriptDurationMs": 125.727,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2045.2119999999923,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.899,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 877.893,
      "heapDeltaBytes": 6907348,
      "heapUsedBytes": 66345980,
      "domNodes": -282,
      "jsHeapTotalBytes": 3510272,
      "scriptDurationMs": 133.67800000000003,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2347.5199999999745,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 14.633000000000004,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1466.401,
      "heapDeltaBytes": -5506568,
      "heapUsedBytes": 55477676,
      "domNodes": -283,
      "jsHeapTotalBytes": 3772416,
      "scriptDurationMs": 491.41599999999994,
      "eventListeners": -179,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2338.341999999898,
      "styleRecalcs": 69,
      "styleRecalcDurationMs": 15.134000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1491.875,
      "heapDeltaBytes": -5187620,
      "heapUsedBytes": 55647988,
      "domNodes": -285,
      "jsHeapTotalBytes": 5083136,
      "scriptDurationMs": 504.09900000000005,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3544.493999999986,
      "styleRecalcs": 64,
      "styleRecalcDurationMs": 15.719999999999997,
      "layouts": 60,
      "layoutDurationMs": 7.987999999999999,
      "taskDurationMs": 1760.68,
      "heapDeltaBytes": 13920116,
      "heapUsedBytes": 76155832,
      "domNodes": -289,
      "jsHeapTotalBytes": 6656000,
      "scriptDurationMs": 615.2019999999999,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3564.9799999999914,
      "styleRecalcs": 64,
      "styleRecalcDurationMs": 15.357999999999997,
      "layouts": 60,
      "layoutDurationMs": 8.276,
      "taskDurationMs": 1743.441,
      "heapDeltaBytes": 16396552,
      "heapUsedBytes": 78670668,
      "domNodes": -290,
      "jsHeapTotalBytes": 6131712,
      "scriptDurationMs": 619.472,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2041.8220000000247,
      "styleRecalcs": 6,
      "styleRecalcDurationMs": 6.796999999999997,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 849.143,
      "heapDeltaBytes": 7553896,
      "heapUsedBytes": 69157728,
      "domNodes": -285,
      "jsHeapTotalBytes": 2723840,
      "scriptDurationMs": 130.171,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "minimap-idle",
      "durationMs": 2031.372000000033,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.492000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 869.9650000000001,
      "heapDeltaBytes": 6751068,
      "heapUsedBytes": 67564500,
      "domNodes": -286,
      "jsHeapTotalBytes": 4034560,
      "scriptDurationMs": 133.586,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 655.6509999999776,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 12.023000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 462.337,
      "heapDeltaBytes": -10400832,
      "heapUsedBytes": 53795556,
      "domNodes": 20,
      "jsHeapTotalBytes": 25952256,
      "scriptDurationMs": 132.41699999999997,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 705.4219999999987,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 11.805,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 460.85200000000003,
      "heapDeltaBytes": -10599864,
      "heapUsedBytes": 53852908,
      "domNodes": 18,
      "jsHeapTotalBytes": 26476544,
      "scriptDurationMs": 137.928,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2005.503000000033,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 9.957,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 633.069,
      "heapDeltaBytes": -18357552,
      "heapUsedBytes": 46124456,
      "domNodes": 1,
      "jsHeapTotalBytes": 23044096,
      "scriptDurationMs": 20.807,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2002.254999999991,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 8.371,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 584.259,
      "heapDeltaBytes": 5900052,
      "heapUsedBytes": 70094132,
      "domNodes": 16,
      "jsHeapTotalBytes": 24641536,
      "scriptDurationMs": 22.213000000000005,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1737.070000000017,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 40.983,
      "layouts": 16,
      "layoutDurationMs": 5.039,
      "taskDurationMs": 913.993,
      "heapDeltaBytes": -16440784,
      "heapUsedBytes": 48050496,
      "domNodes": -284,
      "jsHeapTotalBytes": 22519808,
      "scriptDurationMs": 104.199,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1725.0320000000556,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 43.12100000000001,
      "layouts": 16,
      "layoutDurationMs": 5.224,
      "taskDurationMs": 892.3320000000001,
      "heapDeltaBytes": -3545088,
      "heapUsedBytes": 60801372,
      "domNodes": 62,
      "jsHeapTotalBytes": 25165824,
      "scriptDurationMs": 106.91400000000002,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 1454.5219999999972,
      "styleRecalcs": 17,
      "styleRecalcDurationMs": 32.699000000000005,
      "layouts": 13,
      "layoutDurationMs": 13.456,
      "taskDurationMs": 1079.008,
      "heapDeltaBytes": 30713728,
      "heapUsedBytes": 97939380,
      "domNodes": 13673,
      "jsHeapTotalBytes": 15466496,
      "scriptDurationMs": 43.85,
      "eventListeners": 2375,
      "totalBlockingTimeMs": 152,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 9072.32399999998,
      "styleRecalcs": 249,
      "styleRecalcDurationMs": 38.559,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 5233.677,
      "heapDeltaBytes": 14643632,
      "heapUsedBytes": 74068680,
      "domNodes": -281,
      "jsHeapTotalBytes": 4227072,
      "scriptDurationMs": 1621.9519999999998,
      "eventListeners": -161,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.80000000000109
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 9420.744000000013,
      "styleRecalcs": 249,
      "styleRecalcDurationMs": 39.689,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 5400.852,
      "heapDeltaBytes": 21895368,
      "heapUsedBytes": 81493276,
      "domNodes": -282,
      "jsHeapTotalBytes": 5537792,
      "scriptDurationMs": 1746.194,
      "eventListeners": -161,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 17795.441000000097,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17764.442,
      "heapDeltaBytes": -57494428,
      "heapUsedBytes": 167570516,
      "domNodes": -8312,
      "jsHeapTotalBytes": -7020544,
      "scriptDurationMs": 629.197,
      "eventListeners": -16389,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 18083.45799999995,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 18032.046000000002,
      "heapDeltaBytes": -32710628,
      "heapUsedBytes": 171558196,
      "domNodes": -8312,
      "jsHeapTotalBytes": -11927552,
      "scriptDurationMs": 652.918,
      "eventListeners": -16391,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.773333333333238,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21716.948000000004,
      "styleRecalcs": 152,
      "styleRecalcDurationMs": 21.214999999999982,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 21683.694,
      "heapDeltaBytes": -70431052,
      "heapUsedBytes": 153927952,
      "domNodes": -8312,
      "jsHeapTotalBytes": -11476992,
      "scriptDurationMs": 969.0740000000001,
      "eventListeners": -16383,
      "totalBlockingTimeMs": 445,
      "frameDurationMs": 18.330000000000048,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 22315.655999999988,
      "styleRecalcs": 163,
      "styleRecalcDurationMs": 23.366,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 22268.332000000002,
      "heapDeltaBytes": -38682480,
      "heapUsedBytes": 166921404,
      "domNodes": -8312,
      "jsHeapTotalBytes": -8593408,
      "scriptDurationMs": 963.671,
      "eventListeners": -16381,
      "totalBlockingTimeMs": 441,
      "frameDurationMs": 17.776666666666763,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 465.7210000000305,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 18.064999999999998,
      "layouts": 3,
      "layoutDurationMs": 0.7140000000000002,
      "taskDurationMs": 117.67100000000002,
      "heapDeltaBytes": 5067320,
      "heapUsedBytes": 68497388,
      "domNodes": 120,
      "jsHeapTotalBytes": 4718592,
      "scriptDurationMs": 10.877999999999998,
      "eventListeners": 99,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 472.1560000000409,
      "styleRecalcs": 13,
      "styleRecalcDurationMs": 23.490000000000006,
      "layouts": 3,
      "layoutDurationMs": 0.7049999999999998,
      "taskDurationMs": 128.673,
      "heapDeltaBytes": 5042704,
      "heapUsedBytes": 68649592,
      "domNodes": 121,
      "jsHeapTotalBytes": 4718592,
      "scriptDurationMs": 11.54,
      "eventListeners": 99,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.699999999999818
    }
  ]
}

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 08/18/2026, 04:16:01 PM UTC

Links

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 22.35650% with 257 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
apps/website/src/components/common/AudioPlayer.vue 0.00% 47 Missing ⚠️
...s/website/src/components/common/WireNodeLayout.vue 0.00% 22 Missing ⚠️
...s/website/src/components/blocks/CardWorkflow01.vue 0.00% 17 Missing ⚠️
apps/website/src/templates/fdct/HeroSection.vue 0.00% 17 Missing ⚠️
...bsite/src/components/blocks/TeamMemberDialog01.vue 0.00% 16 Missing ⚠️
apps/website/src/components/blocks/TeamGrid01.vue 0.00% 13 Missing ⚠️
...website/src/components/product/api/HeroSection.vue 0.00% 10 Missing ⚠️
apps/website/src/data/fdct.ts 0.00% 10 Missing ⚠️
.../templates/model-launch/ModelLaunchHeroSection.vue 0.00% 10 Missing ⚠️
...website/src/components/ui/dialog/DialogContent.vue 0.00% 8 Missing ⚠️
... and 33 more

❌ Your patch status has failed because the patch coverage (22.35%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

@@                       Coverage Diff                       @@
##           benjcooley/ecosystem-matrix   #15145      +/-   ##
===============================================================
- Coverage                        80.81%   79.14%   -1.68%     
===============================================================
  Files                             1873     2209     +336     
  Lines                           121954   115475    -6479     
  Branches                         37569    35350    -2219     
===============================================================
- Hits                             98558    91393    -7165     
- Misses                           22849    23561     +712     
+ Partials                           547      521      -26     
Flag Coverage Δ
unit 72.91% <ø> (+0.38%) ⬆️
website-unit 22.59% <22.35%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...bsite/src/components/blocks/FeaturedCarousel01.vue 0.00% <ø> (ø)
.../website/src/components/blocks/WatchAuthorCard.vue 0.00% <ø> (ø)
.../website/src/components/blocks/WatchPageLayout.vue 0.00% <ø> (ø)
...website/src/components/blocks/WatchRelatedCard.vue 0.00% <ø> (ø)
...ebsite/src/components/blocks/WatchRelatedStrip.vue 0.00% <ø> (ø)
apps/website/src/components/common/SiteFooter.vue 0.00% <ø> (ø)
...website/src/components/pricing/PricingTeamCard.vue 90.00% <100.00%> (ø)
...s/website/src/components/product/api/stampCycle.ts 100.00% <100.00%> (ø)
.../src/components/product/enterprise/TeamSection.vue 0.00% <ø> (ø)
apps/website/src/config/routes.ts 92.85% <ø> (ø)
... and 55 more

... and 693 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@benjcooley

Copy link
Copy Markdown
Contributor Author

CI evidence - run 31621967782, all six jobs green:

job result time
matrix-detection-proof pass - every channel fired, control clean, verdict correctly FAILed on poison 1m09s
corpus pass 4m37s
ecosystem-matrix 1-4 pass ~11m each
matrix-verdict (real corpus) pass 11s

The proof job costs about a minute per census-touching PR and permanently guards every measurement channel.

…15356)

## Problem

`browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts:204` —
`@mobile Can pan with touch`, run by the `mobile-chrome` Playwright
project — fails intermittently across unrelated PRs with:

```
40 pixels (ratio 0.01 of all image pixels) are different.
```

Across 9 open PRs the `playwright-tests (mobile-chrome)` check failed on
3 and passed on 6. On one PR it failed, was re-run with no code change,
and passed. Two of those failures
([#15352](#15352),
[#15339](#15339)) are
this flake and produced **byte-identical** actual/diff PNGs on different
runners. The third (#15337) is a genuine rendering change from that PR —
it moved 3146 pixels here and also broke 4 `mobileBaseline.spec.ts`
screenshots, so it is not an instance of this flake.

## Root cause

I pulled the `playwright-report-mobile-chrome` artifacts and located the
40 differing pixels in the diff image:

- Bounding box `x ∈ [202, 348]`, `y ∈ [612, 617]` — inside the glyphs of
the Load Checkpoint node's `ckpt_name` combo widget value,
`v1-5-pruned-emaonly-fp16.sa…`.
- **Every other pixel of the 393×727 image is byte-identical** between
the passing and failing renders — the node's `transform: translate(26px,
444px)`, its error ring, the `ckpt_name` label, the node title, the
toolbar and the canvas grid all match exactly.
- Glyph positions are identical (per-glyph ink-centroid drift < 0.09
px). The failing render simply has ~5% less ink — the same glyphs at the
same coordinates rasterized through a different text-antialiasing path.

So this is **not** an unsettled pan, a mid-frame screenshot, or
momentum: the pan transform is fully deterministic. It is also not
something a readiness signal can wait out — Playwright reports `captured
a stable screenshot` and then re-fails all 3 retries with the exact same
40 pixels.

## Fix

Two changes, neither of which touches the baseline:

1. **Assert the pan actually landed** before comparing pixels, matching
the sibling tests in this file. A pan that never applies now fails with
a readable offset assertion instead of surfacing as a pixel diff.
2. **Bound the comparison with `maxDiffPixels: 100`**, in line with
existing usage in `selectionToolbox.spec.ts`, `interaction.spec.ts`,
`widget.spec.ts` and `canvasSettings.spec.ts`.

### Why not the alternatives

- **Wait on a ready signal** — nothing is unsettled. The geometry is
byte-identical and Playwright already burns 5s of retries on a
screenshot it considers stable.
- **Disable animations** — already the default (`animations:
'disabled'`).
- **Mask the region** — would blank a genuine part of the assertion and
require regenerating the baseline.
- **Product bug in the pan transform** — ruled out; the transform is
identical in both renders.

## Before / after

Replaying the real CI bytes through Playwright's own image comparator
(`playwright-core` `getComparator('image/png')`) against the committed
baseline:

| Input | Before | After |
| --- | --- | --- |
| CI failing render (#15352 / #15339, byte-identical) | **FAIL** — 40 px
| **PASS** |
| CI passing render | PASS | PASS |
| Control: #15337 real rendering change | FAIL — 3146 px | **FAIL** —
3146 px |
| Control: 1px pan error (dx=1) | FAIL — 3615 px | **FAIL** — 3615 px |
| Control: 1px pan error (dy=1) | FAIL — 5845 px | **FAIL** — 5845 px |

The tolerance flips exactly the flake and nothing else. The smallest
real regression measurable on this test is a 1-pixel pan error at 3615
px — 36× the bound and 90× the observed noise.

### Verification caveat

I did not run `--repeat-each` locally. The baseline is
`mobile-chrome-linux`, generated in the CI container, so a local run
would mismatch on unrelated font rendering and would give no signal on
this specific 40-pixel difference. The table above is a stronger check:
it is a deterministic replay of the exact pixels that turned CI red,
through the same comparator Playwright uses. Confirmation that CI is
green comes from this PR's own `mobile-chrome` run.
@benjcooley

Copy link
Copy Markdown
Contributor Author

I have read and agree to the Contributor License Agreement

christian-byrne and others added 18 commits August 17, 2026 20:45
…15273)

*PR Created by the Glary-Bot Agent*

---

Follow-up to the silent-failure thread on
[BE-7550](https://linear.app/comfyorg/issue/BE-7550/bug-new-api-key-rejected-as-invalid-api-key-for-1-minute-after),
which asked why a customer-facing request failure only reached the
browser console.

## What the investigation found

The big instance was the API-key sign-in path, fixed in #15272 — `POST
/customers` failing into `.catch(err => console.error(err))`. That is
the exact console-only error described in the thread.

Beyond it, there is **no shared choke point that swallows errors**, so
there is nothing to fix at the transport layer:

- `fetchWithUnifiedRemint` and `attachUnifiedRemintInterceptor`
propagate failures unchanged.
- `fetchWithCustomerRecovery` deliberately returns the original response
when recovery fails, so callers keep their own error handling.
- `customerEventsService.executeRequest` records failures into `error`
for components to render.
- Every customer POST reached through `useAuthActions`
(`purchaseCredits`, `accessBillingPortal`, `fetchBalance`) already runs
through `wrapWithErrorHandlingAsync(..., reportError)` and toasts.

So the generalized mechanism already exists and is applied nearly
everywhere; auto-toasting from a shared axios interceptor would have
been the wrong fix, since it would also fire for background polls and
for the calls whose callers already handle errors.

## What this changes

One remaining spot where a user-initiated action failed silently:
clicking **Message support** in the billing panel ran the
`ContactSupport` command inside a `try/catch` whose only action was
`console.error`. On failure the spinner stopped and nothing else
happened, so the click looked like it had worked.

It now goes through `useErrorHandling`, which is how the other
customer-facing actions in this area already report failures.

## What this deliberately leaves alone

`handleRefresh` keeps swallowing. Opening the panel is not a request for
that data in particular, the panel already renders its absence, and an
existing test (`swallows refresh failures without surfacing a toast`)
pins that behaviour — so it is an intentional decision, not an
oversight, and not mine to reverse here.

## Testing

Tightened the existing `handleMessageSupport` error test to assert the
toast as well as the loading state; it fails against the pre-fix
composable. The other 7 tests in the file, including the one pinning
`handleRefresh`, are unchanged and still pass. `pnpm typecheck` and
`pnpm lint` are clean.

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary

Upgrades `apps/website` from Astro 6 to Astro 7, bumping the matching
integration majors in the workspace catalog:

- `astro` `^6.4.2` → `^7.2.1`
- `@astrojs/mdx` `^6.0.3` → `^7.0.5`
- `@astrojs/vue` `^6.0.1` → `^7.0.2`
- `@astrojs/check` `^0.9.9` → `^0.9.10`

## Notable behavior change

Astro 7 changed the `compressHTML` default to JSX-style whitespace
stripping. This PR pins `compressHTML: true` in `astro.config.ts` to
preserve the v6 HTML-aware behavior, keeping inline spacing across the
site unchanged.

## Verification

- `pnpm build` — 595 pages built, complete
- `pnpm typecheck` (astro check) — 0 errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

`ComfyApp.getNodeDefs()` baked resolved text into the def at fetch time,
so `nodeDefStore` held translated values and the raw backend strings
were discarded. Locale switching therefore needed a full refetch.

Converts `ComfyNodeDefImpl.display_name` and `description` to getters
delegating to `resolveNodeDefText`, holding the raw backend value in
instance fields. `translateNodeDef` stops resolving those two (still
does `category`). Property-access shape is preserved, so the ~96 files
reading `display_name` are untouched.

## Review focus

Three hazards were checked empirically rather than assumed, and one is
load-bearing:

- **`#private` fields throw through a Proxy.** Vue wraps store instances
in one, so the raw values use TypeScript `private` (plain own
properties). Using `#private` would have made every store read throw.
- **`Object.assign(this, obj)` throws on setter-less accessors** —
defused by destructuring `display_name`/`description` out.
- **Spread, `JSON.stringify`, `structuredClone` and `Object.keys` all
drop prototype getters.** Every `ComfyNodeDefImpl` consumer was swept;
exactly one structurally copies (`nodeBookmarkStore.buildBookmarkTree`,
via es-toolkit `clone`, which preserves the prototype). A regression
test covers it, since swapping that for a spread would silently blank
every bookmark label.

Perf measured at +0.28µs per read, ~1.5ms across 5000 defs.

Known remainders are tracked in #14777: `node.title` is still baked at
registration, and `nodeDefsByDisplayName` still snapshots its keys.

Fixes #14629

---
Previously merged without review and backed out by
#14791. Re-submitting
for normal review. Rebased onto post-revert `main`; content unchanged.

---------

Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Minor version increment to 1.52.0

**Base branch:** `main`

Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
Bumps the e2e/perf container from `0.0.21` to `0.0.22` across the four
workflow references.

`0.0.21` built from ComfyUI **v0.19.3**, which installs
`comfyui-workflow-templates==0.9.57`. That predates several templates
this repo pins by id, so the e2e backend does not serve them. Verified
directly against the templates package:

| templates version | `image_krea2_turbo_t2i` |
| --- | --- |
| **0.9.57** (0.0.21, ComfyUI v0.19.3) | ❌ **404** |
| 0.11.19 (ComfyUI v0.29.0) | ✅ |
| **0.11.27** (0.0.22, ComfyUI v0.30.0) | ✅ |

`image_krea2_turbo_t2i` is the **first entry in `CURATED_TEMPLATE_IDS`**
and the inpaint tutorial card's thumbnail. It landed upstream
2026-07-21; the image was eleven ComfyUI minors behind.

## Why it went unnoticed

`browser_tests/tests/firstRunTourRolePins.spec.ts` asserted a **count**
— six served passed a `> 2` check — so a curated template being unserved
was invisible. Tightening that to an identity check (#14670) is what
surfaced it, and #14670 is blocked until this lands.

## Risk

> [!CAUTION]
> ~~This is a large backend jump under the whole frontend e2e suite. It
is the point of the change — a minimal bump to v0.29.0 would fix the
immediate symptom and leave the image to fall behind again within weeks
— but if it destabilises the suite, v0.29.0 is the conservative fallback
and still resolves the curated-template gap.~~
>
> **Struck — this was wrong and it was mine.** There is no conservative
fallback. The node-category taxonomy reorg landed between v0.22.0 and
v0.24.0, and `image_krea2_turbo_t2i` first ships with ComfyUI v0.27.0,
so **no version both serves the curated templates and keeps the old
taxonomy.** v0.29.0 carries the identical taxonomy change and the
identical restructured qwen template. See the correction comments below.

Container side: Comfy-Org/comfyui-ci-container#29, merged, image
published as `v0.0.22`.

## Sequence

1. this PR merges → e2e runs against templates 0.11.27
2. ~~#14670 should then pass~~ — **struck, also mine and also wrong.**
The bump makes all pinned templates served, but
`image_qwen_image_edit_2509` was restructured upstream and its
prompt/sink pins had to be corrected here first;
`templates-qwen_multiangle.app` is gone from the package entirely and is
replaced in #14683.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: t <t@t.t>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary

Make the new-test video job succeed safely when newly added specs
contain no tests eligible for its Chromium project, while explaining the
skip in the GitHub Actions workflow summary.

## Changes

- **What**: Run Playwright discovery before recording. A genuine
discovery/configuration error still fails, while the specific zero-test
result skips recording and artifact upload.
- **What**: Add a detailed workflow summary naming the project-routing
mismatch, detected files, discovery output, and continued coverage from
project-specific E2E jobs.
- **What**: Document that Chromium-ineligible specs such as `@perf`-only
files are skipped.

## Review Focus

The zero-test condition is deliberately narrow: only Playwright output
reporting `Total: 0 tests in 0 files` is converted to a successful skip.
Other nonzero discovery exits remain failures.

## Verification

- Confirmed an `@perf`-only spec produces `has-tests=false` and the
expected workflow summary.
- Confirmed a standard Chromium spec produces `has-tests=true`.
- `pnpm exec oxfmt --check .github/workflows/ci-tests-e2e.yaml
browser_tests/README.md`
- `git diff --check`
## Summary

Add repository-local Oxlint rules that prevent automatic Vitest cleanup
regressions and unsafe module-scope mocks.

## Changes

- **What**: Adds binding-aware rules for redundant suite cleanup and
module-scope `vi.stubGlobal`/`vi.spyOn`, real-Oxlint integration tests,
workspace-wide audit coverage, and updated testing guidance.
- **Dependencies**: Stacked on #14839 for the local Oxlint plugin
infrastructure. Promotion from warnings to errors depends on #14836
landing and its existing findings being removed.

## Review Focus

Please focus on lexical binding resolution, hook/module
execution-boundary classification, and coverage of normally ignored and
workspace test roots. The rules intentionally remain non-fixable and
warning-only during the audit phase.

---------

Co-authored-by: Amp <amp@ampcode.com>
## Summary

Updates the /seedance-2.5 hero description to mention 1080p output and
video extension.

## Changes

- **What**: Appended "Now available in 1080p with video extension." (and
the zh-CN equivalent) to `seedance.hero.description` in
`apps/website/src/i18n/translations.ts`.

## Review Focus

Copy-only change; both `en` and `zh-CN` strings updated to stay in sync.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary

Extract behavior-focused regression coverage from #14246 that passes
independently against `main`. This consolidates the former stacked PRs
#15323 and #15325.

## Changes

- Add Playwright coverage for renderer transitions, subgraph layout and
reroutes, node replacement, pinned-node copy/paste, widget persistence,
app-mode validation, and legacy drag performance.
- Add unit coverage for graph removal and disconnection lifecycle,
dynamic-input connections, cyclic subgraph definitions, semantic widget
errors, and mixed drag selections.
- Assert observable behavior and public contracts without depending on
ECS stores or private migration state.

## Review Focus

All coverage is independently mergeable into `main`; tests that require
the ECS migration implementation remain in #14246.

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
@github-actions github-actions Bot added risk:ungraded and removed risk:R3 PR risk grade (advisory shadow check; grader-owned) labels Aug 18, 2026
DrJKL and others added 5 commits August 18, 2026 02:22
## Summary

Make the Vite config compatible with the native config loader by
resolving these warnings:

- `import \"./build/plugins\" resolves to a directory index
(vite.config.mts:20:32). Import the index file directly`
- `import \"./comfyAPIPlugin\" without a file extension
(build/plugins/index.ts:1:32). Add the file extension`

## Changes

- **What**: Import the Comfy API plugin directly with its `.ts`
extension, remove the redundant barrel file, and configure TypeScript
for explicit TypeScript imports in this no-emit project.

## Review Focus

Confirm native config loading succeeds without the directory-index or
extension warnings.

Co-authored-by: Amp <amp@ampcode.com>
## Summary

Simplifies the ended-subscription decision tree introduced in #14438
while preserving current billing-state behavior.

## Changes

- **What**: Removes the single-use terminal-personal computed and
handles explicit, accessible, and inaccessible states in one
`isSubscriptionEnded` computed.
- **Conflict resolution**: Merges current `main` and preserves the later
`canAccessSubscriptionFeatures` contract and ended-Personal subscribe
behavior.
- **Tests**: Retains the current component regression coverage after the
later ended-subscription behavior changes on `main` made the original
strengthened assertion obsolete.

## Review Focus

The refactor remains equivalent to the current `main` expression for
explicit ended, canceled inaccessible, and inactive inaccessible
Personal states.

## Screenshots (if applicable)

Not applicable; this is a behavior-preserving logic refactor with no UI
output changes.

Co-authored-by: dante01yoon <6510430+dante01yoon@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
… dialogs (#15388)

SelectContent and SearchAutocomplete panels used a static z-3000 class,
losing to dialogs registered with the shared auto-incrementing modal
z-index counter once it climbs past 3000 (e.g. long cloud sessions where
the fullscreen Load3D viewer scrim reached 3702+ and covered the Up
Direction dropdown). Apply useModalLiftedZIndex on open, matching
SingleSelect/MultiSelect/Popover, while preserving caller-passed styles.

## Screenshots (if applicable)
before 


https://github.com/user-attachments/assets/6cc828da-2a80-4900-b73a-c26a1e208ce7


after



https://github.com/user-attachments/assets/9ff2ad1d-807e-4d0c-ac7d-5511bae759ce
@socket-security

Copy link
Copy Markdown

@benjcooley

Copy link
Copy Markdown
Contributor Author

Superseded by #15175, which carries the complete ecosystem-matrix change against main. This stacked PR shares the same head but has a stale base, causing duplicate CI and an unrelated website patch-coverage context.

@benjcooley benjcooley closed this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:ungraded size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.