Skip to content
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
4 changes: 4 additions & 0 deletions packages/flagship/src/client-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ export class FlagshipClientProvider implements Provider {
};
}

if (cached.reason === 'DISABLED') {
return { value: defaultValue, reason: 'DISABLED', flagMetadata: {} };
}

const actualType = this.getValueType(cached.value);
if (actualType !== expectedType) {
const msg = `Flag "${flagKey}" type mismatch: expected ${expectedType}, got ${actualType}`;
Expand Down
10 changes: 9 additions & 1 deletion packages/flagship/src/server-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export class FlagshipServerProvider implements Provider {
}

// ---------------------------------------------------------------------------
// HTTP mode resolution (existing behaviour)
// HTTP mode resolution
// ---------------------------------------------------------------------------

private async resolveViaHttp<T>(
Expand All @@ -217,6 +217,10 @@ export class FlagshipServerProvider implements Provider {

const result = await this.client!.evaluate(flagKey, context);

if (result.reason === 'DISABLED') {
return { value: defaultValue, reason: 'DISABLED', flagMetadata: {} };
}

const actualType = getValueType(result.value);
if (actualType !== expectedType) {
const msg = `Flag "${flagKey}" type mismatch: expected ${expectedType}, got ${actualType}`;
Expand Down Expand Up @@ -293,6 +297,10 @@ export class FlagshipServerProvider implements Provider {
return { value: defaultValue, errorCode, errorMessage, reason: details.reason ?? 'ERROR' };
}

if (details.reason === 'DISABLED') {
return { value: defaultValue, reason: 'DISABLED', flagMetadata: {} };
}

// Type-check the resolved value.
const actualType = getValueType(details.value);
if (actualType !== expectedType) {
Expand Down
75 changes: 75 additions & 0 deletions packages/flagship/tests/binding-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,81 @@ describe('FlagshipServerProvider (binding mode)', () => {
});
});

// -----------------------------------------------------------------------
// DISABLED flag — falls back to SDK default
// -----------------------------------------------------------------------

describe('DISABLED flag — falls back to SDK default', () => {
it('returns SDK defaultValue (not the flag variation) for a boolean flag', async () => {
const binding = createMockBinding();
(binding.getBooleanDetails as any).mockResolvedValueOnce({
flagKey: 'my-flag',
value: true, // flag's stored default variation — should NOT be used
variant: 'on',
reason: 'DISABLED',
});

const provider = new FlagshipServerProvider({ binding });
const result = await provider.resolveBooleanEvaluation('my-flag', false, {}, noopLogger);

expect(result.value).toBe(false); // SDK caller's default
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
expect(result.variant).toBeUndefined();
});

it('returns SDK defaultValue for a string flag', async () => {
const binding = createMockBinding();
(binding.getStringDetails as any).mockResolvedValueOnce({
flagKey: 'my-flag',
value: 'flag-default',
variant: 'flag-variant',
reason: 'DISABLED',
});

const provider = new FlagshipServerProvider({ binding });
const result = await provider.resolveStringEvaluation('my-flag', 'sdk-default', {}, noopLogger);

expect(result.value).toBe('sdk-default');
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
});

it('returns SDK defaultValue for a number flag', async () => {
const binding = createMockBinding();
(binding.getNumberDetails as any).mockResolvedValueOnce({
flagKey: 'my-flag',
value: 99,
variant: 'high',
reason: 'DISABLED',
});

const provider = new FlagshipServerProvider({ binding });
const result = await provider.resolveNumberEvaluation('my-flag', 0, {}, noopLogger);

expect(result.value).toBe(0);
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
});

it('returns SDK defaultValue for an object flag', async () => {
const binding = createMockBinding();
(binding.getObjectDetails as any).mockResolvedValueOnce({
flagKey: 'my-flag',
value: { stored: true },
variant: 'stored-variant',
reason: 'DISABLED',
});

const provider = new FlagshipServerProvider({ binding });
const result = await provider.resolveObjectEvaluation('my-flag', { sdk: true }, {}, noopLogger);

expect(result.value).toEqual({ sdk: true });
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
});
});

// -----------------------------------------------------------------------
// Context conversion
// -----------------------------------------------------------------------
Expand Down
53 changes: 53 additions & 0 deletions packages/flagship/tests/client-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,4 +626,57 @@ describe('FlagshipClientProvider', () => {
expect(provider.runsOn).toBe('client');
});
});

describe('DISABLED flag — falls back to SDK default', () => {
async function buildProviderWithDisabledFlag(flagKey: string, flagValue: unknown, variant: string): Promise<FlagshipClientProvider> {
(FlagshipClient as any).mockImplementation(function () {
return {
evaluate: vi.fn().mockResolvedValue({ flagKey, value: flagValue, variant, reason: 'DISABLED' }),
};
});
const provider = new FlagshipClientProvider({
endpoint: 'https://api.example.com/evaluate',
prefetchFlags: [flagKey],
});
await provider.initialize({});
return provider;
}

it('returns SDK defaultValue (not the flag variation) for a boolean flag', async () => {
const provider = await buildProviderWithDisabledFlag('my-flag', true, 'on');
const result = provider.resolveBooleanEvaluation('my-flag', false, {}, noopLogger);

expect(result.value).toBe(false); // SDK caller's default, not the flag's stored 'on' variation
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
expect(result.variant).toBeUndefined();
});

it('returns SDK defaultValue for a string flag', async () => {
const provider = await buildProviderWithDisabledFlag('my-flag', 'flag-default', 'flag-variant');
const result = provider.resolveStringEvaluation('my-flag', 'sdk-default', {}, noopLogger);

expect(result.value).toBe('sdk-default');
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
});

it('returns SDK defaultValue for a number flag', async () => {
const provider = await buildProviderWithDisabledFlag('my-flag', 99, 'high');
const result = provider.resolveNumberEvaluation('my-flag', 0, {}, noopLogger);

expect(result.value).toBe(0);
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
});

it('returns SDK defaultValue for an object flag', async () => {
const provider = await buildProviderWithDisabledFlag('my-flag', { stored: true }, 'stored-variant');
const result = provider.resolveObjectEvaluation('my-flag', { sdk: true }, {}, noopLogger);

expect(result.value).toEqual({ sdk: true });
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
});
});
});
59 changes: 59 additions & 0 deletions packages/flagship/tests/server-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -837,4 +837,63 @@ describe('FlagshipServerProvider', () => {
expect(result.errorCode).toBe(ErrorCode.PARSE_ERROR);
});
});

describe('DISABLED flag — falls back to SDK default', () => {
it('returns SDK defaultValue (not the flag variation) for a boolean flag', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ flagKey: 'my-flag', value: true, variant: 'on', reason: 'DISABLED' }),
});

const provider = new FlagshipServerProvider({ endpoint: 'https://api.example.com/evaluate' });
const result = await provider.resolveBooleanEvaluation('my-flag', false, {}, noopLogger);

expect(result.value).toBe(false); // SDK caller's default, not the flag's 'on' variation
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
expect(result.variant).toBeUndefined();
});

it('returns SDK defaultValue for a string flag', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ flagKey: 'my-flag', value: 'flag-default', variant: 'flag-variant', reason: 'DISABLED' }),
});

const provider = new FlagshipServerProvider({ endpoint: 'https://api.example.com/evaluate' });
const result = await provider.resolveStringEvaluation('my-flag', 'sdk-default', {}, noopLogger);

expect(result.value).toBe('sdk-default');
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
});

it('returns SDK defaultValue for a number flag', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ flagKey: 'my-flag', value: 99, variant: 'high', reason: 'DISABLED' }),
});

const provider = new FlagshipServerProvider({ endpoint: 'https://api.example.com/evaluate' });
const result = await provider.resolveNumberEvaluation('my-flag', 0, {}, noopLogger);

expect(result.value).toBe(0);
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
});

it('returns SDK defaultValue for an object flag', async () => {
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ flagKey: 'my-flag', value: { stored: true }, variant: 'stored-variant', reason: 'DISABLED' }),
});

const provider = new FlagshipServerProvider({ endpoint: 'https://api.example.com/evaluate' });
const result = await provider.resolveObjectEvaluation('my-flag', { sdk: true }, {}, noopLogger);

expect(result.value).toEqual({ sdk: true });
expect(result.reason).toBe('DISABLED');
expect(result.errorCode).toBeUndefined();
});
});
});
Loading