ci: detection proof - poison corpus counter-evidence for the matrix - #15145
ci: detection proof - poison corpus counter-evidence for the matrix#15145benjcooley wants to merge 160 commits into
Conversation
## 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>
## 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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
🎭 Playwright: ✅ 1828 passed, 0 failed · 3 flaky📊 Browser Reports
📦 Bundle Size
⚡ Performance Report
Absolute values
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 |
Codecov Report❌ Patch coverage is ❌ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 693 files with indirect coverage changes 🚀 New features to boost your workflow:
|
|
CI evidence - run 31621967782, all six jobs green:
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.
|
I have read and agree to the Contributor License Agreement |
…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>
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>
## 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
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
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. |
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 newmatrix-detection-proofjob (no corpus/cache dependency, fixture checked in, ~2 min) passes only if:verify_detection.pysees 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), andsummarize_matrix.pyexits FAIL on the poison corpus (loads 85.7% < 95% floor; worst op 85.7% < 99% floor).loadedOk0 + message (gated)beforeRegisterNodeDefthrowshookErrorsregisterCustomNodesthrowshookErrorsonNodeCreatedthrowsload/addNodeop errs (gated)onSerializethrowsserializeop err (gated)wn, counts)Building the proof surfaced two blind channels, handled honestly rather than papered over:
extensionService.invokeExtensions*catches per-extension and console.errors, so hook throws could never fail theregisterNodeDeffield (proven: the hook ran — its prototype side effects fired later — while its throw was swallowed). The runner now records the containment signature as ahookErrorsrow field (deduped, capped 20, radar not gate) and the summary gains acontained hook errorsline.r/streadn/aeven underMATRIX_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 failpnpm typecheck.Local evidence:
DETECTION PROOF PASSED: every channel fired, control clean, 7/7 rows; summarizer exit 1 withVERDICT: FAIL - entry JS loads clean 85.7% < 95%; every operation clean (worst: load) 85.7% < 99%. The PR run'smatrix-detection-proofjob is the CI evidence.Stacked on #15110.