Skip to content

Add change summaries and grouping to the revision browser#999

Open
ericnewcomer wants to merge 8 commits intomainfrom
ericnewcomer/revision-browser-current
Open

Add change summaries and grouping to the revision browser#999
ericnewcomer wants to merge 8 commits intomainfrom
ericnewcomer/revision-browser-current

Conversation

@ericnewcomer
Copy link
Copy Markdown
Member

Summary

  • Render a human-friendly per-revision summary built from structured changes records (verb-first, e.g. "Updated 2 messages and added group"). When >2 distinct change types appear, collapse to a category-level form ("Significantly changed structure and translations").
  • Show the current revision at the top with a "Current" label and no Revert action; the Revert pill matches the Current pill style and the row height stays constant whether either is rendered.
  • Collapse revisions made within a 15-minute window onto their most recent member and merge their changes into the combined summary; revisions that are significant on their own break out into a standalone row.
  • Auto-refresh the list when a new revision is saved while the window is open (skipping refresh while previewing an older revision).
  • Widen the window to 340 and lift max height to 500.

Test plan

  • pnpm test:fast — new revision-summary and existing temba-flow-editor-revisions suites pass
  • Open the revisions window in the editor; confirm summaries, the Current/Revert pills, and the 15-minute collapsing
  • Save a new revision while the window is open and confirm the list refreshes

@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @ericnewcomer's task in 3m 49s —— View job


Review

  • Read changed files
  • Analyze revision-summary.ts logic
  • Analyze RevisionsWindow.ts changes
  • Review test coverage
  • Post inline + summary feedback

Overall the feature is well-scoped and well-tested — revision-summary.ts is a clean, pure helper with thorough unit coverage, and the RevisionsWindow test additions cover the new auto-refresh / preview-suppression / 15-min-collapse paths directly. A few things worth addressing before merge:

Likely bugs

  • Plural reuse for metadata_changed (src/flow/revision-summary.ts:93-97) — METADATA_LABELS defines plural === noun, so two metadata_changed events for the same field render as "Changed 2 name". Either give real plurals or set noCount: true on the phrase.
  • liveRevision previous-value guard is a no-op (src/flow/RevisionsWindow.ts:82-87) — @fromStore seeds the property in connectedCallback before first render, so changes.get('liveRevision') !== undefined is always true on a real change. The check intended to suppress the initial fire isn't doing anything; changes.has('liveRevision') && !this.storeViewingRevision is the real condition. Also worth gating to "value increased" so reverts don't re-fetch.

Concerns

  • No in-flight protection in fetchRevisions (src/flow/RevisionsWindow.ts:205) — now invoked from two paths; an out-of-order resolution can clobber fresh results with stale ones. AbortController or a request-id guard would fix it.
  • Opaque revisions get silently merged (src/flow/RevisionsWindow.ts:217-226) — a non-significant rev with null/missing changes is grouped into a 15-min window and disappears from the visible list. Worth deciding if those should always stand alone.

Diff hygiene

  • test/temba-localization.test.ts has ~16 whitespace-only reformats unrelated to this feature — looks like Prettier churn that should be reverted to keep the diff focused.

Test coverage gaps worth adding

  • A test for the metadata pluralization case (catches the bug above).
  • A test for the 15-min boundary (the comparison is strict <).

Inline comments posted on the relevant lines.
· Branch

Comment thread src/flow/revision-summary.ts Outdated
Comment on lines +93 to +97
const METADATA_LABELS: Record<string, { noun: string; plural: string }> = {
name: { noun: 'name', plural: 'name' },
type: { noun: 'flow type', plural: 'flow type' },
expire_after_minutes: { noun: 'expiration', plural: 'expiration' }
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plurals reuse the singular form here, so when two metadata_changed events share the same field, they group into one phrase with count = 2 and the renderer emits ${verb} ${count} ${plural} — e.g. "Changed 2 name". The grouping key is ${order}:${noun}:${verb}, and field only feeds into noun, so this is reachable any time the backend emits two metadata_changed events for the same field in one revision (or in a 15-min collapsed group). Either give these real plurals (names, flow types, expirations) or set noCount: true on the phrase so the count is suppressed.

Comment thread src/flow/RevisionsWindow.ts Outdated
Comment on lines +82 to +87
const newRevisionWhileOpen =
!this.hidden &&
changes.has('liveRevision') &&
changes.get('liveRevision') !== undefined &&
!this.storeViewingRevision;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes.get('liveRevision') !== undefined is intended to skip the first reactive notification, but liveRevision is seeded in connectedCallback (this[name] = selector(...)) before the first render, so by the time updated() runs the previous value is always defined and this check is a no-op. The real guard you want is "the value actually changed and we're not previewing" — changes.has('liveRevision') && !this.storeViewingRevision is enough. Also worth gating on (changes.get('liveRevision') as number) < this.liveRevision so a revert that lowers the revision number doesn't trigger a refetch.

Comment on lines +217 to +226
const flush = () => {
if (group.length === 0) return;
const head = group[0];
const allChanges: RevisionChange[] = [];
for (const r of group) {
if (r.changes) allChanges.push(...r.changes);
}
result.push({ ...head, changes: allChanges });
group = [];
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a non-significant revision arrives with changes: null (or undefined), it's silently merged into a 15-min group and the resulting summary is computed only from whatever non-null neighbors contributed. The user gets one collapsed row with no visual indicator that an opaque revision happened in that window. Worth deciding whether such revisions should always stand alone (so the user can click in to inspect them) or be tagged in the summary.

Comment thread test/temba-localization.test.ts Outdated
Comment on lines +175 to +178
}
});

editor = await fixture(
html`<temba-flow-editor></temba-flow-editor>`
);
editor = await fixture(html`<temba-flow-editor></temba-flow-editor>`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has ~16 of these whitespace-only reformats and no functional changes — they look like Prettier churn unrelated to the revisions work. Consider reverting to keep the diff focused on the feature.

`/flow/revisions/${this.flow}/?version=${FLOW_SPEC_VERSION}`
);
this.revisions = results.slice(1);
this.revisions = this.collapseRevisions(results);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRevisions is now invoked from two paths (open + auto-refresh on liveRevision change) with no in-flight protection. If a save fires while a previous fetch hasn't returned, results can resolve out of order and the older payload will overwrite the newer one. Consider an AbortController or a request-id guard so the late response is dropped.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 29, 2026

📊 Coverage Report

📊 Coverage Report
==================
Lines:      54465/65078 (83.69%)
Functions:  1429/1886 (75.77%)
Branches:   6255/7867 (79.51%)
==================
✅ Statement Coverage: 83.69% (above 80% threshold)

🎉 Build passed: Statement coverage meets the 80% threshold!

Summary

  • Lines: 83.69%
  • Functions: 75.77%
  • Branches: 79.51%

Coverage Report

@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @ericnewcomer's task in 2m 32s —— View job


Re-review after 765e6ce

  • Inspect new commits since last review
  • Re-check the four issues raised previously
  • Read changed files
  • Post inline + summary feedback

All four items from the prior review are addressed cleanly, and the bonus 15-min boundary test I'd asked for is in too. Nothing blocking — this looks ready to merge from my side.

Resolved

Prior comment Fix Test
metadata_changed plural reuse → "Changed 2 name" (revision-summary.ts:93-97) METADATA_LABELS simplified to { noun }; metadata_changed and base_language_changed phrases now carry noCount: true, and the renderer in summarizeChanges (revision-summary.ts:235-239) drops the count when noCount is set test/revision-summary.test.ts:127-134"Changed name" for two repeats
liveRevision previous-value guard was a no-op (RevisionsWindow.ts:82-87) Replaced with previousRevision !== undefined && this.liveRevision > previousRevision (RevisionsWindow.ts:83-89) — correctly skips initial seeding and reverts that lower the number test/temba-flow-editor-revisions.test.ts:306-327
No in-flight protection in fetchRevisions (RevisionsWindow.ts:205) fetchRequestId request-id guard at RevisionsWindow.ts:202-219; stale resolutions/errors short-circuit and only the latest call resets isLoading (covered indirectly; no dedicated test, see below)
Opaque revisions silently merged into 15-min groups (RevisionsWindow.ts:217-226) RevisionsWindow.ts:236-244!rev.changes || rev.changes.length === 0 now also forces a flush + standalone push, alongside the existing significant-change branch test/temba-flow-editor-revisions.test.ts:198-228
Whitespace churn in test/temba-localization.test.ts Fully reverted — git diff origin/main...HEAD -- test/temba-localization.test.ts is now empty n/a
15-min strict-< boundary (suggested) n/a — behavior was already correct test/temba-flow-editor-revisions.test.ts:166-193

Minor leftovers (non-blocking)

  • No direct unit test for the request-id guard. The race is straightforward enough that I'd skip it, but if you want to harden it, two pending stubs resolved out of order would do the job.
  • previousRevision !== undefined is technically redundant — 7 > undefined evaluates to false via NaN — but the explicit check reads more clearly, so leaving it is fine.
  • Errors from fetchRevisions remain silent (just console.error, no UI signal). Pre-existing behavior, not a regression.
    · Branch

@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @ericnewcomer's task in 1m 52s —— View job


Re-review after 884de35

  • Inspect the new commit since last review
  • Read changed files
  • Verify test stub fix is sound
  • Post inline + summary feedback

The new commit is a tightly-scoped test fix and is correct. Nothing else changed; my prior re-review of 765e6ce still stands and nothing here is blocking.

Why the fix is rightResponse bodies are one-shot streams. With fetchStub.resolves(sharedResponse), every fetch() call returns the same Response, and the second .json() (or any consume) throws "body stream already read". The three modified tests drive fetchRevisions more than once via reactive state changes after hidden = false, so the second call hit that error. As the commit message notes, the assertion already passed before the rejection surfaced, so the test body looked green while the runner failed on the unhandled rejection. Switching to callsFake(async () => new Response(...)) hands out a fresh Response per invocation — the standard sinon pattern for fetch stubs that may be hit more than once.

Scope is appropriate — the older fetchStub.resolves(mockResponse) sites (test/temba-flow-editor-revisions.test.ts:66, 81, 100, 154, 186, 222, 269) each call fetchRevisions() exactly once, so they don't need the change. Leaving them alone keeps the diff minimal.

Tiny nit (non-blocking)async () => new Response(...) is fine; () => Promise.resolve(new Response(...)) would be marginally more idiomatic since there's no await in the body, but no reason to churn the diff.

Net: looks ready to merge.

· Branch ·

Show the current revision at the top of the list (labeled, no Revert
action). Render a human-friendly summary for each revision built from
the structured `changes` records on the wire, joined verb-first and
collapsing into a "Significantly changed ..." form when many things
moved at once. Collapse revisions made within a 15-minute window onto
their most recent member, merging their changes into a combined
summary, and let significant revisions break out as standalone rows.
Refresh the list automatically when a new revision is saved while the
window is open. Widen the window to 340 and lift its max height to 500.
- Suppress count for metadata and base_language phrases so two events
  for the same field don't render as "Changed 2 name".
- Replace the no-op `previousRevision !== undefined` guard with one
  that requires the live revision to actually increase, so reverts
  (which lower the revision number transiently) don't refetch.
- Add a request-id guard around fetchRevisions so concurrent calls
  can't resolve out of order and clobber fresher results.
- Always show revisions with no recorded changes as standalone rows
  so the user can click in to inspect them.
- Revert unrelated whitespace reformats in temba-localization.test.ts.
- Add regression tests covering each fix.
Reusing a single Response across multiple fetch invocations triggers
'body stream already read' on the second consume — which surfaces as
an unhandled promise rejection outside the test and fails the CI
runner even though the assertions themselves pass.
@ericnewcomer ericnewcomer force-pushed the ericnewcomer/revision-browser-current branch from 884de35 to 6f4469e Compare April 29, 2026 21:29
@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @ericnewcomer's task in 1m 19s —— View job


Re-review after 6f4469e

  • Inspect new commits since last review
  • Read changed files
  • Verify prior review items remain addressed
  • Post inline + summary feedback

The synchronize event refreshed commit SHAs (likely a rebase to update dates), but the diff content is unchanged from what I reviewed in the previous comment — git show 6f4469e is the same test-stub fix, and RevisionsWindow.ts / revision-summary.ts / the test files match the state I last looked at.

State of prior review items

Item Status Where to confirm
metadata_changed plural reuse → "Changed 2 name" Fixed src/flow/revision-summary.ts:93-97, renderer at :235-239; test test/revision-summary.test.ts:127-134
liveRevision previous-value guard was a no-op Fixed — also gated to "value increased" src/flow/RevisionsWindow.ts:83-89; tests test/temba-flow-editor-revisions.test.ts:284-327
No in-flight protection in fetchRevisions Fixed via fetchRequestId src/flow/RevisionsWindow.ts:202-219
Opaque revisions silently merged into 15-min groups Fixed — opaque revs now stand alone src/flow/RevisionsWindow.ts:236-244; test test/temba-flow-editor-revisions.test.ts:196-229
Whitespace churn in test/temba-localization.test.ts Reverted git diff origin/main...HEAD -- test/temba-localization.test.ts is empty
15-min strict-< boundary Test added test/temba-flow-editor-revisions.test.ts:166-194

Nothing new to flag. No fresh code changes since my last re-review, so the same conclusion holds: this looks ready to merge from my side. Non-blocking leftovers (no dedicated test for the request-id race, silent console.error on fetch failure) are unchanged and pre-existing.

· Branch ·

Auto-refresh on revision changes can fire fetchRevisions during
fixture setup or unrelated store mutations, and an unstubbed fetch
returns undefined which throws in the .then chain. A safe default
keeps unrelated triggers quiet; tests that need a specific payload
still override via callsFake/resolves.
@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @ericnewcomer's task in 2m 27s —— View job


Re-review after 2a5eebd

  • Inspect new commits since last re-review
  • Read changed files
  • Verify prior review items remain addressed
  • Post inline + summary feedback

The only new commit since my last re-review is 2a5eebd, a 7-line test-setup change that adds a default callsFake in beforeEach returning { results: [] }. Nothing else changed. No new code or behavior changes since 765e6ce — all prior review items remain resolved.

Why the new fix is right — Reactive liveRevision updates can fire fetchRevisions from places other than the explicit test calls (e.g. zustand state left over from a prior test, or store mutations during fixture setup). Without a default, the unstubbed fetch returns undefined and .then(r => r.json()) in fetchResults throws. Having beforeEach install a benign { results: [] } default is the right shape: tests that need a specific payload still override via fetchStub.resolves(...) or another callsFake, and sinon's resolves/callsFake replace the default behavior.

One thing worth knowing (non-blocking)fetchStub.resolves(mockResponse) in the existing tests (e.g. test/temba-flow-editor-revisions.test.ts:73, 88, 107, 161, 193, 229, 276) still hands out the same Response object on every call. That's only safe because each of those tests calls fetchRevisions() exactly once and hidden stays true, so no auto-refresh fires. If a future test triggers a second fetch under one of these blocks, the second .json() will throw body stream already read again. Could be hardened by switching all of these to the same callsFake(async () => new Response(...)) pattern the auto-refresh tests already use, but it's not a bug today.

State of prior review items (unchanged from last re-review)

Item Status
metadata_changed plural reuse → "Changed 2 name" Fixed (src/flow/revision-summary.ts:93-97, :235-239)
liveRevision previous-value guard was a no-op Fixed and gated on increase (src/flow/RevisionsWindow.ts:83-89)
No in-flight protection in fetchRevisions Fixed via fetchRequestId (src/flow/RevisionsWindow.ts:202-219)
Opaque revisions silently merged into 15-min groups Fixed — opaque revs stand alone (src/flow/RevisionsWindow.ts:236-244)
Whitespace churn in test/temba-localization.test.ts Reverted
15-min strict-< boundary test Added (test/temba-flow-editor-revisions.test.ts:173-201)

Looks ready to merge from my side. Pre-existing non-blocking leftovers (no dedicated unit test for the request-id race, silent console.error on fetch failure) are unchanged.

· Branch ·

Replace the @fromstore decorator on RevisionsWindow with a manual
zustand.subscribe that activates only while the window is open and
disconnects on close (or on disconnectedCallback). This avoids
keeping a live store subscriber on every editor instance regardless
of whether the user ever views revisions, and trims cross-test
reactive cascades during fixture churn.
@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @ericnewcomer's task in 3m 10s —— View job


Re-review after 3827819

  • Inspect new commits since last re-review
  • Read changed files
  • Verify prior review items remain addressed
  • Post inline + summary feedback

The new commit replaces the two @fromStore decorators with a manual zustand.subscribe(selector, listener) that activates only while the window is open and tears down on close (or disconnectedCallback). It's a clean refactor — nothing blocking.

Why the change is sound

  • AppState.ts:266 wraps the store in subscribeWithSelector, so the two-arg (selector, listener) form is supported and listeners only fire on actual selector-output changes (no initial firing). That removes the need for the previous "skip first notification" guard entirely.
  • The original current > previous and !viewingRevision checks are preserved inside the listener (RevisionsWindow.ts:96-101) — reverts that lower the revision number and active previews still don't trigger refetches.
  • disconnectedCallback calls unwatchRevisions (RevisionsWindow.ts:71-74), so the subscription doesn't outlive the element.
  • The double-subscribe guard in watchRevisions (RevisionsWindow.ts:92) means rapid open/close toggles can't stack listeners.
  • @fromStore is fully removed from this file (grep confirms), and the import in RevisionsWindow.ts:9 no longer pulls it in.

Behavioral nuance worth knowing (not a bug) — the open branch still keys on changes.get('hidden') === true, so an element constructed with hidden=false from the start won't auto-fetch or auto-subscribe on first render. That's pre-existing and unchanged by this commit; the "open by toggling hidden true→false" path is the only intended entry.

Test coverage

  • The existing auto-refresh tests (test/temba-flow-editor-revisions.test.ts:291-357) still pass against the new subscription model — they set hidden = false, mutate zustand.setState({ flowDefinition: { revision: ... } }), and assert call counts. That covers the open + auto-refresh + viewing-revision-suppression + revision-decrease paths against the new code path unchanged.
  • Gap: no direct test for the close→unwatch contract (toggle hidden back to true, then mutate revision, assert no fetch). Posted inline. Non-blocking.

State of prior review items (unchanged from previous re-review)

Item Status
metadata_changed plural reuse → "Changed 2 name" Fixed (src/flow/revision-summary.ts:93,108-117,236)
liveRevision previous-value guard was a no-op Now superseded — manual subscription with current > previous (RevisionsWindow.ts:91-104)
No in-flight protection in fetchRevisions Fixed via fetchRequestId (RevisionsWindow.ts:219-236)
Opaque revisions silently merged into 15-min groups Fixed — opaque revs stand alone (RevisionsWindow.ts:253-261)
Whitespace churn in test/temba-localization.test.ts Reverted (git diff origin/main...HEAD -- test/temba-localization.test.ts is empty)
15-min strict-< boundary test Added (test/temba-flow-editor-revisions.test.ts)

Looks ready to merge from my side. Pre-existing non-blocking leftovers: no direct unit test for the request-id race, silent console.error on fetch failure, and now the missing close→unwatch test noted inline.

· Branch ·

Comment thread src/flow/RevisionsWindow.ts Outdated
Comment on lines +79 to +88
if (
changes.has('hidden') &&
!this.hidden &&
changes.get('hidden') === true
) {
this.fetchRevisions();
this.watchRevisions();
} else if (changes.has('hidden') && this.hidden) {
this.unwatchRevisions();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The headline behavior of this commit — "subscription is torn down on close, so no refetch fires after the user closes the window" — isn't directly tested. The existing auto-refresh tests only cover the open path. A test that toggles hidden back to true and then mutates flowDefinition.revision, asserting fetchStub.callCount stays at zero, would lock in the close→unwatch contract. Non-blocking.

The revisions tests mutate the singleton zustand store (setting
viewingRevision and flowDefinition.revision) to exercise auto-refresh
paths. Without an explicit reset, leftover state can affect subsequent
test files that share the same store, producing intermittent failures
in unrelated suites.
@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @ericnewcomer's task in 1m 56s —— View job


Re-review after e56ab83

  • Inspect new commits since last re-review
  • Read changed files
  • Verify prior review items remain addressed
  • Post inline + summary feedback

The only new commit since my last re-review is e56ab83, a 5-line test hygiene fix that resets viewingRevision and flowDefinition to their initial values in afterEach. No source changes — all prior items remain resolved.

Why the fix is rightzustand (src/store/AppState.ts:265) is a module-singleton, so mutations from flowDefinition: { revision: N } and viewingRevision: true in this file's tests (test/temba-flow-editor-revisions.test.ts:302,310,324,333,347,355) persist across files unless explicitly reset. The reset values (viewingRevision: false, flowDefinition: null) match the store's initial state at AppState.ts:274,279, so this faithfully restores defaults. Plausible cause of the intermittent failures the commit message mentions.

Order is saferestore() runs before the setState, so by the time the state mutates, fetchStub is gone and window.fetch is real. That could be a footgun if the new state change fired the auto-refresh listener, but it can't:

  1. The selector is state.flowDefinition?.revision ?? 0 (RevisionsWindow.ts:94); going from e.g. 5null (which evaluates to 0) makes current > previous false, so the listener short-circuits.
  2. viewingRevision: false doesn't intersect the revision selector.
  3. The fixture's element from the prior test is disconnected by @open-wc/testing cleanup, which fires disconnectedCallbackunwatchRevisions (RevisionsWindow.ts:71-74), so there's no live subscriber anyway.

So no real fetch can leak out of the reset.

Minor (non-blocking) — Only two keys are reset. If a future test in this file mutates other state (e.g. dirtyDate, isTranslating, flowInfo, activity), those would leak again. Not a problem today — grep confirms only these two are touched in this file — but if leakage recurs, a more thorough reset or zustand.setState(zustand.getInitialState(), true) would be sturdier. Not worth churning the diff for now.

State of prior review items (unchanged from previous re-review)

Item Status
metadata_changed plural reuse → "Changed 2 name" Fixed (src/flow/revision-summary.ts:93,108-117,236)
liveRevision previous-value guard was a no-op Superseded — manual zustand.subscribe with current > previous (RevisionsWindow.ts:91-104)
No in-flight protection in fetchRevisions Fixed via fetchRequestId (RevisionsWindow.ts:202-219)
Opaque revisions silently merged into 15-min groups Fixed — opaque revs stand alone (RevisionsWindow.ts:236-244)
Whitespace churn in test/temba-localization.test.ts Reverted
15-min strict-< boundary test Added

Looks ready to merge from my side. Pre-existing non-blocking leftovers (no direct unit test for the request-id race, silent console.error on fetch failure, missing close→unwatch test) are unchanged.

· Branch ·

Replace the zustand subscription with an imperative refresh() method
on RevisionsWindow that the Editor's saveChanges callback invokes
after a successful save. This removes the cross-test reactive cascade
risk from a singleton-store subscription and keeps the auto-refresh
behavior exactly where it's needed.
@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @ericnewcomer's task in 1m 45s —— View job


Re-review after 4f0262f

  • Inspect new commits since last re-review
  • Read changed files
  • Verify prior review items remain addressed
  • Post inline + summary feedback

This commit replaces the zustand subscription with an imperative RevisionsWindow.refresh() called directly from Editor.saveChanges after a successful save. It's a clean simplification — nothing blocking.

Why the new model is sound

  • Editor.ts:1601 calls this.getRevisionsWindow()?.refresh() only inside the success branch (after the response.status check), so failed saves don't trigger refetches. Optional chaining handles the case where the window isn't in the DOM yet.
  • RevisionsWindow.refresh() (RevisionsWindow.ts:70-74) gates on !this.hidden, so it's a no-op when closed. That removes the need to manage subscription lifecycle on open/close — a meaningful net reduction (~24 lines of watchRevisions/unwatchRevisions/disconnectedCallback gone).
  • fetchRequestId in-flight protection (RevisionsWindow.ts:194-209) is preserved, so back-to-back refresh + open-fetch can't race.
  • The hidden transition still triggers the initial fetch via updated() (RevisionsWindow.ts:76-85), so opening the window still loads the list.
  • afterEach zustand reset from e56ab83 is correctly removed — without the subscription there's no shared-singleton cross-test bleed to clean up.

Behavioral nuance — refresh while previewing
The previous subscription explicitly suppressed refetch while viewingRevision was set; refresh() does not. In practice this is unreachable from the only caller: handleRevertClick (RevisionsWindow.ts:319-338) sets this.viewingRevision = null before firing the revert event that triggers the save, and there's no other path that drives saveChanges while previewing (the preview path doesn't dirty the document). So the prior guard is effectively redundant under the new model. If refresh() ever gains another caller, that assumption may need to be revisited — but it's fine today.

Test coverage

  • New tests cover both branches of refresh() directly (test/temba-flow-editor-revisions.test.ts:290-317). Good.
  • The deleted does not refresh when the revision number decreases and does not refresh when previewing an older revision tests are correctly retired — those guards lived in the subscription, which is gone.
  • Gap: there's no integration test that exercises Editor.saveChanges() → refresh(). That'd lock in the wiring (Editor.ts:1601), since a future refactor could quietly drop the call and the unit tests on refresh() itself would still pass. Non-blocking, but worth considering. Posted inline.

State of prior review items

Item Status
metadata_changed plural reuse → "Changed 2 name" Fixed (src/flow/revision-summary.ts:93,108-117,236)
liveRevision previous-value guard was a no-op Superseded — replaced by imperative call from saveChanges (Editor.ts:1601)
No in-flight protection in fetchRevisions Fixed via fetchRequestId (RevisionsWindow.ts:193-209)
Opaque revisions silently merged into 15-min groups Fixed — opaque revs stand alone (RevisionsWindow.ts:230-235)
Whitespace churn in test/temba-localization.test.ts Reverted
15-min strict-< boundary test Added (test/temba-flow-editor-revisions.test.ts:172-200)

Looks ready to merge from my side. Pre-existing non-blocking leftovers: silent console.error on fetch failure, and the missing saveChanges → refresh integration test noted above.

· Branch ·

Comment thread src/flow/Editor.ts
getStore().getState().setDirtyDate(null);

// Refresh the revisions list if it's currently open.
this.getRevisionsWindow()?.refresh();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test exercises this exact wiring (Editor.saveChanges() → getRevisionsWindow()?.refresh()). The unit tests on refresh() in test/temba-flow-editor-revisions.test.ts:290-317 confirm the method behaves correctly when called, but a future refactor that drops this call site would still pass. An integration-level test that stubs getStore().postJSON to resolve a successful response, opens the revisions window, triggers saveChanges(), and asserts fetchStub.callCount increased after the save would lock in the contract. Non-blocking.

test/utils.test.ts is a shared helper file — it exports test utilities
and registers a global mocha before()/after() pair via module-level
side effects, but has no describe()/it() blocks of its own. The
*.test.ts naming caused wtr to load it as its own zero-test session
that never cleanly signaled completion. With one more test file in
the suite (revision-summary.test.ts), this pushed wtr past its
tolerance and the coverage run silently exited without finishing the
last batch of files. Excluding the file from the glob makes the run
deterministic; the exported helpers and registered hooks still load
via the imports already in every test file that depends on them.

This was the consistent CI red on PR #999 — the run reaches 108-120 of
122 files and ELIFECYCLEs without a specific test failure. Locally
reproduced and bisected: removing any single file makes the suite
pass; removing utils.test.ts specifically is the correct fix because
it isn't actually a test file.
@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @ericnewcomer's task in 1m 44s —— View job


Re-review after 69821fa

  • Inspect new commits since last re-review
  • Read changed files
  • Verify prior review items remain addressed
  • Post inline + summary feedback

The only new commit since the last re-review is 69821fa, a one-line web-test-runner.config.mjs change that excludes test/utils.test.ts from the wtr file glob. No source or test-source changes — all prior review items remain resolved.

Why the fix is right

  • test/utils.test.ts is genuinely a helper module, not a test file. grep -L -E 'describe\(|it\(' test/*.test.ts returns only test/utils.test.ts — every other *.test.ts in the suite has at least one describe or it block. So the exclusion's scope is correct: it strips exactly the one file that produces a zero-test wtr session, and nothing else.
  • The helper's runtime contract is preserved. Its global before(...) (test/utils.test.ts:130-146) and after(...) (:148-150) hooks register at module-load time, and 48 other test files import from ./utils.test (grep -lr "from './utils.test'" test), so the import chain still pulls the side effects (mocha hooks, style override, resize-listener suppression) into every session that needs them. The commit message's claim "the exported helpers and registered hooks still load via the imports already in every test file that depends on them" checks out.
  • coverageConfig.include is scoped to src/** (web-test-runner.config.mjs:347), so excluding a test-side file from the glob has no effect on coverage measurement.
  • The glob array form ['**/test/**/*.test.ts', '!**/test/utils.test.ts'] is the standard globby/wtr negation pattern.

Minor (non-blocking) — A rename to e.g. test/utils.ts would exclude this file naturally without a per-file glob entry, and would self-document the fact that it isn't a test. The trade-off is touching all 48 importers; the glob exclusion is the lower-blast-radius fix, which is a reasonable call for a CI hotfix. Not worth churning the diff for.

State of prior review items (unchanged from previous re-review)

Item Status
metadata_changed plural reuse → "Changed 2 name" Fixed (src/flow/revision-summary.ts:93,108-117,236)
liveRevision previous-value guard was a no-op Superseded — replaced by imperative call from saveChanges (Editor.ts:1601)
No in-flight protection in fetchRevisions Fixed via fetchRequestId (RevisionsWindow.ts:193-209)
Opaque revisions silently merged into 15-min groups Fixed — opaque revs stand alone (RevisionsWindow.ts:230-235)
Whitespace churn in test/temba-localization.test.ts Reverted
15-min strict-< boundary test Added (test/temba-flow-editor-revisions.test.ts:172-200)

Looks ready to merge from my side. Pre-existing non-blocking leftovers: silent console.error on fetch failure, missing saveChanges → refresh integration test.

· Branch · ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant