Skip to content

Proj/setting auto update#696

Open
bao-byterover wants to merge 14 commits into
mainfrom
proj/setting-auto-update
Open

Proj/setting auto update#696
bao-byterover wants to merge 14 commits into
mainfrom
proj/setting-auto-update

Conversation

@bao-byterover
Copy link
Copy Markdown
Collaborator

Summary

  • Problem: brv-cli runs update-notifier at every init (polls npm registry, shows interactive y/n prompt when outdated). Separately, @oclif/plugin-update schedules brv update --autoupdate in the background on a 1ms debounce. Users have no way to opt out of either path.
  • Why it matters: v4 is a breaking release. Users should decide when to upgrade, not be surprised by background updates or interrupted by unsolicited prompts.
  • What changed:
    1. New init hook block-autoupdate-when-off exits silently (process.exit(0)) when commandId === 'update' AND argv contains --autoupdate AND the setting is off. Stops plugin-update's background autoupdate without noisy errors.
    2. update-notifier init hook now wraps its existing logic in shouldRunUpdateCheck(), which combines: the new setting gate, the anti-recursion guard (commandId !== 'update'), and the dev-env guard (BRV_ENV !== 'development').
    3. New sync helper checkForUpdatesSetting() reads the user's settings.json. Default is true (back-compat). Returns false only when the file explicitly contains update.checkForUpdates: false.
  • What did NOT change (scope boundary):
    • Manual brv update flow: tarball still works, npm still errors with existing "use npm update -g" message.
    • Interactive y/n prompt and execute-on-y path: still fully functional when setting is ON.
    • TUI version logo ((latest) / (outdated) tags): still polls and shows tags regardless of the setting. Deferred per team decision.
    • block-command-update-npm hook: untouched. Existing aggressive block on brv update for npm installs is preserved.
    • update-notifier package's internal background HTTP cache refresh: unchanged.

Type of change

  • Bug fix
  • New feature
  • Refactor (no behavior change)
  • Documentation
  • Test
  • Chore (build, dependencies, CI)

Scope (select all touched areas)

  • TUI / REPL
  • Agent / Tools
  • LLM Providers
  • Server / Daemon
  • Shared (constants, types, transport events)
  • CLI Commands (oclif)
  • Hub / Connectors
  • Cloud Sync
  • CI/CD / Infra

Linked issues

  • Closes ENG-2895
  • Related: ENG-2889 (T1 registry boolean), ENG-2890 (T2 store v2), ENG-2891 (T3 transport validation), ENG-2892 (T4 oclif rendering), ENG-2893 (T5 TUI toggle)

Root cause (bug fixes only, otherwise write N/A)

N/A (new feature)

Test plan

  • Coverage added:
    • Unit test
    • Integration test
    • Manual verification only
  • Test file(s):
    • test/unit/oclif/lib/check-for-updates-setting.test.ts (10 specs)
    • test/hooks/init/block-autoupdate-when-off.test.ts (7 specs)
    • test/hooks/init/update-notifier.test.ts (+5 new shouldRunUpdateCheck specs, 9 existing preserved)
  • Key scenario(s) covered:
    • File missing, invalid JSON, wrong shape, wrong type all fall back to default true
    • Setting OFF + brv update --autoupdate exits silently
    • Setting ON + autoupdate passes through (plugin-update proceeds on tarball, existing npm block fires on npm)
    • Anti-recursion guard (commandId === 'update' in notifier hook)
    • Dev-env guard (BRV_ENV=development)
    • Setting OFF + non-update command skips entire notifier hook (zero HTTP, zero prompt)

User-visible changes

  • New setting update.checkForUpdates (boolean, default true) is now visible in brv settings list, brv settings get, brv settings set, and brv settings reset.
  • When the user sets update.checkForUpdates false:
    • Background brv update --autoupdate (spawned by plugin-update) silently exits without performing an update.
    • Init-time update-notifier hook returns early. No y/n prompt, no npm registry HTTP poll, no notification banner.
    • User can still run brv update manually (the manual escape hatch is unaffected).
  • When the user keeps the default (true): no behavior change from today.

Evidence

End-to-end verification on the dev build (./bin/run.js) with two temp data dirs:

Scenario Exit code Elapsed Stdout/Stderr
Setting OFF + brv update --autoupdate 0 675 ms (none, silent)
Setting ON + brv update --autoupdate 1 1569 ms downstream block-command-update-npm fires with the existing npm-install error
Setting OFF + brv --version 0 normal version string only, no update notifier output
Setting OFF + brv --help 0 normal help text only, no update notifier output

Unit-test RED then GREEN per TDD; final unit-test count: 8197 passing, 0 failing.

Checklist

  • Tests added or updated and passing (npm test: 8197 passing, 0 failing, 16 pending)
  • Lint passes (npm run lint: 0 errors, 234 pre-existing warnings)
  • Type check passes (npm run typecheck: 0 errors)
  • Build succeeds (npm run build: tsc + vite both clean)
  • Commits follow Conventional Commits format
  • Documentation updated (QA flow posted as comment on ENG-2895)
  • No breaking changes (setting defaults to true, preserves today's behavior)
  • Branch is up to date with main (branch parent is proj/setting-auto-update; rebase on merge to main)

Risks and mitigations

  • Risk: Init-hook ordering. block-autoupdate-when-off is registered first in package.json. If a future refactor reorders the hooks so this one fires after block-command-update-npm, the npm-install + setting OFF + autoupdate path would log an exit-1 error from block-command-update-npm instead of exiting silently. The actual update is still blocked, only the cosmetics regress.
    • Mitigation: Hook order is asserted in the QA flow doc on ENG-2895. The shipped behavior is robust because block-command-update-npm also blocks the actual update on npm installs, so no real autoupdate runs regardless of order.
  • Risk: Sync fs.readFileSync of settings.json at init time adds latency to every brv invocation.
    • Mitigation: Single read of a small file; measured well below 10 ms. The notifier path it replaces was already doing a similar npm list -g execSync, so net startup cost is comparable.
  • Risk: A user manually edits settings.json to an invalid JSON shape.
    • Mitigation: The helper try/catches every parse step and falls back to true (current behavior preserved). 10 unit specs cover the edge cases (missing file, malformed JSON, wrong top-level type, wrong values type, wrong value type per key).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 23, 2026

Claude finished @bao-byterover's task in 4m 25s —— View job


Claude PR Review

Overall score: 4.5 / 5

A focused, well-scoped feature with strong TDD discipline, clean dependency-injection boundaries, and well-articulated scope guarantees (manual brv update, the existing npm block, and the version-logo tag are all preserved). Behavior gates are pure and individually testable. Two non-blocking quality issues and one defensive nit — see inline comments.

Strengths

  • Hook composition (update-notifier.ts:60-64): the new shouldRunUpdateCheck extracts the three independent conditions (setting-off, anti-recursion, dev-env) into one function with explicit short-circuit ordering. handleUpdateNotification itself stays pure (no env / disk side effects). Tests at test/hooks/init/update-notifier.test.ts:212-258 cover each gate.
  • Silent-exit semantics in block-autoupdate-when-off: process.exit(0) is the right escape — plugin-update sees a normal termination and doesn't retry or log. Hook is registered first in package.json:159-164 so it precedes block-command-update-npm. Argv check argv.includes('--autoupdate') is robust to flag ordering.
  • TDD trail: 88-line unit test for checkForUpdatesSetting covers missing file, invalid JSON, wrong top-level type, wrong values shape, and wrong value type. Each fallback path is asserted, not assumed.
  • WebUI/TUI consistency: both surfaces now filter the "restart required" banner by per-row restartRequired, so boolean toggles like update.checkForUpdates (registry sets restartRequired: false) don't trigger a misleading prompt. See restart-banner-store.ts:14 and settings-page.tsx:38-46.
  • Schema migration: v1→v2 migration in file-settings-store.ts:181-183 is idempotent and forward-compatible (unknown future versions are left untouched), which matches the stated "newer brv's v3 file isn't silently downgraded" intent.

Issues raised inline

  • (non-blocking, low impact) Hardcoded default in checkForUpdatesSetting — the helper returns literal true on every fallback path instead of importing UPDATE_CHECK_FOR_UPDATES_DEFAULT. CLAUDE.md explicitly says descriptors reference constants so a default change propagates; this helper sits outside that wiring and could drift.
  • (non-blocking, low impact) FileSettingsStore.get/list hardcodes restartRequired: true for every returned SettingItem, ignoring the descriptor. The handler currently bypasses this by rebuilding DTOs from the descriptor, so the wire format is correct and no user-visible bug exists today. But the store's contract is leaking misleading data — worth tightening to source from the descriptor.
  • (question) Hook ordering test guard — the PR body acknowledges that reordering block-autoupdate-when-off after block-command-update-npm would regress to a noisy exit-1. A small test asserting the package.json hook array order would be a cheap CI tripwire.
  • (nitpick) fs.readFileSync size guard — defense in depth against a pathologically large settings.json on the synchronous startup path. Skip if overkill.

Risks already mitigated

  • Setting default true preserves today's behavior — no breaking change to existing users.
  • Try/catch in checkForUpdatesSetting falls back to the safe default for every parse / shape / type error.
  • Manual brv update (no --autoupdate) is untouched — user retains the escape hatch.

Todos

  • Read PR diff and changed files
  • Review hook ordering and core logic (block-autoupdate-when-off, update-notifier)
  • Review checkForUpdatesSetting helper
  • Review settings registry / store / validator changes
  • Review WebUI/TUI surfaces
  • Post inline comments + summary
    · Branch

Comment on lines +22 to +37
export function checkForUpdatesSetting(): boolean {
try {
const path = join(getGlobalDataDir(), SETTINGS_FILENAME)
if (!existsSync(path)) return true

const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
if (!isRecord(parsed)) return true
if (!isRecord(parsed.values)) return true

const value = parsed.values[SETTINGS_KEY]
if (typeof value !== 'boolean') return true
return value
} catch {
return true
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (non-blocking): The fallback values (return true on lines 25, 28, 29, 32, 35) hardcode true, duplicating the default that the registry sources from UPDATE_CHECK_FOR_UPDATES_DEFAULT in src/server/constants.ts:137.

Per CLAUDE.md: "Descriptors in server/core/domain/entities/settings.ts reference src/constants.ts so a constant change updates the setting's default automatically." This helper bypasses that wiring, so if the team ever flips the default to false (or makes it env-conditional) the constant will change but this helper will silently keep returning true — the very drift the registry pattern was designed to prevent.

Suggested change
export function checkForUpdatesSetting(): boolean {
try {
const path = join(getGlobalDataDir(), SETTINGS_FILENAME)
if (!existsSync(path)) return true
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
if (!isRecord(parsed)) return true
if (!isRecord(parsed.values)) return true
const value = parsed.values[SETTINGS_KEY]
if (typeof value !== 'boolean') return true
return value
} catch {
return true
}
}
import {UPDATE_CHECK_FOR_UPDATES_DEFAULT} from '../../server/constants.js'
export function checkForUpdatesSetting(): boolean {
try {
const path = join(getGlobalDataDir(), SETTINGS_FILENAME)
if (!existsSync(path)) return UPDATE_CHECK_FOR_UPDATES_DEFAULT
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
if (!isRecord(parsed)) return UPDATE_CHECK_FOR_UPDATES_DEFAULT
if (!isRecord(parsed.values)) return UPDATE_CHECK_FOR_UPDATES_DEFAULT
const value = parsed.values[SETTINGS_KEY]
if (typeof value !== 'boolean') return UPDATE_CHECK_FOR_UPDATES_DEFAULT
return value
} catch {
return UPDATE_CHECK_FOR_UPDATES_DEFAULT
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

Note: the suggestion above adds an import that this file doesn't currently have — apply the import line separately if you accept it.

Comment on lines +21 to +26
export function handleBlockAutoupdateWhenOff(deps: BlockAutoupdateWhenOffDeps): void {
if (deps.commandId !== 'update') return
if (!deps.argv.includes('--autoupdate')) return
if (checkForUpdatesSetting()) return
deps.exitFn(0)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question (non-blocking): Hook ordering — the PR description (and package.json:159-164) registers block-autoupdate-when-off before block-command-update-npm, which is critical: if reversed, the npm-install + setting-off + autoupdate path would log a noisy exit-1 instead of the intended silent exit.

There's no test guard that asserts this ordering. Given the risk you've already documented in the PR body ("If a future refactor reorders the hooks..."), would it be worth adding an integration-style test that reads package.json and asserts block-autoupdate-when-off precedes block-command-update-npm in oclif.hooks.init? Cheap to write, gives you a CI tripwire if anyone alphabetizes or reorders the array.

const path = join(getGlobalDataDir(), SETTINGS_FILENAME)
if (!existsSync(path)) return true

const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): readFileSync is used here without a size guard. If a user (or a corrupted writer) ever produces a multi-megabyte settings.json, this synchronous read runs on every invocation of brv. The PR body notes "single read of a small file; measured well below 10 ms" — which is true for the happy path — but the test suite doesn't pin that assumption.

Strictly optional: add an fs.statSync size check and bail to default if the file is unexpectedly large (e.g. > 1 MB). The current try/catch already handles parse failures, so this is purely defense-in-depth against a pathologically slow startup. Skip if you think it's overkill.

Comment on lines +60 to +64
export function shouldRunUpdateCheck(args: {commandId: string | undefined}): boolean {
if (args.commandId === 'update') return false
if (process.env.BRV_ENV === 'development') return false
return checkForUpdatesSetting()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

praise: Clean three-condition gate with explicit short-circuit ordering — commandId === 'update' is checked first to avoid an unnecessary filesystem read on the recursive path, then BRV_ENV === 'development', then the disk read. Good order. The test coverage in update-notifier.test.ts:212-258 covers each condition independently, and the new shouldRunUpdateCheck boundary keeps handleUpdateNotification itself pure (no env / disk side effects). Nice extraction.

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.

2 participants