Skip to content

Commit 952a824

Browse files
intel352claude
andcommitted
Fix CI: lint warnings, UI tests for removed module types
- Fix gosec G301: use 0750 directory permissions in feature flag module - Fix gosec G115: mask hash to avoid integer overflow in migration lock - Update UI tests to reflect removed modular module types and added feature flag types (7 removed, 3 added = net -4 module types) - Add featureflag.service, step.feature_flag, step.ff_gate to UI MODULE_TYPES Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d27932d commit 952a824

7 files changed

Lines changed: 61 additions & 32 deletions

File tree

migration/lock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,5 @@ func hashLockKey(key string) int64 {
7575
h ^= uint64(key[i])
7676
h *= 1099511628211 // FNV prime
7777
}
78-
return int64(h)
78+
return int64(h & 0x7FFFFFFFFFFFFFFF) //nolint:gosec // intentional truncation for advisory lock key
7979
}

module/feature_flag_module.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func NewFeatureFlagModule(name string, cfg FeatureFlagModuleConfig) (*FeatureFla
4848
}
4949
// Ensure parent directory exists for the SQLite file.
5050
if dir := filepath.Dir(dbPath); dir != "" && dir != "." {
51-
if mkErr := os.MkdirAll(dir, 0o755); mkErr != nil {
51+
if mkErr := os.MkdirAll(dir, 0o750); mkErr != nil {
5252
return nil, fmt.Errorf("feature flag module %q: failed to create db directory %q: %w", name, dir, mkErr)
5353
}
5454
}

ui/src/components/sidebar/NodePalette.test.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ describe('NodePalette', () => {
1818
expect(screen.getByText('Modules')).toBeInTheDocument();
1919
});
2020

21-
it('renders all categories', () => {
21+
it('renders all categories that have modules', () => {
2222
renderPalette();
2323

24-
for (const cat of CATEGORIES) {
24+
// Only categories with at least one module type are rendered
25+
const categoriesWithModules = CATEGORIES.filter((cat) =>
26+
MODULE_TYPES.some((t) => t.category === cat.key),
27+
);
28+
expect(categoriesWithModules.length).toBeGreaterThan(0);
29+
30+
for (const cat of categoriesWithModules) {
2531
const matches = screen.getAllByText(cat.label);
2632
expect(matches.length).toBeGreaterThanOrEqual(1);
2733
}
@@ -33,12 +39,10 @@ describe('NodePalette', () => {
3339
fireEvent.click(screen.getByText('Middleware'));
3440
fireEvent.click(screen.getByText('Messaging'));
3541
fireEvent.click(screen.getByText('Scheduling'));
36-
fireEvent.click(screen.getByText('Events'));
3742

3843
expect(screen.getByText('Auth Middleware')).toBeInTheDocument();
3944
expect(screen.getByText('Message Broker')).toBeInTheDocument();
4045
expect(screen.getByText('Scheduler')).toBeInTheDocument();
41-
expect(screen.getByText('Event Logger')).toBeInTheDocument();
4246
expect(screen.getByText('Rate Limiter')).toBeInTheDocument();
4347
expect(screen.getByText('CORS Middleware')).toBeInTheDocument();
4448
});

ui/src/store/workflowStore.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,17 @@ describe('workflowStore', () => {
9898
expect(getStore().nodes[0].type).toBe('schedulerNode');
9999
});
100100

101-
it('maps event types to eventNode component', () => {
101+
it('maps notification.slack to integrationNode component', () => {
102102
act(() => {
103-
getStore().addNode('eventbus.modular', { x: 0, y: 0 });
103+
getStore().addNode('notification.slack', { x: 0, y: 0 });
104104
});
105105

106-
expect(getStore().nodes[0].type).toBe('eventNode');
106+
expect(getStore().nodes[0].type).toBe('integrationNode');
107107
});
108108

109-
it('maps httpclient to integrationNode component', () => {
109+
it('maps step types to integrationNode component', () => {
110110
act(() => {
111-
getStore().addNode('httpclient.modular', { x: 0, y: 0 });
111+
getStore().addNode('step.validate', { x: 0, y: 0 });
112112
});
113113

114114
expect(getStore().nodes[0].type).toBe('integrationNode');

ui/src/types/workflow.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { ModuleCategory } from './workflow.ts';
1010
describe('workflow types', () => {
1111
describe('MODULE_TYPES', () => {
1212
it('has the expected number of module types', () => {
13-
expect(MODULE_TYPES.length).toBe(74);
13+
expect(MODULE_TYPES.length).toBe(70);
1414
});
1515

1616
it('each module type has required fields', () => {

ui/src/types/workflow.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,48 @@ export const MODULE_TYPES: ModuleTypeInfo[] = [
940940
{ key: 'schedule', label: 'Schedule Window', type: 'json', description: 'Time window for scheduled gates (weekdays, start_hour, end_hour)' },
941941
],
942942
},
943+
944+
// ---- Feature Flags ----
945+
{
946+
type: 'featureflag.service',
947+
label: 'Feature Flag Service',
948+
category: 'infrastructure',
949+
defaultConfig: { provider: 'generic', cache_ttl: '1m', sse_enabled: true },
950+
configFields: [
951+
{ key: 'provider', label: 'Provider', type: 'select', options: ['generic', 'launchdarkly'], defaultValue: 'generic', description: 'Feature flag backend provider' },
952+
{ key: 'cache_ttl', label: 'Cache TTL', type: 'string', defaultValue: '1m', description: 'Duration to cache flag evaluations', placeholder: '1m' },
953+
{ key: 'sse_enabled', label: 'SSE Enabled', type: 'boolean', defaultValue: true, description: 'Enable Server-Sent Events for real-time flag change notifications' },
954+
{ key: 'store_path', label: 'Store Path', type: 'string', description: 'Path for the flag definition store (file-based provider)', placeholder: 'data/flags.json' },
955+
{ key: 'launchdarkly_sdk_key', label: 'LaunchDarkly SDK Key', type: 'string', sensitive: true, description: 'LaunchDarkly server-side SDK key (required when provider is launchdarkly)', group: 'LaunchDarkly' },
956+
],
957+
ioSignature: { inputs: [], outputs: [{ name: 'featureflag.Service', type: 'featureflag.Service' }] },
958+
maxIncoming: 0,
959+
},
960+
{
961+
type: 'step.feature_flag',
962+
label: 'Feature Flag Check',
963+
category: 'pipeline',
964+
defaultConfig: { output_key: 'flag_value' },
965+
configFields: [
966+
{ key: 'flag', label: 'Flag Key', type: 'string', required: true, description: 'Feature flag key to evaluate', placeholder: 'feature.my-flag' },
967+
{ key: 'user_from', label: 'User From', type: 'string', description: 'Template expression to extract user identifier from context', placeholder: '{{.request.user_id}}' },
968+
{ key: 'group_from', label: 'Group From', type: 'string', description: 'Template expression to extract group identifier from context', placeholder: '{{.request.group}}' },
969+
{ key: 'output_key', label: 'Output Key', type: 'string', defaultValue: 'flag_value', description: 'Key to store the flag value in pipeline context', placeholder: 'flag_value' },
970+
],
971+
},
972+
{
973+
type: 'step.ff_gate',
974+
label: 'Feature Flag Gate',
975+
category: 'pipeline',
976+
defaultConfig: {},
977+
configFields: [
978+
{ key: 'flag', label: 'Flag Key', type: 'string', required: true, description: 'Feature flag key to evaluate', placeholder: 'feature.my-flag' },
979+
{ key: 'on_enabled', label: 'On Enabled', type: 'string', description: 'Branch or step to execute when flag is enabled' },
980+
{ key: 'on_disabled', label: 'On Disabled', type: 'string', description: 'Branch or step to execute when flag is disabled' },
981+
{ key: 'user_from', label: 'User From', type: 'string', description: 'Template expression to extract user identifier from context', placeholder: '{{.request.user_id}}' },
982+
{ key: 'group_from', label: 'Group From', type: 'string', description: 'Template expression to extract group identifier from context', placeholder: '{{.request.group}}' },
983+
],
984+
},
943985
];
944986

945987
export const MODULE_TYPE_MAP: Record<string, ModuleTypeInfo> = Object.fromEntries(

ui/src/utils/serialization.test.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -402,23 +402,6 @@ describe('serialization', () => {
402402
expect((autoWireEdges[0].data as WorkflowEdgeData).label).toBe('auto-wired');
403403
});
404404

405-
it('auto-wires to chimux.router when no http.router exists', () => {
406-
const config: WorkflowConfig = {
407-
modules: [
408-
{ name: 'my-server', type: 'http.server' },
409-
{ name: 'my-chi-router', type: 'chimux.router' },
410-
{ name: 'health', type: 'health.checker' },
411-
],
412-
workflows: {},
413-
triggers: {},
414-
};
415-
416-
const { edges } = configToNodes(config);
417-
const autoWireEdges = edges.filter((e) => (e.data as WorkflowEdgeData)?.edgeType === 'auto-wire');
418-
expect(autoWireEdges).toHaveLength(1);
419-
expect(autoWireEdges[0].target).toContain('chimux_router');
420-
});
421-
422405
it('does not create auto-wire edges when no router exists', () => {
423406
const config: WorkflowConfig = {
424407
modules: [
@@ -487,8 +470,8 @@ describe('serialization', () => {
487470
{ name: 'Broker', type: 'messaging.broker' },
488471
{ name: 'SM', type: 'statemachine.engine' },
489472
{ name: 'Sched', type: 'scheduler.modular' },
490-
{ name: 'EvtBus', type: 'eventbus.modular' },
491-
{ name: 'Client', type: 'httpclient.modular' },
473+
{ name: 'Slack', type: 'notification.slack' },
474+
{ name: 'Step', type: 'step.validate' },
492475
{ name: 'DB', type: 'database.modular' },
493476
],
494477
workflows: {},
@@ -500,7 +483,7 @@ describe('serialization', () => {
500483
expect(nodes[1].type).toBe('messagingNode');
501484
expect(nodes[2].type).toBe('stateMachineNode');
502485
expect(nodes[3].type).toBe('schedulerNode');
503-
expect(nodes[4].type).toBe('eventNode');
486+
expect(nodes[4].type).toBe('integrationNode');
504487
expect(nodes[5].type).toBe('integrationNode');
505488
expect(nodes[6].type).toBe('infrastructureNode');
506489
});

0 commit comments

Comments
 (0)