From 71f686aefaa8c6c73901f1e455f2c16eebb65d19 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 8 Oct 2026 13:09:51 +0000 Subject: [PATCH] Harden SQLite, model families, and audit overrides Map unreadable Codex databases to the existing UNSUPPORTED health state, fall back from state_5.sqlite to the newest state_*.sqlite, and derive Model dial families from models_cache.json. Patch the brace-expansion override and fast-uri so audits pass, and keep test:fast free of native helper spawns. Co-authored-by: Todd Dailey --- CHANGELOG.md | 21 ++++ MARKETPLACE.md | 2 +- package-lock.json | 12 +- package.json | 3 +- src/lib/codex-store.ts | 173 ++++++++++++++++++++++++++--- src/lib/health.ts | 2 + src/lib/model.ts | 49 +++++++- test/codex-store-database.test.ts | 162 +++++++++++++++++++++++++++ test/health-diagnostics.test.ts | 8 ++ test/model-families.test.ts | 93 ++++++++++++++++ test/profile-keys.behavior.test.ts | 5 + vitest.config.ts | 1 + 12 files changed, 504 insertions(+), 27 deletions(-) create mode 100644 test/codex-store-database.test.ts create mode 100644 test/model-families.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 20273d6..d0f2cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ All notable changes to this project are documented here. +## Unreleased + +### Changed + +- Depend on `@elgato/streamdeck` 3.0.1. +- Derive Model dial families from `models_cache.json` with the existing + slug and reasoning allow-list, falling back to Luna, Terra, Sol, and Astra + when the cache is missing. +- Prefer `state_5.sqlite` and fall back to the highest-numbered + `state_*.sqlite` when that default file is absent. +- Keep `test:fast` free of native helper spawns, including the Astra picker + fixtures. + +### Fixed + +- Pin `brace-expansion` to a patched override and update `fast-uri` so + `npm run audit:full` and production High audits pass. +- Map a missing, locked, or incompatible Codex SQLite database to the + existing `UNSUPPORTED` availability state instead of throwing and blanking + keys. Health and `npm run doctor` report the store when it is unreadable. + ## 0.2.4 — 2026-09-05 ### Fixed diff --git a/MARKETPLACE.md b/MARKETPLACE.md index 2da8e6e..918acbd 100644 --- a/MARKETPLACE.md +++ b/MARKETPLACE.md @@ -8,7 +8,7 @@ It is release evidence, not a substitute for Maker Console review. - [x] Reverse-DNS plugin UUID includes author and product identity. - [x] Every action UUID is prefixed by the plugin UUID. - [x] SDK version 3 and Stream Deck 7.1 minimum. -- [x] Node.js SDK `@elgato/streamdeck` 2.x and Node.js 24 runtime. +- [x] Node.js SDK `@elgato/streamdeck` 3.0.1 and Node.js 24 runtime. - [x] macOS 13 minimum is declared. - [x] Plugin and category names are aligned. - [x] Eleven actions—within Elgato's recommended 2–30 range. diff --git a/package-lock.json b/package-lock.json index 53868c0..0bebc04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1719,9 +1719,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1912,9 +1912,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index fbc7605..4e1b40d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "openai" ], "overrides": { - "brace-expansion": "5.0.9", + "brace-expansion": ">=5.0.12", + "fast-uri": "3.1.8", "nanoid": "3.3.18" }, "engines": { diff --git a/src/lib/codex-store.ts b/src/lib/codex-store.ts index d4a373f..fdc295d 100644 --- a/src/lib/codex-store.ts +++ b/src/lib/codex-store.ts @@ -1,14 +1,16 @@ import { closeSync, + existsSync, fstatSync, openSync, + readdirSync, readFileSync, readSync, statSync, } from "node:fs"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; -import { DatabaseSync } from "node:sqlite"; +import { DatabaseSync, type SQLInputValue } from "node:sqlite"; import type { AgentSnapshot, ContextSnapshot, @@ -79,26 +81,48 @@ export function resolveCodexHome(): string { return resolve(expandHome(configured || join(homedir(), ".codex"))); } +const STATE_SQLITE_NAME = /^state_(\d+)\.sqlite$/; +const DEFAULT_STATE_SQLITE = "state_5.sqlite"; + +function resolveSqliteInDirectory(directory: string): string { + const preferred = join(directory, DEFAULT_STATE_SQLITE); + if (existsSync(preferred)) return preferred; + try { + let highest: { name: string; generation: number } | undefined; + for (const name of readdirSync(directory)) { + const match = STATE_SQLITE_NAME.exec(name); + if (!match) continue; + const generation = Number(match[1]); + if (!highest || generation > highest.generation) { + highest = { name, generation }; + } + } + if (highest) return join(directory, highest.name); + } catch { + // An unreadable directory still resolves to the default filename. + } + return preferred; +} + export function resolveStateDatabase(codexHome = resolveCodexHome()): string { const explicit = process.env["CODEX_SQLITE_HOME"]; if (explicit) { const expanded = resolve(expandHome(explicit)); return expanded.endsWith(".sqlite") ? expanded - : join(expanded, "state_5.sqlite"); + : resolveSqliteInDirectory(expanded); } try { const config = readFileSync(join(codexHome, "config.toml"), "utf8"); const match = config.match(/^\s*sqlite_home\s*=\s*["']([^"']+)["']/m); if (match?.[1]) { - const configuredHome = resolve(expandHome(match[1])); - return join(configuredHome, "state_5.sqlite"); + return resolveSqliteInDirectory(resolve(expandHome(match[1]))); } } catch { // The default location remains authoritative when no readable config exists. } - return join(codexHome, "state_5.sqlite"); + return resolveSqliteInDirectory(codexHome); } export function readFileTail(path: string, maxBytes = 512 * 1024): string { @@ -200,11 +224,83 @@ function sameFile( ); } +interface StoreStatement { + all: (...params: SQLInputValue[]) => unknown; + get: (...params: SQLInputValue[]) => unknown; +} + +interface StoreDatabase { + prepare(sql: string): StoreStatement; + exec(sql: string): void; + close(): void; +} + +const UNAVAILABLE_STORE_DATABASE: StoreDatabase = { + prepare() { + return { all: () => [], get: () => undefined }; + }, + exec() {}, + close() {}, +}; + +const DATABASE_SCHEMA_PROBE = `SELECT + t.id, t.rollout_path, t.cwd, t.title, t.preview, t.recency_at_ms, + t.reasoning_effort, t.model, t.archived, e.status AS spawn_status + FROM threads t + LEFT JOIN thread_spawn_edges e ON e.child_thread_id = t.id + LIMIT 0`; + +function guardedStoreDatabase( + database: DatabaseSync, + onFailure: (error: unknown) => void, +): StoreDatabase { + const statement = (sql: string): StoreStatement => { + try { + const prepared = database.prepare(sql); + return { + all: (...params: SQLInputValue[]) => { + try { + return prepared.all(...params); + } catch (error) { + onFailure(error); + return []; + } + }, + get: (...params: SQLInputValue[]) => { + try { + return prepared.get(...params); + } catch (error) { + onFailure(error); + return undefined; + } + }, + }; + } catch (error) { + onFailure(error); + return { all: () => [], get: () => undefined }; + } + }; + return { + prepare: statement, + exec(sql: string) { + try { + database.exec(sql); + } catch (error) { + onFailure(error); + } + }, + close() { + database.close(); + }, + }; +} + export class CodexStore { readonly codexHome: string; readonly databasePath: string; readonly #acknowledged = new Map(); - #database: DatabaseSync | undefined; + #database: StoreDatabase | undefined; + #databaseUnavailableReason: AvailabilityReason | undefined; #cache: { at: number; snapshots: AgentSnapshot[] } | undefined; #usageCache: | { at: number; snapshot?: UsageSnapshot; reason?: AvailabilityReason } @@ -285,6 +381,18 @@ export class CodexStore { close(): void { this.#database?.close(); this.#database = undefined; + this.#databaseUnavailableReason = undefined; + } + + /** + * Cheap local SQLite readability. Failures are `unsupported-schema` so + * Health and doctor can render the existing UNSUPPORTED label. + */ + storeAvailability(): Availability { + this.#probeDatabase(); + return this.#databaseUnavailableReason + ? unavailable(this.#databaseUnavailableReason) + : ready("readable"); } acknowledge(threadId: string, at = Date.now()): void { @@ -824,15 +932,52 @@ export class CodexStore { ); } - #open(): DatabaseSync { + #markDatabaseUnavailable(): void { + this.#databaseUnavailableReason = "unsupported-schema"; + const database = this.#database; + this.#database = undefined; + try { + database?.close(); + } catch { + // Closing a failed handle must not hide the original unavailability. + } + } + + #probeDatabase(): void { + this.#open().prepare(DATABASE_SCHEMA_PROBE).all(); + } + + #open(): StoreDatabase { if (this.#database) return this.#database; - const database = new DatabaseSync(this.databasePath, { - readOnly: true, - timeout: 1000, - }); - database.exec("PRAGMA query_only = ON"); - this.#database = database; - return database; + if (!existsSync(this.databasePath)) { + this.#markDatabaseUnavailable(); + return UNAVAILABLE_STORE_DATABASE; + } + try { + const database = new DatabaseSync(this.databasePath, { + readOnly: true, + timeout: 1000, + }); + try { + database.exec("PRAGMA query_only = ON"); + const guarded = guardedStoreDatabase(database, () => { + this.#markDatabaseUnavailable(); + }); + this.#database = guarded; + this.#databaseUnavailableReason = undefined; + return guarded; + } catch (error) { + try { + database.close(); + } catch { + // Prefer the original open/exec failure. + } + throw error; + } + } catch { + this.#markDatabaseUnavailable(); + return UNAVAILABLE_STORE_DATABASE; + } } } diff --git a/src/lib/health.ts b/src/lib/health.ts index 0b5e91c..a2a937b 100644 --- a/src/lib/health.ts +++ b/src/lib/health.ts @@ -11,6 +11,7 @@ export const HEALTH_COMPONENTS = [ "reasoning", "usage", "input", + "store", ] as const; export type HealthComponent = (typeof HEALTH_COMPONENTS)[number]; @@ -68,6 +69,7 @@ export function collectHealth(store: CodexStore): HealthSnapshot { (snapshot) => `${Math.round(100 - snapshot.usedPercent)}% left`, ), input: inputHealth, + store: summarize(store.storeAvailability(), (value) => value), }, }; } diff --git a/src/lib/model.ts b/src/lib/model.ts index a999f99..84c1e59 100644 --- a/src/lib/model.ts +++ b/src/lib/model.ts @@ -2,8 +2,13 @@ import type { ModelOption } from "../types.js"; import { normalizeReasoningLevels } from "./reasoning.js"; import type { DialFeedback } from "./visuals.js"; -const MODEL_FAMILIES = ["luna", "terra", "sol", "astra"] as const; -const MODEL_SLUG = /^gpt-[a-z0-9.-]+-(luna|terra|sol|astra)$/i; +export const FALLBACK_MODEL_FAMILIES = [ + "luna", + "terra", + "sol", + "astra", +] as const; +const MODEL_SLUG = /^gpt-[a-z0-9.-]+-([a-z][a-z0-9]{0,31})$/i; const SAFE_DISPLAY_NAME = /^[a-z0-9 ._-]{1,64}$/i; const REASONING_LEVELS = new Set([ "none", @@ -28,13 +33,47 @@ interface CachedModel { supported_reasoning_levels?: Array<{ effort?: string }>; } -export function supportedModelOptions(parsed: unknown): ModelOption[] { +function cachedModels(parsed: unknown): CachedModel[] { if (typeof parsed !== "object" || parsed === null) return []; - const models = Array.isArray((parsed as { models?: unknown }).models) + return Array.isArray((parsed as { models?: unknown }).models) ? ((parsed as { models: CachedModel[] }).models ?? []) : []; +} + +function familyFromSlug(slug: string): string | undefined { + if (slug.length > 64) return undefined; + const match = MODEL_SLUG.exec(slug); + return match?.[1]?.toLowerCase(); +} + +/** + * Families advertised by the live catalog, in the Luna → Terra → Sol → Astra + * order when those are present, then any additional matching suffixes. A + * missing or empty cache keeps the current four-family fallback. + */ +export function modelFamiliesFromCache(parsed: unknown): string[] { + const seen = new Set(); + const extras: string[] = []; + for (const candidate of cachedModels(parsed)) { + if (typeof candidate?.slug !== "string") continue; + const family = familyFromSlug(candidate.slug); + if (!family || seen.has(family)) continue; + seen.add(family); + if (!(FALLBACK_MODEL_FAMILIES as readonly string[]).includes(family)) { + extras.push(family); + } + } + if (seen.size === 0) return [...FALLBACK_MODEL_FAMILIES]; + return [ + ...FALLBACK_MODEL_FAMILIES.filter((family) => seen.has(family)), + ...extras, + ]; +} + +export function supportedModelOptions(parsed: unknown): ModelOption[] { + const models = cachedModels(parsed); - return MODEL_FAMILIES.flatMap((family) => { + return modelFamiliesFromCache(parsed).flatMap((family) => { const model = models.find( (candidate) => typeof candidate?.slug === "string" && diff --git a/test/codex-store-database.test.ts b/test/codex-store-database.test.ts new file mode 100644 index 0000000..17781c1 --- /dev/null +++ b/test/codex-store-database.test.ts @@ -0,0 +1,162 @@ +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; +import { afterEach, describe, expect, it } from "vitest"; +import { CodexStore, resolveStateDatabase } from "../src/lib/codex-store.js"; +import { collectHealth } from "../src/lib/health.js"; + +const previousSqliteHome = process.env["CODEX_SQLITE_HOME"]; + +afterEach(() => { + if (previousSqliteHome === undefined) { + delete process.env["CODEX_SQLITE_HOME"]; + } else { + process.env["CODEX_SQLITE_HOME"] = previousSqliteHome; + } +}); + +function tempRoot(): string { + return mkdtempSync(join(tmpdir(), "streamdeck-sqlite-")); +} + +function writeState(directory: string, generation: number): string { + const path = join(directory, `state_${generation}.sqlite`); + const database = new DatabaseSync(path); + database.exec("CREATE TABLE threads (id TEXT);"); + database.close(); + return path; +} + +describe("Codex SQLite path discovery", () => { + it("keeps state_5 as the default when it exists beside a newer file", () => { + delete process.env["CODEX_SQLITE_HOME"]; + const root = tempRoot(); + const preferred = writeState(root, 5); + writeState(root, 9); + expect(resolveStateDatabase(root)).toBe(preferred); + }); + + it("falls back to the highest-numbered state_*.sqlite when state_5 is missing", () => { + delete process.env["CODEX_SQLITE_HOME"]; + const root = tempRoot(); + writeState(root, 4); + const newest = writeState(root, 10); + writeState(root, 9); + expect(resolveStateDatabase(root)).toBe(newest); + }); + + it("still names state_5 when no generationed databases exist", () => { + delete process.env["CODEX_SQLITE_HOME"]; + const root = tempRoot(); + expect(resolveStateDatabase(root)).toBe(join(root, "state_5.sqlite")); + }); + + it("honors CODEX_SQLITE_HOME files and directory fallback", () => { + const root = tempRoot(); + const file = writeState(root, 2); + process.env["CODEX_SQLITE_HOME"] = file; + expect(resolveStateDatabase(tempRoot())).toBe(file); + + const directory = tempRoot(); + writeState(directory, 3); + const newest = writeState(directory, 8); + process.env["CODEX_SQLITE_HOME"] = directory; + expect(resolveStateDatabase(tempRoot())).toBe(newest); + }); + + it("honors config.toml sqlite_home with the same state_5-then-fallback rule", () => { + delete process.env["CODEX_SQLITE_HOME"]; + const codexHome = tempRoot(); + const sqliteHome = tempRoot(); + writeState(sqliteHome, 2); + const newest = writeState(sqliteHome, 6); + writeFileSync( + join(codexHome, "config.toml"), + `sqlite_home = "${sqliteHome}"\n`, + ); + expect(resolveStateDatabase(codexHome)).toBe(newest); + + writeState(sqliteHome, 5); + expect(resolveStateDatabase(codexHome)).toBe( + join(sqliteHome, "state_5.sqlite"), + ); + }); +}); + +describe("Codex SQLite fail-safe", () => { + it("maps a missing database to unsupported-schema without throwing", () => { + const store = new CodexStore({ + databasePath: join(tempRoot(), "missing.sqlite"), + activeThreadId: () => "focused", + }); + try { + expect(() => store.recentThreads()).not.toThrow(); + expect(() => store.focusedThread()).not.toThrow(); + expect(store.recentThreads()).toEqual([]); + expect(store.focusedThread()).toBeUndefined(); + expect(store.storeAvailability()).toMatchObject({ + state: "unavailable", + reason: "unsupported-schema", + }); + expect(collectHealth(store).components.store).toMatchObject({ + state: "unavailable", + reason: "unsupported-schema", + }); + } finally { + store.close(); + } + }); + + it("maps a schema change or corrupt file to unsupported-schema", () => { + const root = tempRoot(); + const incompatible = join(root, "incompatible.sqlite"); + const corrupt = join(root, "corrupt.sqlite"); + const database = new DatabaseSync(incompatible); + database.exec("CREATE TABLE unrelated (id TEXT);"); + database.close(); + writeFileSync(corrupt, "not a sqlite database"); + + for (const databasePath of [incompatible, corrupt]) { + const store = new CodexStore({ + databasePath, + activeThreadId: () => "focused", + }); + try { + expect(() => store.recentThreads()).not.toThrow(); + expect(() => store.focusedThread()).not.toThrow(); + expect(store.storeAvailability()).toMatchObject({ + state: "unavailable", + reason: "unsupported-schema", + }); + } finally { + store.close(); + } + } + }); + + it("reports a readable store once the expected tables exist", () => { + const root = tempRoot(); + const databasePath = join(root, "state.sqlite"); + const database = new DatabaseSync(databasePath); + database.exec(` + CREATE TABLE threads ( + id TEXT PRIMARY KEY, rollout_path TEXT, cwd TEXT, title TEXT, + preview TEXT, recency_at_ms INTEGER, reasoning_effort TEXT, + model TEXT, archived INTEGER + ); + CREATE TABLE thread_spawn_edges (child_thread_id TEXT, status TEXT); + `); + database.close(); + const store = new CodexStore({ databasePath }); + try { + expect(store.storeAvailability()).toMatchObject({ + state: "ready", + value: "readable", + }); + expect(store.recentThreads()).toEqual([]); + } finally { + store.close(); + } + }); +}); diff --git a/test/health-diagnostics.test.ts b/test/health-diagnostics.test.ts index 878f33f..7ecde16 100644 --- a/test/health-diagnostics.test.ts +++ b/test/health-diagnostics.test.ts @@ -117,6 +117,10 @@ describe("reason-coded health diagnostics", () => { state: "unavailable", reason: "not-exposed", }); + expect(cached.components.store).toMatchObject({ + state: "ready", + value: "readable", + }); await store.refreshLiveComposer(); await store.usageSnapshot(); @@ -135,6 +139,10 @@ describe("reason-coded health diagnostics", () => { state: "ready", value: "70% left", }); + expect(live.components.store).toMatchObject({ + state: "ready", + value: "readable", + }); } finally { store.close(); spawnSync("trash", [root]); diff --git a/test/model-families.test.ts b/test/model-families.test.ts new file mode 100644 index 0000000..e539cad --- /dev/null +++ b/test/model-families.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { + FALLBACK_MODEL_FAMILIES, + modelFamiliesFromCache, + supportedModelOptions, +} from "../src/lib/model.js"; + +function catalog( + models: Array<{ + slug: string; + display_name?: string; + default_reasoning_level?: string; + supported_reasoning_levels?: string[]; + }>, +) { + return { + models: models.map((model) => ({ + ...model, + default_reasoning_level: model.default_reasoning_level ?? "medium", + supported_reasoning_levels: ( + model.supported_reasoning_levels ?? ["medium"] + ).map((effort) => ({ effort })), + })), + }; +} + +describe("model dial families from cache", () => { + it("falls back to Luna, Terra, Sol, and Astra when the cache is missing", () => { + expect(modelFamiliesFromCache(undefined)).toEqual([ + ...FALLBACK_MODEL_FAMILIES, + ]); + expect(modelFamiliesFromCache(null)).toEqual([...FALLBACK_MODEL_FAMILIES]); + expect(modelFamiliesFromCache({ models: [] })).toEqual([ + ...FALLBACK_MODEL_FAMILIES, + ]); + expect(supportedModelOptions(undefined)).toEqual([]); + }); + + it("keeps the current family order and strips cache-only Max", () => { + const options = supportedModelOptions( + catalog([ + { slug: "gpt-6-astra", supported_reasoning_levels: ["medium", "max"] }, + { slug: "gpt-5.6-sol", supported_reasoning_levels: ["low", "max"] }, + { slug: "gpt-5.6-terra" }, + { slug: "gpt-5.6-luna" }, + ]), + ); + expect(options.map((option) => option.label)).toEqual([ + "LUNA", + "TERRA", + "SOL", + "ASTRA", + ]); + expect( + options.map((option) => option.supportedReasoning).flat(), + ).not.toContain("max"); + }); + + it("does not invent fallback families for a partial catalog", () => { + expect( + supportedModelOptions(catalog([{ slug: "gpt-6-astra" }])).map( + (option) => option.label, + ), + ).toEqual(["ASTRA"]); + }); + + it("derives additional families from matching slugs", () => { + const options = supportedModelOptions( + catalog([ + { slug: "gpt-7-helix", display_name: "GPT-7-Helix" }, + { slug: "gpt-5.6-luna" }, + { slug: "gpt-5.5" }, + ]), + ); + expect(options.map((option) => option.label)).toEqual(["LUNA", "HELIX"]); + expect(options[1]).toMatchObject({ + slug: "gpt-7-helix", + pickerLabel: "Helix", + displayName: "GPT-7-Helix", + }); + }); + + it("rejects unsafe slugs and cache-only reasoning", () => { + expect( + supportedModelOptions( + catalog([ + { slug: "gpt-5.7-terra;open", supported_reasoning_levels: ["low"] }, + { slug: "gpt-5.6-sol", supported_reasoning_levels: ["max"] }, + ]), + ), + ).toEqual([]); + }); +}); diff --git a/test/profile-keys.behavior.test.ts b/test/profile-keys.behavior.test.ts index d920f8f..7a7a451 100644 --- a/test/profile-keys.behavior.test.ts +++ b/test/profile-keys.behavior.test.ts @@ -103,6 +103,11 @@ const harness = vi.hoisted(() => { reason: "unsupported-schema", observedAt: 1, })), + storeAvailability: vi.fn(() => ({ + state: "ready", + value: "readable", + observedAt: 1, + })), }, }; }); diff --git a/vitest.config.ts b/vitest.config.ts index d7786ab..5b5061a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,6 +5,7 @@ import { transformWithOxc } from "vite"; // Node process. `npm run test:fast` skips them for the inner loop; `check` // and CI run both projects. const integrationTests = [ + "test/astra-picker.fixture.test.ts", "test/build-pipeline.test.ts", "test/health-diagnostics.test.ts", "test/keypad-profiles.test.ts",