Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 81 additions & 3 deletions patterns/idp/__tests__/feature-toggles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ describe('FileToggleStore', () => {
expect(toggle.createdAt).toBeTruthy();
});

it('creates a toggle with options', () => {
it('creates a toggle with options (new canonical namespaces)', () => {
const store = new FileToggleStore(STORE_PATH);
const toggle = store.create('ENABLE_BETA', 'uiforge-webapp', {
const toggle = store.create('ENABLE_BETA', 'siza', {
description: 'Beta features',
enabled: true
});
expect(toggle.enabled).toBe(true);
expect(toggle.description).toBe('Beta features');
expect(toggle.namespace).toBe('uiforge-webapp');
expect(toggle.namespace).toBe('siza');
});

it('throws when toggle already exists', () => {
Expand Down Expand Up @@ -171,6 +171,84 @@ describe('FileToggleStore', () => {
});
});

describe('canonical namespaces', () => {
it('accepts ui-mcp namespace', () => {
const store = new FileToggleStore(STORE_PATH);
const t = store.create('UI_FEATURE', 'ui-mcp');
expect(t.namespace).toBe('ui-mcp');
});

it('accepts siza namespace', () => {
const store = new FileToggleStore(STORE_PATH);
const t = store.create('SIZA_FEATURE', 'siza', { enabled: true });
expect(t.namespace).toBe('siza');
expect(t.enabled).toBe(true);
});

it('accepts legacy uiforge-mcp namespace for backwards compat', () => {
const store = new FileToggleStore(STORE_PATH);
const t = store.create('LEGACY_MCP', 'uiforge-mcp');
expect(t.namespace).toBe('uiforge-mcp');
});

it('filters by ui-mcp namespace', () => {
const store = new FileToggleStore(STORE_PATH);
store.create('A', 'ui-mcp', { enabled: true });
store.create('B', 'siza');
store.create('C', 'global');
const result = store.list({ namespace: 'ui-mcp' });
expect(result).toHaveLength(1);
expect(result[0]?.name).toBe('A');
});

it('filters by siza namespace', () => {
const store = new FileToggleStore(STORE_PATH);
store.create('A', 'ui-mcp');
store.create('B', 'siza', { enabled: true });
store.create('C', 'siza');
const result = store.list({ namespace: 'siza' });
expect(result).toHaveLength(2);
});
});

describe('create with custom strategies', () => {
it('persists custom strategy config', () => {
const store = new FileToggleStore(STORE_PATH);
const toggle = store.create('GRADUAL', 'global', {
strategies: [{ name: 'gradual-rollout', parameters: { rollout: 50 } }]
});
expect(toggle.strategies[0]?.name).toBe('gradual-rollout');
expect(toggle.strategies[0]?.parameters?.['rollout']).toBe(50);
});

it('persists user-ids strategy', () => {
const store = new FileToggleStore(STORE_PATH);
const toggle = store.create('BETA_USERS', 'global', {
strategies: [{ name: 'user-ids', parameters: { ids: ['u1', 'u2'] } }]
});
expect(toggle.strategies[0]?.name).toBe('user-ids');
});
});

describe('list filtering edge cases', () => {
it('filters by disabled (enabled: false)', () => {
const store = new FileToggleStore(STORE_PATH);
store.create('ON', 'global', { enabled: true });
store.create('OFF1', 'global');
store.create('OFF2', 'mcp-gateway');
const result = store.list({ enabled: false });
expect(result).toHaveLength(2);
expect(result.every(t => !t.enabled)).toBe(true);
});

it('returns empty array for namespace with no toggles', () => {
const store = new FileToggleStore(STORE_PATH);
store.create('A', 'global');
const result = store.list({ namespace: 'ui-mcp' });
expect(result).toHaveLength(0);
});
});

describe('reload', () => {
it('reloads store from file', () => {
const store = new FileToggleStore(STORE_PATH);
Expand Down
8 changes: 5 additions & 3 deletions patterns/idp/feature-toggles/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import type { ToggleNamespace } from './schema.js';
const VALID_NAMESPACES: ToggleNamespace[] = [
'global',
'mcp-gateway',
'uiforge-mcp',
'uiforge-webapp'
'ui-mcp',
'siza',
'uiforge-mcp', // legacy — prefer 'ui-mcp'
'uiforge-webapp' // legacy — prefer 'siza'
];

function parseArgs(args: string[]) {
Expand Down Expand Up @@ -48,7 +50,7 @@ Usage:

Options:
--store <path> Toggle store file (default: .forge/features.json)
--namespace <ns> Filter by namespace: global, mcp-gateway, uiforge-mcp, uiforge-webapp
--namespace <ns> Filter by namespace: global, mcp-gateway, ui-mcp, siza
--description <d> Toggle description
--enabled Filter or create as enabled
--disabled Filter by disabled
Expand Down
9 changes: 8 additions & 1 deletion patterns/idp/feature-toggles/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export type ToggleNamespace = 'global' | 'mcp-gateway' | 'uiforge-mcp' | 'uiforge-webapp';
/** Canonical Forge Space namespaces. Legacy 'uiforge-mcp'/'uiforge-webapp' kept for backwards compat. */
export type ToggleNamespace =
| 'global'
| 'mcp-gateway'
| 'ui-mcp'
| 'siza'
| 'uiforge-mcp' // @deprecated — use 'ui-mcp'
| 'uiforge-webapp'; // @deprecated — use 'siza'

export type ToggleStrategy = 'default' | 'gradual-rollout' | 'user-ids';

Expand Down
Loading