-
Notifications
You must be signed in to change notification settings - Fork 10.3k
feat(updater): add update install mode preference (#4467) #4543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
cbeaulieu-gt
wants to merge
9
commits into
nexu-io:main
from
cbeaulieu-gt:feat/4467-update-install-mode
Closed
Changes from 2 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
92509be
feat(updater): add update install mode preference (#4467)
cbeaulieu-gt cf70200
fix(updater): address review — contract sync + payload-capable toggle…
cbeaulieu-gt 234b70a
fix(cli): clear config keys on `od config unset` via null merge-patch…
cbeaulieu-gt 74daba2
fix(updater): clear updateInstallMode on daemon omission (#4543)
cbeaulieu-gt 5d0530b
fix(updater): honor manual mode after payload download
TheAngryPit c2c9f65
Merge remote-tracking branch 'origin/main' into feat/4467-update-inst…
cbeaulieu-gt 217086d
fix(updater): reconcile install mode symmetrically before install
cbeaulieu-gt 9c52816
Merge commit '3f670090d78cefe37c64ec7f6dc34dbc382e37bc' into pr-4543
cbeaulieu-gt 29a1b3c
fix(merge): correct two merge-conflict-resolution defects
cbeaulieu-gt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // Tests for updateInstallMode pref — part of #4467 (PR1). | ||
| // | ||
| // Spec: apps/daemon/src/app-config.ts gains: | ||
| // - `updateInstallMode?: 'automatic' | 'manual'` on AppConfigPrefs | ||
| // - key added to ALLOWED_KEYS | ||
| // - applyConfigValue validates the enum (accept 'automatic'/'manual'; reject anything else) | ||
| // - absent/unset treated as 'automatic' (no migration needed) | ||
| // | ||
| // These tests are RED until the implementation lands. | ||
|
|
||
| import { mkdtemp, rm } from 'node:fs/promises'; | ||
| import { tmpdir } from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { afterEach, beforeEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| import { readAppConfig, writeAppConfig } from '../src/app-config.js'; | ||
|
|
||
| describe('app-config updateInstallMode pref', () => { | ||
| let dataDir: string; | ||
|
|
||
| beforeEach(async () => { | ||
| dataDir = await mkdtemp(path.join(tmpdir(), 'od-update-install-mode-')); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await rm(dataDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| it("persists 'manual' and reads it back", async () => { | ||
| await writeAppConfig(dataDir, { updateInstallMode: 'manual' }); | ||
| const cfg = await readAppConfig(dataDir); | ||
| expect(cfg.updateInstallMode).toBe('manual'); | ||
| }); | ||
|
|
||
| it("persists 'automatic' and reads it back", async () => { | ||
| await writeAppConfig(dataDir, { updateInstallMode: 'automatic' }); | ||
| const cfg = await readAppConfig(dataDir); | ||
| expect(cfg.updateInstallMode).toBe('automatic'); | ||
| }); | ||
|
|
||
| it('rejects invalid enum values and drops them', async () => { | ||
| // 'bogus' is not in the allowed enum; writeAppConfig must drop it. | ||
| await writeAppConfig(dataDir, { updateInstallMode: 'bogus' as any }); | ||
| const cfg = await readAppConfig(dataDir); | ||
| expect(cfg.updateInstallMode).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('treats absent field as automatic (no stored value, no crash)', async () => { | ||
| // Fresh config with no updateInstallMode — should be absent (undefined), | ||
| // which callers treat as 'automatic'. | ||
| const cfg = await readAppConfig(dataDir); | ||
| expect(cfg.updateInstallMode).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("updateInstallMode is included in ALLOWED_KEYS (round-trip without unknown-key filter dropping it)", async () => { | ||
| // ALLOWED_KEYS gate: only keys in the set survive writeAppConfig. | ||
| // If updateInstallMode is missing from ALLOWED_KEYS it is silently | ||
| // dropped — this test catches that regression. | ||
| await writeAppConfig(dataDir, { updateInstallMode: 'manual', agentId: 'claude' }); | ||
| const cfg = await readAppConfig(dataDir); | ||
| expect(cfg.updateInstallMode).toBe('manual'); | ||
| expect(cfg.agentId).toBe('claude'); | ||
| }); | ||
|
|
||
| it("clears updateInstallMode when null is sent", async () => { | ||
| await writeAppConfig(dataDir, { updateInstallMode: 'manual' }); | ||
| await writeAppConfig(dataDir, { updateInstallMode: null as any }); | ||
| const cfg = await readAppConfig(dataDir); | ||
| expect(cfg.updateInstallMode).toBeUndefined(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| // Tests for the exported readUpdateInstallMode helper in apps/desktop/src/main/index.ts — #4467 (PR2). | ||
| // | ||
| // Spec: | ||
| // readUpdateInstallMode(baseUrl, fetchImpl?) fetches GET /api/app-config from the daemon | ||
| // and returns the updateInstallMode pref, or undefined on any failure. | ||
| // | ||
| // Signature: (baseUrl: string, fetchImpl?: typeof globalThis.fetch) => Promise<'automatic' | 'manual' | undefined> | ||
| // | ||
| // These tests are RED until the implementation lands. | ||
|
|
||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { readUpdateInstallMode } from "../../src/main/index.js"; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Helpers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| type FetchImpl = typeof globalThis.fetch; | ||
|
|
||
| /** Builds a fetch stub that resolves to a JSON response body. */ | ||
| function fakeFetchOk(body: unknown): FetchImpl { | ||
| return async (_input: RequestInfo | URL, _init?: RequestInit) => { | ||
| const text = JSON.stringify(body); | ||
| return new Response(text, { | ||
| status: 200, | ||
| headers: { "content-type": "application/json" }, | ||
| }); | ||
| }; | ||
| } | ||
|
|
||
| /** Builds a fetch stub that resolves to a non-OK HTTP status. */ | ||
| function fakeFetchStatus(status: number): FetchImpl { | ||
| return async () => | ||
| new Response("error", { status }); | ||
| } | ||
|
|
||
| /** Builds a fetch stub that rejects with an error. */ | ||
| function fakeFetchReject(message: string): FetchImpl { | ||
| return async () => { | ||
| throw new Error(message); | ||
| }; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Tests | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| describe("readUpdateInstallMode", () => { | ||
| it("returns 'manual' when the daemon config contains updateInstallMode='manual'", async () => { | ||
| const fetch = fakeFetchOk({ config: { updateInstallMode: "manual" } }); | ||
| const result = await readUpdateInstallMode("http://127.0.0.1:9999", fetch); | ||
| expect(result).toBe("manual"); | ||
| }); | ||
|
|
||
| it("returns 'automatic' when the daemon config contains updateInstallMode='automatic'", async () => { | ||
| const fetch = fakeFetchOk({ config: { updateInstallMode: "automatic" } }); | ||
| const result = await readUpdateInstallMode("http://127.0.0.1:9999", fetch); | ||
| expect(result).toBe("automatic"); | ||
| }); | ||
|
|
||
| it("returns undefined when fetch throws (network error / daemon unreachable)", async () => { | ||
| const fetch = fakeFetchReject("ECONNREFUSED"); | ||
| const result = await readUpdateInstallMode("http://127.0.0.1:9999", fetch); | ||
| expect(result).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("returns undefined when the daemon config does not contain the field", async () => { | ||
| const fetch = fakeFetchOk({ config: { agentId: "claude" } }); | ||
| const result = await readUpdateInstallMode("http://127.0.0.1:9999", fetch); | ||
| expect(result).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("returns undefined when the daemon returns a non-OK HTTP status", async () => { | ||
| const fetch = fakeFetchStatus(503); | ||
| const result = await readUpdateInstallMode("http://127.0.0.1:9999", fetch); | ||
| expect(result).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("calls GET /api/app-config relative to the given baseUrl", async () => { | ||
| const calls: string[] = []; | ||
| const fetch: FetchImpl = async (input) => { | ||
| calls.push(String(input)); | ||
| return new Response(JSON.stringify({ config: { updateInstallMode: "manual" } }), { | ||
| status: 200, | ||
| headers: { "content-type": "application/json" }, | ||
| }); | ||
| }; | ||
| await readUpdateInstallMode("http://127.0.0.1:8080", fetch); | ||
| expect(calls).toEqual(["http://127.0.0.1:8080/api/app-config"]); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.