mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* fix(web): add hover tooltips to Design Files action buttons (#283) The batch-download, select-all, and clear-selection buttons in DesignFilesPanel had no title attribute, so users hovering them saw no tooltip. The other action buttons (refresh, new sketch, paste, upload) already had titles. Added titles to the three missing ones using the existing translation keys, so hover behavior is consistent across the panel. Closes #283. * docs: point pi-ai links to pi-mono packages (#275) The pi project moved from a standalone repo to the pi-mono monorepo. The old URL https://github.com/mariozechner/pi-ai now 404s. Replaced both shapes of reference: - The reference-style [piai]: definition now points at https://github.com/badlogic/pi-mono/tree/main/packages/ai (the multi-provider LLM API package). - Inline links whose visible text is the CLI tool 'pi' or 'Pi' now point at https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent (the interactive coding-agent CLI), so a reader clicking 'pi' in the daemon-discovery section lands on the actual binary's docs. Affected: README.md and 10 translated READMEs, plus docs/spec.md, docs/architecture.md, docs/references.md, docs/roadmap.md. Closes #275. * fix(daemon): expand $HOME / ${HOME} in OD_DATA_DIR (#390) Some launchers (systemd unit files, NixOS modules, certain Docker entrypoints) pass OD_DATA_DIR with a literal '$HOME' or '${HOME}' because no shell ever expands them. resolveDataDir previously only handled '~/' shorthand, so '$HOME/.open-design' fell through to path.resolve(PROJECT_ROOT, '$HOME/.open-design') and produced paths like /opt/open-design/$HOME/.open-design. resolveDataDir now expands '~', '~/...', '$HOME', '$HOME/...', '${HOME}', and '${HOME}/...' to os.homedir() before the absolute / relative branch runs. Rebuilds via path.join so the platform separator is correct on Windows even when the input used forward slashes. Tests: 7 unit tests cover empty/undefined, '~', '~/...', '$HOME', '$HOME/...', '${HOME}/...', absolute paths, and relative paths. Closes #390. * fix(daemon): accept backslash separators + hermetic resolve-data-dir tests Round 1 review feedback on PR #530. The previous regex only matched forward-slash separators, so a Windows launcher passing OD_DATA_DIR=$HOME\.open-design or ${HOME}\.open-design fell through to path.resolve(projectRoot, ...) and produced a directory named $HOME or ~ under projectRoot. The regex now accepts both forward and back slashes for the home-prefix separator. The previous tests called the real resolveDataDir against literal ~/od-test, $HOME/od-test, etc., which created and write-checked directories under the developer's or CI runner's actual home. The tests now stub os.homedir() with vi.spyOn to a per-test mkdtemp directory and remove it in afterEach, so no test ever writes outside its own sandbox. Added explicit fixtures for the Windows backslash forms ($HOME\od-test, ${HOME}\od-test, ~\od-test) so launcher coverage stays cross-platform. 12/12 resolve-data-dir tests pass, daemon typecheck clean. * fix(docs,daemon): apply pi-mono links to README.es and await test cleanup Round 2 review feedback on PR #530. README.es.md was added in upstream #552 after my pi-mono link sweep landed, so the daemon-discovery paragraph (line 222), the [piai] reference (line 684), and the Pi table row (line 709) still pointed at the broken https://github.com/mariozechner/pi-ai URL. Applied the same replacements: the [piai] ref now points at packages/ai, and the inline Pi link now points at packages/coding-agent. Spanish readers get the same coverage as the other 11 locales. The absolute-path test in tests/resolve-data-dir.test.ts dropped its fixture via void rm(abs, ...), so a failed async removal could leak rdd-abs-* directories from the suite. The test is now async and awaits the rm in the finally block, matching the awaited cleanup in afterEach. 12/12 resolve-data-dir tests still pass, daemon typecheck clean. * fix(daemon): share $HOME expander between OD_DATA_DIR and OD_MEDIA_CONFIG_DIR Round 3 review feedback on PR #530. resolveDataDir (server.ts) now expands $HOME / ${HOME} / ~, but media-config.ts had its own resolveOverrideDir that only handled ~/. Because configFile() falls back to OD_DATA_DIR when OD_MEDIA_CONFIG_DIR is unset, setting OD_DATA_DIR=$HOME/.open-design split state: SQLite, projects, and artifacts went to the expanded path while media-config.json stayed under <projectRoot>/$HOME/.open-design. Stored provider keys then appeared missing on the next read. Extracted the home-prefix expansion into apps/daemon/src/home-expansion.ts so resolveDataDir and resolveOverrideDir share one resolver. Both now recognize ~ / $HOME / ${HOME} (bare tokens) and ~/, ~\, $HOME/, $HOME\, ${HOME}/, ${HOME}\ (prefix forms with either separator). Three new media-config routing tests cover the OD_DATA_DIR fallback for $HOME/..., ${HOME}/..., and the OD_MEDIA_CONFIG_DIR explicit-override $HOME/... case so the co-location guarantee is locked down by tests. Daemon typecheck clean. Tests pass on Linux CI; the existing pattern in the file uses process.env.HOME which os.homedir() reads on POSIX. Resolve-data-dir tests stay hermetic via vi.spyOn. * docs(daemon): media-config comments reflect full $HOME / ${HOME} expansion Round 3 review feedback on PR #530 (lefarcen, P3 non-blocking). The file-header and resolveOverrideDir() function comment said only ~/ expands. Updated both to mention the shared expandHomePrefix() helper and the full set of forms it handles (~, $HOME, ${HOME} with either separator), so a future reader does not need to chase the implementation to understand what env values are accepted. * test(daemon): stub os.homedir() in media-config routing tests Round 4 review feedback on PR #530. The new $HOME / ${HOME} routing tests relied on process.env.HOME being read by os.homedir(), which works on POSIX but is unreliable on Windows (Node prefers USERPROFILE / profile APIs there). The tests would expand to the real user home while fixtures were written under the per-test homeDir, causing platform-specific failures in the same area this PR is making cross-platform. The inner describe block now stubs os.homedir() via vi.spyOn to return the per-test homeDir, matching the pattern in resolve-data-dir.test.ts. Restored in afterEach. The four $HOME-form routing tests now pass on both POSIX and Windows. Daemon typecheck clean. The two OAuth fallback test failures unrelated to this change (real ChatGPT/Codex tokens in the local env) remain out-of-scope. * fix(i18n): drop duplicate uk.ts promptTemplates keys after rebase Upstream #674 added the same Ukrainian translations my earlier commit added. The rebase landed both copies; tsc rejects duplicate property names. Drop my copies so #674 (which is now upstream) is the single source for these keys. --------- Co-authored-by: Nagendhra <nagendhra405@gmail.com>
452 lines
15 KiB
TypeScript
452 lines
15 KiB
TypeScript
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import os, { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
readMaskedConfig,
|
|
resolveProviderConfig,
|
|
writeConfig,
|
|
} from '../src/media-config.js';
|
|
|
|
const TEST_NANOBANANA_BASE_URL = 'https://nano-banana-gateway.example.test';
|
|
|
|
const OPENAI_ENV_KEYS = [
|
|
'OD_OPENAI_API_KEY',
|
|
'OPENAI_API_KEY',
|
|
'AZURE_API_KEY',
|
|
'AZURE_OPENAI_API_KEY',
|
|
];
|
|
|
|
describe('media-config OpenAI OAuth fallback', () => {
|
|
let homeDir: string;
|
|
let projectRoot: string;
|
|
const originalHome = process.env.HOME;
|
|
const originalEnv = Object.fromEntries(
|
|
OPENAI_ENV_KEYS.map((key) => [key, process.env[key]]),
|
|
);
|
|
const originalMediaConfigDir = process.env.OD_MEDIA_CONFIG_DIR;
|
|
const originalDataDir = process.env.OD_DATA_DIR;
|
|
|
|
beforeEach(async () => {
|
|
homeDir = await mkdtemp(path.join(tmpdir(), 'od-media-home-'));
|
|
projectRoot = await mkdtemp(path.join(tmpdir(), 'od-media-project-'));
|
|
process.env.HOME = homeDir;
|
|
for (const key of OPENAI_ENV_KEYS) {
|
|
delete process.env[key];
|
|
}
|
|
delete process.env.OD_MEDIA_CONFIG_DIR;
|
|
delete process.env.OD_DATA_DIR;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (originalHome == null) {
|
|
delete process.env.HOME;
|
|
} else {
|
|
process.env.HOME = originalHome;
|
|
}
|
|
for (const key of OPENAI_ENV_KEYS) {
|
|
if (originalEnv[key] == null) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = originalEnv[key];
|
|
}
|
|
}
|
|
if (originalMediaConfigDir == null) {
|
|
delete process.env.OD_MEDIA_CONFIG_DIR;
|
|
} else {
|
|
process.env.OD_MEDIA_CONFIG_DIR = originalMediaConfigDir;
|
|
}
|
|
if (originalDataDir == null) {
|
|
delete process.env.OD_DATA_DIR;
|
|
} else {
|
|
process.env.OD_DATA_DIR = originalDataDir;
|
|
}
|
|
await rm(homeDir, { recursive: true, force: true });
|
|
await rm(projectRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
async function writeHomeJson(relPath: string, data: unknown) {
|
|
const file = path.join(homeDir, relPath);
|
|
await mkdir(path.dirname(file), { recursive: true });
|
|
await writeFile(file, JSON.stringify(data), 'utf8');
|
|
}
|
|
|
|
async function writeStoredMediaConfig(data: unknown) {
|
|
const file = path.join(projectRoot, '.od', 'media-config.json');
|
|
await mkdir(path.dirname(file), { recursive: true });
|
|
await writeFile(file, JSON.stringify(data), 'utf8');
|
|
}
|
|
|
|
function openaiProvider(masked: { providers: unknown }) {
|
|
return (masked.providers as Record<string, unknown>).openai;
|
|
}
|
|
|
|
it('uses Hermes openai-codex OAuth when no API key is configured', async () => {
|
|
await writeHomeJson('.hermes/auth.json', {
|
|
providers: {
|
|
'openai-codex': {
|
|
tokens: { access_token: 'hermes-oauth-token' },
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
const masked = await readMaskedConfig(projectRoot);
|
|
|
|
expect(resolved.apiKey).toBe('hermes-oauth-token');
|
|
expect(openaiProvider(masked)).toMatchObject({
|
|
configured: true,
|
|
source: 'oauth-hermes',
|
|
apiKeyTail: '',
|
|
});
|
|
});
|
|
|
|
it('uses Codex OAuth when Hermes has no OpenAI Codex credential', async () => {
|
|
await writeHomeJson('.codex/auth.json', {
|
|
tokens: { access_token: 'codex-oauth-token' },
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
const masked = await readMaskedConfig(projectRoot);
|
|
|
|
expect(resolved.apiKey).toBe('codex-oauth-token');
|
|
expect(openaiProvider(masked)).toMatchObject({
|
|
configured: true,
|
|
source: 'oauth-codex',
|
|
apiKeyTail: '',
|
|
});
|
|
});
|
|
|
|
it('keeps stored provider config ahead of OAuth fallbacks', async () => {
|
|
await writeHomeJson('.hermes/auth.json', {
|
|
providers: {
|
|
'openai-codex': {
|
|
tokens: { access_token: 'hermes-oauth-token' },
|
|
},
|
|
},
|
|
});
|
|
await writeStoredMediaConfig({
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'stored-openai-key',
|
|
baseUrl: 'https://example.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
const masked = await readMaskedConfig(projectRoot);
|
|
|
|
expect(resolved).toEqual({
|
|
apiKey: 'stored-openai-key',
|
|
baseUrl: 'https://example.test/v1',
|
|
});
|
|
expect(openaiProvider(masked)).toMatchObject({
|
|
configured: true,
|
|
source: 'stored',
|
|
apiKeyTail: '-key',
|
|
baseUrl: 'https://example.test/v1',
|
|
});
|
|
});
|
|
|
|
it('resolves Nano Banana env and stored model overrides', async () => {
|
|
process.env.OD_NANOBANANA_API_KEY = 'env-nano-key';
|
|
await writeStoredMediaConfig({
|
|
providers: {
|
|
nanobanana: {
|
|
apiKey: 'stored-nano-key',
|
|
baseUrl: TEST_NANOBANANA_BASE_URL,
|
|
model: 'gemini-3.1-flash-image-preview-custom',
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'nanobanana');
|
|
const masked = await readMaskedConfig(projectRoot);
|
|
const provider = (masked.providers as Record<string, unknown>).nanobanana;
|
|
|
|
expect(resolved).toEqual({
|
|
apiKey: 'env-nano-key',
|
|
baseUrl: TEST_NANOBANANA_BASE_URL,
|
|
model: 'gemini-3.1-flash-image-preview-custom',
|
|
});
|
|
expect(provider).toMatchObject({
|
|
configured: true,
|
|
source: 'env',
|
|
apiKeyTail: '-key',
|
|
baseUrl: TEST_NANOBANANA_BASE_URL,
|
|
model: 'gemini-3.1-flash-image-preview-custom',
|
|
});
|
|
|
|
delete process.env.OD_NANOBANANA_API_KEY;
|
|
});
|
|
|
|
describe('OD_MEDIA_CONFIG_DIR / OD_DATA_DIR storage routing', () => {
|
|
let overrideRoot: string;
|
|
let originalMediaConfigDir: string | undefined;
|
|
let originalDataDir: string | undefined;
|
|
let homedirSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(async () => {
|
|
overrideRoot = await mkdtemp(path.join(tmpdir(), 'od-media-override-'));
|
|
originalMediaConfigDir = process.env.OD_MEDIA_CONFIG_DIR;
|
|
originalDataDir = process.env.OD_DATA_DIR;
|
|
delete process.env.OD_MEDIA_CONFIG_DIR;
|
|
delete process.env.OD_DATA_DIR;
|
|
// Stub os.homedir() to point at the per-test fake home so the
|
|
// ~/, $HOME, ${HOME} expansion in resolveOverrideDir lands inside
|
|
// homeDir on every platform. Without this the production path
|
|
// (which now goes through expandHomePrefix -> os.homedir()) would
|
|
// expand to USERPROFILE on Windows while the fixture is written
|
|
// under homeDir, and the assertion would fail platform-specifically.
|
|
homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(homeDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (originalMediaConfigDir == null) {
|
|
delete process.env.OD_MEDIA_CONFIG_DIR;
|
|
} else {
|
|
process.env.OD_MEDIA_CONFIG_DIR = originalMediaConfigDir;
|
|
}
|
|
if (originalDataDir == null) {
|
|
delete process.env.OD_DATA_DIR;
|
|
} else {
|
|
process.env.OD_DATA_DIR = originalDataDir;
|
|
}
|
|
homedirSpy.mockRestore();
|
|
await rm(overrideRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
async function writeProvidersAt(dir: string, data: unknown) {
|
|
await mkdir(dir, { recursive: true });
|
|
await writeFile(
|
|
path.join(dir, 'media-config.json'),
|
|
JSON.stringify(data),
|
|
'utf8',
|
|
);
|
|
}
|
|
|
|
it('reads media-config.json from an absolute OD_MEDIA_CONFIG_DIR', async () => {
|
|
process.env.OD_MEDIA_CONFIG_DIR = overrideRoot;
|
|
await writeProvidersAt(overrideRoot, {
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'absolute-key',
|
|
baseUrl: 'https://absolute.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
expect(resolved).toEqual({
|
|
apiKey: 'absolute-key',
|
|
baseUrl: 'https://absolute.test/v1',
|
|
});
|
|
});
|
|
|
|
it('expands a leading ~/ against the user home directory', async () => {
|
|
// Per-test HOME points at a tmpdir (set by outer beforeEach), so the
|
|
// expansion lands somewhere safe to write.
|
|
const subdir = '.od-test';
|
|
process.env.OD_MEDIA_CONFIG_DIR = `~/${subdir}`;
|
|
const expandedDir = path.join(homeDir, subdir);
|
|
await writeProvidersAt(expandedDir, {
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'tilde-key',
|
|
baseUrl: 'https://tilde.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
expect(resolved).toEqual({
|
|
apiKey: 'tilde-key',
|
|
baseUrl: 'https://tilde.test/v1',
|
|
});
|
|
});
|
|
|
|
it('resolves a relative override against projectRoot, not process.cwd', async () => {
|
|
// process.cwd() during tests is typically the workspace root, which
|
|
// is unrelated to the per-test projectRoot. A relative override must
|
|
// land inside projectRoot, mirroring how resolveDataDir() in
|
|
// server.ts anchors OD_DATA_DIR.
|
|
const relative = 'config/media';
|
|
process.env.OD_MEDIA_CONFIG_DIR = relative;
|
|
const anchoredDir = path.join(projectRoot, relative);
|
|
await writeProvidersAt(anchoredDir, {
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'relative-key',
|
|
baseUrl: 'https://relative.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
expect(resolved).toEqual({
|
|
apiKey: 'relative-key',
|
|
baseUrl: 'https://relative.test/v1',
|
|
});
|
|
});
|
|
|
|
it('falls back to OD_DATA_DIR when OD_MEDIA_CONFIG_DIR is unset', async () => {
|
|
// Packaged daemon (apps/packaged/src/sidecars.ts) and the
|
|
// Home Manager / NixOS modules already set OD_DATA_DIR for the
|
|
// rest of the daemon's runtime state. media-config should
|
|
// co-locate there without needing a second env var.
|
|
process.env.OD_DATA_DIR = overrideRoot;
|
|
await writeProvidersAt(overrideRoot, {
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'datadir-key',
|
|
baseUrl: 'https://datadir.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
expect(resolved).toEqual({
|
|
apiKey: 'datadir-key',
|
|
baseUrl: 'https://datadir.test/v1',
|
|
});
|
|
});
|
|
|
|
it('OD_MEDIA_CONFIG_DIR takes precedence over OD_DATA_DIR', async () => {
|
|
const dataDir = await mkdtemp(path.join(tmpdir(), 'od-media-data-'));
|
|
try {
|
|
process.env.OD_DATA_DIR = dataDir;
|
|
process.env.OD_MEDIA_CONFIG_DIR = overrideRoot;
|
|
// Two competing files; only the OD_MEDIA_CONFIG_DIR one should
|
|
// be read.
|
|
await writeProvidersAt(dataDir, {
|
|
providers: {
|
|
openai: { apiKey: 'data-key', baseUrl: 'https://data/v1' },
|
|
},
|
|
});
|
|
await writeProvidersAt(overrideRoot, {
|
|
providers: {
|
|
openai: { apiKey: 'media-key', baseUrl: 'https://media/v1' },
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
expect(resolved).toEqual({
|
|
apiKey: 'media-key',
|
|
baseUrl: 'https://media/v1',
|
|
});
|
|
} finally {
|
|
await rm(dataDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('writeConfig creates the override directory tree on first write', async () => {
|
|
// Reproduces the actual user-reported failure mode: the override
|
|
// directory does not exist yet (first launch on a read-only
|
|
// install root), so writeConfig must mkdir -p before writing.
|
|
// Without recursive mkdir + a writable override, this would
|
|
// surface as ENOENT/EROFS to PUT /api/media/config.
|
|
const target = path.join(overrideRoot, 'nested', 'inner');
|
|
process.env.OD_MEDIA_CONFIG_DIR = target;
|
|
|
|
await writeConfig(projectRoot, {
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'fresh-write-key',
|
|
baseUrl: 'https://fresh.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
// File materialised at the override path.
|
|
const onDisk = await readFile(
|
|
path.join(target, 'media-config.json'),
|
|
'utf8',
|
|
);
|
|
expect(JSON.parse(onDisk)).toEqual({
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'fresh-write-key',
|
|
baseUrl: 'https://fresh.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
// And resolveProviderConfig reads it back correctly.
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
expect(resolved).toEqual({
|
|
apiKey: 'fresh-write-key',
|
|
baseUrl: 'https://fresh.test/v1',
|
|
});
|
|
});
|
|
|
|
// Round 3 review feedback on PR #530.
|
|
// resolveOverrideDir shares expandHomePrefix with resolveDataDir, so
|
|
// OD_DATA_DIR=$HOME/.open-design (and ${HOME}/.open-design) routes
|
|
// both daemon runtime data AND media credentials to the same expanded
|
|
// path. Without this, media-config.json was written under
|
|
// <projectRoot>/$HOME/.open-design and stored provider keys appeared
|
|
// missing on the next read.
|
|
it('expands $HOME/... in OD_DATA_DIR fallback so media-config co-locates with daemon data', async () => {
|
|
const subdir = '.od-test-home';
|
|
process.env.OD_DATA_DIR = `$HOME/${subdir}`;
|
|
const expandedDir = path.join(homeDir, subdir);
|
|
await writeProvidersAt(expandedDir, {
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'home-key',
|
|
baseUrl: 'https://home.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
expect(resolved).toEqual({
|
|
apiKey: 'home-key',
|
|
baseUrl: 'https://home.test/v1',
|
|
});
|
|
});
|
|
|
|
it('expands ${HOME}/... in OD_DATA_DIR fallback', async () => {
|
|
const subdir = '.od-test-braced';
|
|
process.env.OD_DATA_DIR = `\${HOME}/${subdir}`;
|
|
const expandedDir = path.join(homeDir, subdir);
|
|
await writeProvidersAt(expandedDir, {
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'braced-key',
|
|
baseUrl: 'https://braced.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
expect(resolved).toEqual({
|
|
apiKey: 'braced-key',
|
|
baseUrl: 'https://braced.test/v1',
|
|
});
|
|
});
|
|
|
|
it('expands $HOME/... in OD_MEDIA_CONFIG_DIR (explicit override path)', async () => {
|
|
const subdir = '.od-media-home';
|
|
process.env.OD_MEDIA_CONFIG_DIR = `$HOME/${subdir}`;
|
|
const expandedDir = path.join(homeDir, subdir);
|
|
await writeProvidersAt(expandedDir, {
|
|
providers: {
|
|
openai: {
|
|
apiKey: 'media-home-key',
|
|
baseUrl: 'https://media-home.test/v1',
|
|
},
|
|
},
|
|
});
|
|
|
|
const resolved = await resolveProviderConfig(projectRoot, 'openai');
|
|
expect(resolved).toEqual({
|
|
apiKey: 'media-home-key',
|
|
baseUrl: 'https://media-home.test/v1',
|
|
});
|
|
});
|
|
});
|
|
});
|