mirror of
https://github.com/Mesh-LLM/mesh-llm.git
synced 2026-08-08 22:23:19 -04:00
ui(data-mode): migrate legacy harness storage to v2 (#1040)
* centralize E2E data mode storage key * use E2E alias for shared data mode key
This commit is contained in:
parent
cf2d6addad
commit
a1649daaa0
8 changed files with 101 additions and 12 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import AxeBuilder from '@axe-core/playwright'
|
||||
import { expect, test, type Page, type TestInfo } from '@playwright/test'
|
||||
import { DATA_MODE_STORAGE_KEY } from '@e2e/support/data-mode'
|
||||
|
||||
const DATA_MODE_STORAGE_KEY = 'mesh-llm-ui-preview:data-mode:v1'
|
||||
const FEATURE_FLAGS_STORAGE_KEY = 'mesh-llm-ui-preview:feature-flags:v1'
|
||||
|
||||
type JsonRecord = Record<string, unknown>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { mkdir, writeFile } from 'node:fs/promises'
|
|||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { expect, test, type APIRequestContext, type Page } from '@playwright/test'
|
||||
import { DATA_MODE_STORAGE_KEY } from '@e2e/support/data-mode'
|
||||
|
||||
const pluginName = 'web-ui-exemplar'
|
||||
const pluginApi = `/api/plugins/${pluginName}`
|
||||
|
|
@ -94,10 +95,13 @@ test.describe('installed plugin web UI exemplar @plugin', () => {
|
|||
const browserApiResponses = collectBrowserApiResponses(page)
|
||||
await mkdir(evidenceDirectory, { recursive: true })
|
||||
|
||||
await page.addInitScript((preferences) => {
|
||||
window.localStorage.setItem('mesh-llm-ui-preview:preferences:v1', JSON.stringify(preferences))
|
||||
window.localStorage.setItem('mesh-llm-ui-preview:data-mode:v1', 'live')
|
||||
}, darkUiPreferences)
|
||||
await page.addInitScript(
|
||||
({ dataModeStorageKey, preferences }) => {
|
||||
window.localStorage.setItem('mesh-llm-ui-preview:preferences:v1', JSON.stringify(preferences))
|
||||
window.localStorage.setItem(dataModeStorageKey, 'live')
|
||||
},
|
||||
{ dataModeStorageKey: DATA_MODE_STORAGE_KEY, preferences: darkUiPreferences }
|
||||
)
|
||||
|
||||
try {
|
||||
const consoleResponse = await request.get('/')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { devices, expect, test, type Page } from '@playwright/test'
|
||||
import { DATA_MODE_STORAGE_KEY } from '@e2e/support/data-mode'
|
||||
|
||||
const DATA_MODE_STORAGE_KEY = 'mesh-llm-ui-preview:data-mode:v1'
|
||||
const API_ORIGIN = 'http://127.0.0.1:3131'
|
||||
const IPHONE_KEYBOARD_VIEWPORT = { width: 390, height: 520 }
|
||||
const IPHONE_14 = devices['iPhone 14']
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, test, type Page, type TestInfo } from '@playwright/test'
|
||||
import { DATA_MODE_STORAGE_KEY } from '@e2e/support/data-mode'
|
||||
|
||||
const DATA_MODE_STORAGE_KEY = 'mesh-llm-ui-preview:data-mode:v1'
|
||||
const API_ORIGIN = 'http://127.0.0.1:3131'
|
||||
const CLIP_FIXTURE_PATH = decodeURIComponent(new URL('../fixtures/clip.mp3', import.meta.url).pathname)
|
||||
|
||||
|
|
|
|||
1
crates/mesh-llm-ui/e2e/support/data-mode.ts
Normal file
1
crates/mesh-llm-ui/e2e/support/data-mode.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const DATA_MODE_STORAGE_KEY = 'mesh-llm-ui-preview:data-mode:v2'
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { DATA_MODE_STORAGE_KEY, DataModeProvider } from '@/lib/data-mode/DataModeContext'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DATA_MODE_STORAGE_KEY, DataModeProvider, LEGACY_DATA_MODE_STORAGE_KEY } from '@/lib/data-mode/DataModeContext'
|
||||
import { useDataMode } from '@/lib/data-mode/useDataMode'
|
||||
import { env } from '@/lib/env'
|
||||
|
||||
|
|
@ -20,6 +20,7 @@ describe('DataModeProvider', () => {
|
|||
|
||||
afterEach(() => {
|
||||
env.isDevelopment = originalIsDevelopment
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('defaults to live mode in production builds and persists under the preview namespace', async () => {
|
||||
|
|
@ -70,6 +71,54 @@ describe('DataModeProvider', () => {
|
|||
expect(result.current.mode).toBe('live')
|
||||
})
|
||||
|
||||
it('migrates a legacy harness default to the production default without deleting v1', () => {
|
||||
env.isDevelopment = false
|
||||
window.localStorage.setItem(LEGACY_DATA_MODE_STORAGE_KEY, 'harness')
|
||||
|
||||
const { result } = renderHook(() => useDataMode(), { wrapper: providerWrapper() })
|
||||
|
||||
expect(result.current.mode).toBe('live')
|
||||
expect(window.localStorage.getItem(DATA_MODE_STORAGE_KEY)).toBe('live')
|
||||
expect(window.localStorage.getItem(LEGACY_DATA_MODE_STORAGE_KEY)).toBe('harness')
|
||||
})
|
||||
|
||||
it('does not repeat migration when a valid v2 choice already exists', () => {
|
||||
window.localStorage.setItem(LEGACY_DATA_MODE_STORAGE_KEY, 'live')
|
||||
window.localStorage.setItem(DATA_MODE_STORAGE_KEY, 'harness')
|
||||
const setItem = vi.spyOn(Storage.prototype, 'setItem')
|
||||
|
||||
const { result } = renderHook(() => useDataMode(), { wrapper: providerWrapper({ initialMode: 'live' }) })
|
||||
|
||||
expect(result.current.mode).toBe('harness')
|
||||
expect(setItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('repairs a malformed v2 value without restoring stale v1 state', async () => {
|
||||
window.localStorage.setItem(LEGACY_DATA_MODE_STORAGE_KEY, 'harness')
|
||||
window.localStorage.setItem(DATA_MODE_STORAGE_KEY, 'not-a-data-mode')
|
||||
|
||||
const { result } = renderHook(() => useDataMode(), { wrapper: providerWrapper({ initialMode: 'live' }) })
|
||||
|
||||
expect(result.current.mode).toBe('live')
|
||||
await waitFor(() => {
|
||||
expect(window.localStorage.getItem(DATA_MODE_STORAGE_KEY)).toBe('live')
|
||||
})
|
||||
expect(window.localStorage.getItem(LEGACY_DATA_MODE_STORAGE_KEY)).toBe('harness')
|
||||
})
|
||||
|
||||
it('keeps legacy state intact when the v2 migration write fails', () => {
|
||||
window.localStorage.setItem(LEGACY_DATA_MODE_STORAGE_KEY, 'harness')
|
||||
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
||||
throw new DOMException('Storage unavailable', 'QuotaExceededError')
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useDataMode(), { wrapper: providerWrapper({ initialMode: 'live' }) })
|
||||
|
||||
expect(result.current.mode).toBe('live')
|
||||
expect(window.localStorage.getItem(DATA_MODE_STORAGE_KEY)).toBeNull()
|
||||
expect(window.localStorage.getItem(LEGACY_DATA_MODE_STORAGE_KEY)).toBe('harness')
|
||||
})
|
||||
|
||||
it('persists data mode updates', async () => {
|
||||
const { result } = renderHook(() => useDataMode(), { wrapper: providerWrapper() })
|
||||
|
||||
|
|
@ -93,4 +142,20 @@ describe('DataModeProvider', () => {
|
|||
expect(result.current.mode).toBe('live')
|
||||
expect(window.localStorage.getItem(storageKey)).toBeNull()
|
||||
})
|
||||
|
||||
it('does not run the app upgrade migration for a host-owned storage key', async () => {
|
||||
const storageKey = 'host-owned:data-mode'
|
||||
window.localStorage.setItem(LEGACY_DATA_MODE_STORAGE_KEY, 'harness')
|
||||
|
||||
const { result } = renderHook(() => useDataMode(), {
|
||||
wrapper: providerWrapper({ initialMode: 'live', storageKey })
|
||||
})
|
||||
|
||||
expect(result.current.mode).toBe('live')
|
||||
await waitFor(() => {
|
||||
expect(window.localStorage.getItem(storageKey)).toBe('live')
|
||||
})
|
||||
expect(window.localStorage.getItem(DATA_MODE_STORAGE_KEY)).toBeNull()
|
||||
expect(window.localStorage.getItem(LEGACY_DATA_MODE_STORAGE_KEY)).toBe('harness')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { useEffect, useState, type ReactNode, type SetStateAction } from 'react'
|
|||
import { env } from '@/lib/env'
|
||||
import { DataModeContext, type DataMode } from '@/lib/data-mode/data-mode-context'
|
||||
|
||||
export const DATA_MODE_STORAGE_KEY = `${env.storageNamespace}:data-mode:v1`
|
||||
export const DATA_MODE_STORAGE_KEY = `${env.storageNamespace}:data-mode:v2`
|
||||
export const LEGACY_DATA_MODE_STORAGE_KEY = `${env.storageNamespace}:data-mode:v1`
|
||||
|
||||
export type DataModeProviderProps = {
|
||||
children: ReactNode
|
||||
|
|
@ -20,7 +21,21 @@ function readStoredDataMode(storageKey: string, fallbackMode: DataMode, persist:
|
|||
|
||||
try {
|
||||
const storedValue = window.localStorage.getItem(storageKey)
|
||||
return isDataMode(storedValue) ? storedValue : fallbackMode
|
||||
if (isDataMode(storedValue)) return storedValue
|
||||
|
||||
// v1 may contain the old production default (`harness`), so its value
|
||||
// cannot safely be interpreted as an explicit choice. A valid legacy key
|
||||
// only marks this as an upgrade: seed v2 from today's environment default
|
||||
// and retain v1 so a downgraded console can still start normally.
|
||||
if (
|
||||
storedValue === null &&
|
||||
storageKey === DATA_MODE_STORAGE_KEY &&
|
||||
isDataMode(window.localStorage.getItem(LEGACY_DATA_MODE_STORAGE_KEY))
|
||||
) {
|
||||
writeStoredDataMode(storageKey, fallbackMode, persist)
|
||||
}
|
||||
|
||||
return fallbackMode
|
||||
} catch {
|
||||
return fallbackMode
|
||||
}
|
||||
|
|
@ -30,6 +45,7 @@ function writeStoredDataMode(storageKey: string, mode: DataMode, persist: boolea
|
|||
if (!persist || typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
if (window.localStorage.getItem(storageKey) === mode) return
|
||||
window.localStorage.setItem(storageKey, mode)
|
||||
} catch {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["./src/*"] }
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@e2e/*": ["./e2e/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue