-
Notifications
You must be signed in to change notification settings - Fork 15
test(win): Pro-on-Windows integration + capture/settings fixes #77
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
base: main
Are you sure you want to change the base?
Changes from all commits
f9eb43d
fe12d1d
2c78ba2
cf985f5
b9d20d2
a4431cf
6d63246
1475b13
7c528a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| // Regression: LLMService must read its persisted state LAZILY, not in the constructor. | ||
| // | ||
| // `llm` is a module-level singleton (`export const llm = new LLMService()`), so it is | ||
| // constructed while index.ts's IMPORTS are still evaluating — which under ESM finishes | ||
| // BEFORE index.ts's own body runs `unifyUserDataPath()` → `app.setPath('userData', …)`. | ||
| // Any path resolved during construction therefore points at the PRE-override profile. | ||
| // | ||
| // Two real consequences, both of which these tests pin: | ||
| // 1. Production: the canonical-dir migration ("My Memories" / "my-memories" → | ||
| // "Off Grid AI Desktop") has not run yet at construction, so the user's saved | ||
| // settings and active model were silently missed and replaced by defaults. | ||
| // 2. E2E/harness: an OFFGRID_USER_DATA temp profile was ignored entirely — which is | ||
| // what made `settings-sections.spec.ts` "resource mode survives a relaunch" fail. | ||
| // A probe confirmed the constructor resolving the REAL profile while | ||
| // OFFGRID_USER_DATA pointed at the temp dir. | ||
| // | ||
| // Writes never had the bug: `persist()` goes through the `settingsFile` getter, which | ||
| // resolves late. These tests assert the READ side now behaves the same way, by doing | ||
| // what production does — construct FIRST, point the data dir somewhere SECOND. | ||
| import { describe, it, expect, beforeEach, afterEach } from 'vitest' | ||
| import fs from 'fs' | ||
| import os from 'os' | ||
| import path from 'path' | ||
| import { LLMService } from '../llm' | ||
| import { configureRuntime } from '../runtime-env' | ||
|
|
||
| let tmp: string | ||
|
|
||
| /** Write an llm-settings.json into the models dir of a data dir, as `persist()` would. */ | ||
| const seedSettings = (dataDir: string, settings: Record<string, unknown>): void => { | ||
| const modelsDir = path.join(dataDir, 'models') | ||
| fs.mkdirSync(modelsDir, { recursive: true }) | ||
| fs.writeFileSync(path.join(modelsDir, 'llm-settings.json'), JSON.stringify(settings)) | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-llm-lazy-')) | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| // Release the override so a later test isn't pinned to a deleted temp dir. | ||
| configureRuntime({ dataDir: undefined }) | ||
| fs.rmSync(tmp, { recursive: true, force: true }) | ||
| }) | ||
|
|
||
| describe('LLMService reads persisted settings lazily (not at construction)', () => { | ||
| it('picks up a data dir configured AFTER the instance was constructed', () => { | ||
| // Construct FIRST — mirrors the module-level singleton being built during imports. | ||
| const svc = new LLMService() | ||
| // ...then point the runtime at the profile, as index.ts's body does later. | ||
| seedSettings(tmp, { performanceMode: 'extreme', temperature: 0.42 }) | ||
| configureRuntime({ dataDir: tmp }) | ||
|
|
||
| const s = svc.getSettings() | ||
| expect(s.performanceMode).toBe('extreme') | ||
| expect(s.temperature).toBe(0.42) | ||
| }) | ||
|
|
||
| it('survives the relaunch shape: persisted mode is read back by a fresh instance', () => { | ||
| // What settings-sections.spec.ts "resource mode survives a relaunch" exercises: | ||
| // one process writes the mode, the next process constructs and must read it back. | ||
| seedSettings(tmp, { performanceMode: 'conservative' }) | ||
| const relaunched = new LLMService() | ||
| configureRuntime({ dataDir: tmp }) | ||
|
|
||
| expect(relaunched.getSettings().performanceMode).toBe('conservative') | ||
| }) | ||
|
|
||
| it('loads once and does not re-read after the first access', () => { | ||
| seedSettings(tmp, { performanceMode: 'conservative' }) | ||
| const svc = new LLMService() | ||
| configureRuntime({ dataDir: tmp }) | ||
| expect(svc.getSettings().performanceMode).toBe('conservative') | ||
|
|
||
| // A later on-disk edit must NOT leak in: the load is once-only, so in-memory state | ||
| // stays authoritative until something explicitly persists. This guards against | ||
| // turning the lazy guard into a read-on-every-call, which would re-read the file | ||
| // on every getSettings and clobber unsaved in-memory changes. | ||
| seedSettings(tmp, { performanceMode: 'extreme' }) | ||
| expect(svc.getSettings().performanceMode).toBe('conservative') | ||
| }) | ||
|
|
||
| it('falls back to defaults when the profile has no settings file', () => { | ||
| const svc = new LLMService() | ||
| configureRuntime({ dataDir: tmp }) // seeded with nothing | ||
| expect(svc.getSettings().performanceMode).toBe('balanced') | ||
| }) | ||
|
|
||
| // The direct guard on the defect, stated behaviourally rather than by spying on fs: | ||
| // if construction reads eagerly, it reads the profile configured AT THAT MOMENT. | ||
| // Point the runtime at profile A, construct, then switch to profile B before first | ||
| // use — a lazy reader returns B, an eager one returns A. This is the exact shape of | ||
| // the production bug (construct during imports, real profile chosen afterwards). | ||
| it('reads the profile configured at FIRST USE, not the one present at construction', () => { | ||
| const other = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-llm-lazy-other-')) | ||
| try { | ||
| seedSettings(other, { performanceMode: 'extreme' }) // profile A | ||
| seedSettings(tmp, { performanceMode: 'conservative' }) // profile B | ||
|
|
||
| configureRuntime({ dataDir: other }) // A is current... | ||
| const svc = new LLMService() // ...at construction | ||
| configureRuntime({ dataDir: tmp }) // the override lands afterwards | ||
|
|
||
| // Eager construction would have pinned 'extreme' from profile A. | ||
| expect(svc.getSettings().performanceMode).toBe('conservative') | ||
| } finally { | ||
| fs.rmSync(other, { recursive: true, force: true }) | ||
| } | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,8 +17,8 @@ import type { SearchHit } from './types' | |
| import { loadProFeaturesRenderer } from './bootstrap/loadProFeaturesRenderer' | ||
| import { renderProView, type ProViewContext } from './bootstrap/proView' | ||
| import { UpgradeScreen } from './components/pro/UpgradeScreen' | ||
| import { getProFeature, proFeatureComingSoon } from './components/pro/proCatalog' | ||
| import { currentPlatform, isMac } from './lib/device' | ||
| import { getProFeature, proFeatureComingSoon, landingView } from './components/pro/proCatalog' | ||
| import { currentPlatform } from './lib/device' | ||
| import { NotificationProvider } from './hooks/NotificationProvider' | ||
| import { useNotifications } from './hooks/useNotifications' | ||
| import { ToastProvider } from './hooks/ToastProvider' | ||
|
|
@@ -226,9 +226,10 @@ function AppContent() { | |
| } | ||
| }, []) | ||
|
|
||
| // Free users land on Models (download a model first, with the sidebar to | ||
| // explore); Mac Pro users land on Day. Never land on a locked or unavailable tab. | ||
| const [viewMode, setViewMode] = useState<ViewMode>(isPro && isMac() ? 'day' : 'models') | ||
| // Where to open: derived from the per-feature capability seam (see landingView), so | ||
| // the landing screen can never disagree with nav and gating about whether Day is | ||
| // available on this platform. | ||
| const [viewMode, setViewMode] = useState<ViewMode>(landingView(currentPlatform(), isPro)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+229
to
+232
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Keep root-path routing consistent with
As per coding guidelines, "Define mappings, routing rules, and capability checks once and reuse the single source of truth" and "Add user-behavior integration tests through real product boundaries." 📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null) | ||
| const [selectedMemoryId, setSelectedMemoryId] = useState<number | null>(null) | ||
| // Version of a downloaded-and-staged update (null = none). Surfaced as a banner | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a lazy active-model regression case.
These tests cover
llm-settings.jsononly.ensureLoaded()also defersactive-model.jsonresolution.Seed a model file and
active-model.json, constructLLMServicebeforeconfigureRuntime(), then assert thatactiveModelInfo()reads the model from the profile configured at first use.As per coding guidelines, "Add regression tests in the same change for every behavior change, including bug cases, branches, conditions, error paths, and copy or contract changes."
Proposed regression test
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines