feat(config): add federated tier + rename team→standalone (FED-M1-01) (#470)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed

This commit was merged in pull request #470.
This commit is contained in:
2026-04-19 23:11:11 +00:00
parent 8aabb8c5b2
commit 9c89c32684
9 changed files with 288 additions and 30 deletions

View File

@@ -1,7 +1,8 @@
export type { MosaicConfig, StorageTier, MemoryConfigRef } from './mosaic-config.js';
export {
DEFAULT_LOCAL_CONFIG,
DEFAULT_TEAM_CONFIG,
DEFAULT_STANDALONE_CONFIG,
DEFAULT_FEDERATED_CONFIG,
loadConfig,
validateConfig,
} from './mosaic-config.js';

View File

@@ -0,0 +1,109 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
validateConfig,
DEFAULT_LOCAL_CONFIG,
DEFAULT_STANDALONE_CONFIG,
DEFAULT_FEDERATED_CONFIG,
} from './mosaic-config.js';
describe('validateConfig — tier enum', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let stderrSpy: any;
beforeEach(() => {
stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
});
afterEach(() => {
stderrSpy.mockRestore();
});
it('accepts tier="local"', () => {
const result = validateConfig({
tier: 'local',
storage: { type: 'pglite', dataDir: '.mosaic/storage-pglite' },
queue: { type: 'local', dataDir: '.mosaic/queue' },
memory: { type: 'keyword' },
});
expect(result.tier).toBe('local');
});
it('accepts tier="standalone"', () => {
const result = validateConfig({
tier: 'standalone',
storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5432/mosaic' },
queue: { type: 'bullmq' },
memory: { type: 'keyword' },
});
expect(result.tier).toBe('standalone');
});
it('accepts tier="federated"', () => {
const result = validateConfig({
tier: 'federated',
storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5433/mosaic' },
queue: { type: 'bullmq' },
memory: { type: 'pgvector' },
});
expect(result.tier).toBe('federated');
});
it('accepts deprecated tier="team" as alias for "standalone" and emits a deprecation warning', () => {
const result = validateConfig({
tier: 'team',
storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5432/mosaic' },
queue: { type: 'bullmq' },
memory: { type: 'keyword' },
});
expect(result.tier).toBe('standalone');
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('DEPRECATED'));
});
it('rejects an invalid tier with an error listing all three valid values', () => {
expect(() =>
validateConfig({
tier: 'invalid',
storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5432/mosaic' },
queue: { type: 'bullmq' },
memory: { type: 'keyword' },
}),
).toThrow(/local.*standalone.*federated|federated.*standalone.*local/);
});
it('error message for invalid tier mentions all three valid values', () => {
let message = '';
try {
validateConfig({
tier: 'invalid',
storage: { type: 'postgres', url: 'postgresql://...' },
queue: { type: 'bullmq' },
memory: { type: 'keyword' },
});
} catch (err) {
message = err instanceof Error ? err.message : String(err);
}
expect(message).toContain('"local"');
expect(message).toContain('"standalone"');
expect(message).toContain('"federated"');
});
});
describe('DEFAULT_* config constants', () => {
it('DEFAULT_LOCAL_CONFIG has tier="local"', () => {
expect(DEFAULT_LOCAL_CONFIG.tier).toBe('local');
});
it('DEFAULT_STANDALONE_CONFIG has tier="standalone"', () => {
expect(DEFAULT_STANDALONE_CONFIG.tier).toBe('standalone');
});
it('DEFAULT_FEDERATED_CONFIG has tier="federated" and pgvector memory', () => {
expect(DEFAULT_FEDERATED_CONFIG.tier).toBe('federated');
expect(DEFAULT_FEDERATED_CONFIG.memory.type).toBe('pgvector');
});
it('DEFAULT_FEDERATED_CONFIG uses port 5433 (distinct from standalone 5432)', () => {
const url = (DEFAULT_FEDERATED_CONFIG.storage as { url: string }).url;
expect(url).toContain('5433');
});
});

View File

@@ -7,7 +7,7 @@ import type { QueueAdapterConfig as QueueConfig } from '@mosaicstack/queue';
/* Types */
/* ------------------------------------------------------------------ */
export type StorageTier = 'local' | 'team';
export type StorageTier = 'local' | 'standalone' | 'federated';
export interface MemoryConfigRef {
type: 'pgvector' | 'sqlite-vec' | 'keyword';
@@ -31,10 +31,17 @@ export const DEFAULT_LOCAL_CONFIG: MosaicConfig = {
memory: { type: 'keyword' },
};
export const DEFAULT_TEAM_CONFIG: MosaicConfig = {
tier: 'team',
export const DEFAULT_STANDALONE_CONFIG: MosaicConfig = {
tier: 'standalone',
storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5432/mosaic' },
queue: { type: 'bullmq' },
memory: { type: 'keyword' },
};
export const DEFAULT_FEDERATED_CONFIG: MosaicConfig = {
tier: 'federated',
storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5433/mosaic' },
queue: { type: 'bullmq' },
memory: { type: 'pgvector' },
};
@@ -42,7 +49,7 @@ export const DEFAULT_TEAM_CONFIG: MosaicConfig = {
/* Validation */
/* ------------------------------------------------------------------ */
const VALID_TIERS = new Set<string>(['local', 'team']);
const VALID_TIERS = new Set<string>(['local', 'standalone', 'federated']);
const VALID_STORAGE_TYPES = new Set<string>(['postgres', 'pglite', 'files']);
const VALID_QUEUE_TYPES = new Set<string>(['bullmq', 'local']);
const VALID_MEMORY_TYPES = new Set<string>(['pgvector', 'sqlite-vec', 'keyword']);
@@ -55,9 +62,19 @@ export function validateConfig(raw: unknown): MosaicConfig {
const obj = raw as Record<string, unknown>;
// tier
const tier = obj['tier'];
let tier = obj['tier'];
// Deprecated alias: 'team' → 'standalone' (kept for backward-compat with 0.0.x installs)
if (tier === 'team') {
process.stderr.write(
'[mosaic] DEPRECATED: tier="team" is deprecated — use "standalone" instead. ' +
'Update your mosaic.config.json.\n',
);
tier = 'standalone';
}
if (typeof tier !== 'string' || !VALID_TIERS.has(tier)) {
throw new Error(`Invalid tier "${String(tier)}" — expected "local" or "team"`);
throw new Error(
`Invalid tier "${String(tier)}" — expected "local", "standalone", or "federated"`,
);
}
// storage
@@ -105,7 +122,7 @@ export function validateConfig(raw: unknown): MosaicConfig {
function detectFromEnv(): MosaicConfig {
if (process.env['DATABASE_URL']) {
return {
...DEFAULT_TEAM_CONFIG,
...DEFAULT_STANDALONE_CONFIG,
storage: {
type: 'postgres',
url: process.env['DATABASE_URL'],