@omnidev-ai/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +31 -0
  2. package/src/capability/AGENTS.md +58 -0
  3. package/src/capability/commands.test.ts +414 -0
  4. package/src/capability/commands.ts +70 -0
  5. package/src/capability/docs.test.ts +199 -0
  6. package/src/capability/docs.ts +46 -0
  7. package/src/capability/index.ts +20 -0
  8. package/src/capability/loader.test.ts +815 -0
  9. package/src/capability/loader.ts +492 -0
  10. package/src/capability/registry.test.ts +473 -0
  11. package/src/capability/registry.ts +55 -0
  12. package/src/capability/rules.test.ts +145 -0
  13. package/src/capability/rules.ts +133 -0
  14. package/src/capability/skills.test.ts +316 -0
  15. package/src/capability/skills.ts +56 -0
  16. package/src/capability/sources.test.ts +338 -0
  17. package/src/capability/sources.ts +966 -0
  18. package/src/capability/subagents.test.ts +478 -0
  19. package/src/capability/subagents.ts +103 -0
  20. package/src/capability/yaml-parser.ts +81 -0
  21. package/src/config/AGENTS.md +46 -0
  22. package/src/config/capabilities.ts +82 -0
  23. package/src/config/env.test.ts +286 -0
  24. package/src/config/env.ts +96 -0
  25. package/src/config/index.ts +6 -0
  26. package/src/config/loader.test.ts +282 -0
  27. package/src/config/loader.ts +137 -0
  28. package/src/config/parser.test.ts +281 -0
  29. package/src/config/parser.ts +55 -0
  30. package/src/config/profiles.test.ts +259 -0
  31. package/src/config/profiles.ts +75 -0
  32. package/src/config/provider.test.ts +79 -0
  33. package/src/config/provider.ts +55 -0
  34. package/src/debug.ts +20 -0
  35. package/src/gitignore/manager.test.ts +219 -0
  36. package/src/gitignore/manager.ts +167 -0
  37. package/src/index.test.ts +26 -0
  38. package/src/index.ts +39 -0
  39. package/src/mcp-json/index.ts +1 -0
  40. package/src/mcp-json/manager.test.ts +415 -0
  41. package/src/mcp-json/manager.ts +118 -0
  42. package/src/state/active-profile.test.ts +131 -0
  43. package/src/state/active-profile.ts +41 -0
  44. package/src/state/index.ts +2 -0
  45. package/src/state/manifest.test.ts +548 -0
  46. package/src/state/manifest.ts +164 -0
  47. package/src/sync.ts +213 -0
  48. package/src/templates/agents.test.ts +23 -0
  49. package/src/templates/agents.ts +14 -0
  50. package/src/templates/claude.test.ts +48 -0
  51. package/src/templates/claude.ts +122 -0
  52. package/src/test-utils/helpers.test.ts +196 -0
  53. package/src/test-utils/helpers.ts +187 -0
  54. package/src/test-utils/index.ts +30 -0
  55. package/src/test-utils/mocks.test.ts +83 -0
  56. package/src/test-utils/mocks.ts +101 -0
  57. package/src/types/capability-export.ts +234 -0
  58. package/src/types/index.test.ts +28 -0
  59. package/src/types/index.ts +270 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Helper functions for testing
3
+ */
4
+
5
+ import { expect } from "bun:test";
6
+
7
+ /**
8
+ * Expects an async function to throw an error
9
+ * @param fn - Async function that should throw
10
+ * @param errorMatch - Optional string or regex to match against error message
11
+ * @throws If the function doesn't throw
12
+ */
13
+ export async function expectToThrowAsync(
14
+ fn: () => Promise<unknown>,
15
+ errorMatch?: string | RegExp,
16
+ ): Promise<void> {
17
+ let threw = false;
18
+ let caughtError: Error | undefined;
19
+
20
+ try {
21
+ await fn();
22
+ } catch (e) {
23
+ threw = true;
24
+ caughtError = e as Error;
25
+ }
26
+
27
+ expect(threw).toBe(true);
28
+
29
+ if (errorMatch && caughtError) {
30
+ if (typeof errorMatch === "string") {
31
+ expect(caughtError.message).toContain(errorMatch);
32
+ } else {
33
+ expect(caughtError.message).toMatch(errorMatch);
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Waits for a condition to be true
40
+ * @param condition - Function that returns true when condition is met
41
+ * @param timeout - Maximum time to wait in milliseconds (default: 1000)
42
+ * @param interval - Check interval in milliseconds (default: 50)
43
+ * @throws If timeout is reached before condition is met
44
+ */
45
+ export async function waitForCondition(
46
+ condition: () => boolean | Promise<boolean>,
47
+ timeout = 1000,
48
+ interval = 50,
49
+ ): Promise<void> {
50
+ const startTime = Date.now();
51
+
52
+ while (Date.now() - startTime < timeout) {
53
+ const result = await condition();
54
+ if (result) {
55
+ return;
56
+ }
57
+ await delay(interval);
58
+ }
59
+
60
+ throw new Error(`Condition not met within ${timeout}ms`);
61
+ }
62
+
63
+ /**
64
+ * Delays execution for a specified amount of time
65
+ * @param ms - Milliseconds to delay
66
+ */
67
+ export function delay(ms: number): Promise<void> {
68
+ return new Promise((resolve) => setTimeout(resolve, ms));
69
+ }
70
+
71
+ /**
72
+ * Creates a spy function that records calls and arguments
73
+ * @returns Spy function with call tracking
74
+ */
75
+ export function createSpy<TArgs extends unknown[], TReturn>(
76
+ implementation?: (...args: TArgs) => TReturn,
77
+ ) {
78
+ const calls: TArgs[] = [];
79
+
80
+ const spy = ((...args: TArgs) => {
81
+ calls.push(args);
82
+ if (implementation) {
83
+ return implementation(...args);
84
+ }
85
+ return undefined as TReturn;
86
+ }) as {
87
+ (...args: TArgs): TReturn;
88
+ calls: TArgs[];
89
+ callCount: number;
90
+ reset: () => void;
91
+ };
92
+
93
+ Object.defineProperty(spy, "calls", {
94
+ get: () => calls,
95
+ });
96
+
97
+ Object.defineProperty(spy, "callCount", {
98
+ get: () => calls.length,
99
+ });
100
+
101
+ spy.reset = () => {
102
+ calls.length = 0;
103
+ };
104
+
105
+ return spy;
106
+ }
107
+
108
+ /**
109
+ * Creates a mock function that returns predefined values
110
+ * @param returnValues - Array of values to return on consecutive calls
111
+ * @returns Mock function
112
+ */
113
+ export function createMockFn<T>(...returnValues: T[]): () => T {
114
+ let callIndex = 0;
115
+
116
+ return () => {
117
+ if (callIndex >= returnValues.length) {
118
+ throw new Error("Mock function called more times than return values provided");
119
+ }
120
+ const value = returnValues[callIndex++];
121
+ if (value === undefined) {
122
+ throw new Error("Mock function returned undefined");
123
+ }
124
+ return value;
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Creates a mock promise that can be resolved or rejected manually
130
+ * @returns Object with promise and resolve/reject functions
131
+ */
132
+ export function createDeferredPromise<T>() {
133
+ let resolveRef: ((value: T) => void) | undefined;
134
+ let rejectRef: ((reason?: unknown) => void) | undefined;
135
+
136
+ const promise = new Promise<T>((res, rej) => {
137
+ resolveRef = res;
138
+ rejectRef = rej;
139
+ });
140
+
141
+ if (!resolveRef || !rejectRef) {
142
+ throw new Error("Promise executor did not initialize resolve/reject");
143
+ }
144
+
145
+ return {
146
+ promise,
147
+ resolve: resolveRef,
148
+ reject: rejectRef,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Captures console output during test execution
154
+ * @param fn - Function to execute while capturing output
155
+ * @returns Object with stdout and stderr arrays
156
+ */
157
+ export async function captureConsole<T>(
158
+ fn: () => Promise<T> | T,
159
+ ): Promise<{ stdout: string[]; stderr: string[]; result: T }> {
160
+ const stdout: string[] = [];
161
+ const stderr: string[] = [];
162
+
163
+ const originalLog = console.log;
164
+ const originalError = console.error;
165
+ const originalWarn = console.warn;
166
+
167
+ console.log = (...args: unknown[]) => {
168
+ stdout.push(args.map(String).join(" "));
169
+ };
170
+
171
+ console.error = (...args: unknown[]) => {
172
+ stderr.push(args.map(String).join(" "));
173
+ };
174
+
175
+ console.warn = (...args: unknown[]) => {
176
+ stderr.push(args.map(String).join(" "));
177
+ };
178
+
179
+ try {
180
+ const result = await fn();
181
+ return { stdout, stderr, result };
182
+ } finally {
183
+ console.log = originalLog;
184
+ console.error = originalError;
185
+ console.warn = originalWarn;
186
+ }
187
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Test utilities for OmniDev
3
+ *
4
+ * This module provides shared test utilities including:
5
+ * - Mock factories for creating test data
6
+ * - Helper functions for async testing
7
+ * - Spy and mock function utilities
8
+ */
9
+
10
+ // Re-export all helper functions
11
+ export {
12
+ captureConsole,
13
+ createDeferredPromise,
14
+ createMockFn,
15
+ createSpy,
16
+ delay,
17
+ expectToThrowAsync,
18
+ waitForCondition,
19
+ } from "./helpers";
20
+ // Re-export all mock factories
21
+ export {
22
+ createMockCapability,
23
+ createMockConfig,
24
+ createMockRule,
25
+ createMockSkill,
26
+ type MockCapability,
27
+ type MockConfig,
28
+ type MockRule,
29
+ type MockSkill,
30
+ } from "./mocks";
@@ -0,0 +1,83 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createMockCapability, createMockConfig, createMockRule, createMockSkill } from "./mocks";
3
+
4
+ describe("createMockCapability", () => {
5
+ test("should create a mock capability with default values", () => {
6
+ const capability = createMockCapability();
7
+ expect(capability.id).toBe("test-capability");
8
+ expect(capability.name).toBe("Test Capability");
9
+ expect(capability.version).toBe("1.0.0");
10
+ expect(capability.enabled).toBe(true);
11
+ });
12
+
13
+ test("should allow overriding default values", () => {
14
+ const capability = createMockCapability({
15
+ id: "custom-id",
16
+ name: "Custom Name",
17
+ version: "2.0.0",
18
+ enabled: false,
19
+ });
20
+ expect(capability.id).toBe("custom-id");
21
+ expect(capability.name).toBe("Custom Name");
22
+ expect(capability.version).toBe("2.0.0");
23
+ expect(capability.enabled).toBe(false);
24
+ });
25
+ });
26
+
27
+ describe("createMockConfig", () => {
28
+ test("should create a mock config with default values", () => {
29
+ const config = createMockConfig();
30
+ expect(config.project).toBe("test-project");
31
+ expect(config.capabilities.enable).toEqual([]);
32
+ expect(config.capabilities.disable).toEqual([]);
33
+ });
34
+
35
+ test("should allow overriding default values", () => {
36
+ const config = createMockConfig({
37
+ project: "custom-project",
38
+ capabilities: {
39
+ enable: ["cap1", "cap2"],
40
+ },
41
+ });
42
+ expect(config.project).toBe("custom-project");
43
+ expect(config.capabilities.enable).toEqual(["cap1", "cap2"]);
44
+ });
45
+ });
46
+
47
+ describe("createMockSkill", () => {
48
+ test("should create a mock skill with default values", () => {
49
+ const skill = createMockSkill();
50
+ expect(skill.id).toBe("test-skill");
51
+ expect(skill.name).toBe("Test Skill");
52
+ expect(skill.description).toBe("A test skill for unit testing");
53
+ expect(skill.instructions).toBe("Test instructions");
54
+ });
55
+
56
+ test("should allow overriding default values", () => {
57
+ const skill = createMockSkill({
58
+ id: "custom-skill",
59
+ triggers: ["trigger1", "trigger2"],
60
+ });
61
+ expect(skill.id).toBe("custom-skill");
62
+ expect(skill.triggers).toEqual(["trigger1", "trigger2"]);
63
+ });
64
+ });
65
+
66
+ describe("createMockRule", () => {
67
+ test("should create a mock rule with default values", () => {
68
+ const rule = createMockRule();
69
+ expect(rule.id).toBe("test-rule");
70
+ expect(rule.name).toBe("Test Rule");
71
+ expect(rule.content).toBe("# Test Rule\n\nTest rule content");
72
+ expect(rule.priority).toBe(1);
73
+ });
74
+
75
+ test("should allow overriding default values", () => {
76
+ const rule = createMockRule({
77
+ id: "custom-rule",
78
+ priority: 10,
79
+ });
80
+ expect(rule.id).toBe("custom-rule");
81
+ expect(rule.priority).toBe(10);
82
+ });
83
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Mock factories for creating test data
3
+ */
4
+
5
+ export interface MockCapability {
6
+ id: string;
7
+ name: string;
8
+ version: string;
9
+ enabled?: boolean;
10
+ metadata?: Record<string, unknown>;
11
+ }
12
+
13
+ export interface MockConfig {
14
+ project: string;
15
+ capabilities: {
16
+ enable: string[];
17
+ disable?: string[];
18
+ };
19
+ profiles?: Record<string, unknown>;
20
+ env?: Record<string, string>;
21
+ }
22
+
23
+ export interface MockSkill {
24
+ id: string;
25
+ name: string;
26
+ description: string;
27
+ instructions: string;
28
+ triggers?: string[];
29
+ }
30
+
31
+ export interface MockRule {
32
+ id: string;
33
+ name: string;
34
+ content: string;
35
+ priority?: number;
36
+ }
37
+
38
+ /**
39
+ * Creates a mock capability with default values
40
+ * @param overrides - Partial capability object to override defaults
41
+ * @returns Mock capability object
42
+ */
43
+ export function createMockCapability(overrides: Partial<MockCapability> = {}): MockCapability {
44
+ return {
45
+ id: "test-capability",
46
+ name: "Test Capability",
47
+ version: "1.0.0",
48
+ enabled: true,
49
+ metadata: {},
50
+ ...overrides,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Creates a mock config with default values
56
+ * @param overrides - Partial config object to override defaults
57
+ * @returns Mock config object
58
+ */
59
+ export function createMockConfig(overrides: Partial<MockConfig> = {}): MockConfig {
60
+ return {
61
+ project: "test-project",
62
+ capabilities: {
63
+ enable: [],
64
+ disable: [],
65
+ },
66
+ profiles: {},
67
+ env: {},
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Creates a mock skill with default values
74
+ * @param overrides - Partial skill object to override defaults
75
+ * @returns Mock skill object
76
+ */
77
+ export function createMockSkill(overrides: Partial<MockSkill> = {}): MockSkill {
78
+ return {
79
+ id: "test-skill",
80
+ name: "Test Skill",
81
+ description: "A test skill for unit testing",
82
+ instructions: "Test instructions",
83
+ triggers: [],
84
+ ...overrides,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Creates a mock rule with default values
90
+ * @param overrides - Partial rule object to override defaults
91
+ * @returns Mock rule object
92
+ */
93
+ export function createMockRule(overrides: Partial<MockRule> = {}): MockRule {
94
+ return {
95
+ id: "test-rule",
96
+ name: "Test Rule",
97
+ content: "# Test Rule\n\nTest rule content",
98
+ priority: 1,
99
+ ...overrides,
100
+ };
101
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Capability Export Types
3
+ *
4
+ * These types define the structure that capabilities use to export their features.
5
+ * Capabilities should import these types from @omnidev-ai/core and use them in their index.ts.
6
+ */
7
+
8
+ /**
9
+ * File content structure for programmatic file creation
10
+ */
11
+ export interface FileContent {
12
+ /** File name (relative path within capability) */
13
+ name: string;
14
+
15
+ /** File content */
16
+ content: string;
17
+ }
18
+
19
+ /**
20
+ * Documentation export structure
21
+ */
22
+ export interface DocExport {
23
+ /** Document title */
24
+ title: string;
25
+
26
+ /** Markdown content */
27
+ content: string;
28
+ }
29
+
30
+ /**
31
+ * Skill export structure
32
+ */
33
+ export interface SkillExport {
34
+ /** SKILL.md content (markdown with YAML frontmatter) */
35
+ skillMd: string;
36
+
37
+ /** Optional: Reference files to create (files the skill needs access to) */
38
+ references?: FileContent[];
39
+
40
+ /** Optional: Additional files to create (templates, examples, etc.) */
41
+ additionalFiles?: FileContent[];
42
+ }
43
+
44
+ /**
45
+ * JSON Schema type for tool parameters
46
+ */
47
+ export interface JSONSchema {
48
+ type?: string | string[];
49
+ properties?: Record<string, JSONSchema>;
50
+ required?: string[];
51
+ items?: JSONSchema;
52
+ enum?: unknown[];
53
+ description?: string;
54
+ default?: unknown;
55
+ [key: string]: unknown;
56
+ }
57
+
58
+ /**
59
+ * Subagent export structure
60
+ *
61
+ * Defines a subagent that Claude can delegate tasks to.
62
+ * Uses YAML frontmatter in markdown format for configuration.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const codeReviewer: SubagentExport = {
67
+ * subagentMd: `---
68
+ * name: code-reviewer
69
+ * description: Reviews code for quality and best practices
70
+ * tools: Read, Glob, Grep
71
+ * model: sonnet
72
+ * ---
73
+ *
74
+ * You are a code reviewer. When invoked, analyze the code and provide
75
+ * specific, actionable feedback on quality, security, and best practices.`
76
+ * };
77
+ * ```
78
+ */
79
+ export interface SubagentExport {
80
+ /** SUBAGENT.md content (markdown with YAML frontmatter) */
81
+ subagentMd: string;
82
+ }
83
+
84
+ /**
85
+ * Slash command export structure
86
+ *
87
+ * Defines a slash command that can be invoked in Claude Code.
88
+ * Uses YAML frontmatter in markdown format for configuration.
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * const fixIssue: CommandExport = {
93
+ * commandMd: `---
94
+ * name: fix-issue
95
+ * description: Fix a GitHub issue following coding standards
96
+ * allowed-tools: Bash(git add:*), Bash(git commit:*)
97
+ * ---
98
+ *
99
+ * Fix issue #$ARGUMENTS following our coding standards.
100
+ *
101
+ * 1. Read the issue details
102
+ * 2. Implement the fix
103
+ * 3. Write tests
104
+ * 4. Create a commit`
105
+ * };
106
+ * ```
107
+ */
108
+ export interface CommandExport {
109
+ /** COMMAND.md content (markdown with YAML frontmatter) */
110
+ commandMd: string;
111
+ }
112
+
113
+ /**
114
+ * Sandbox tool export structure
115
+ *
116
+ * Defines a tool that can be called from sandbox code via omni_execute.
117
+ * Full schema is required for proper introspection and type generation.
118
+ *
119
+ * For MCP wrapper capabilities, these are auto-discovered from the child MCP.
120
+ * For custom capabilities, these must be explicitly defined with full schemas.
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * const createTask: SandboxToolExport = {
125
+ * name: "createTask",
126
+ * description: "Create a new task",
127
+ * inputSchema: {
128
+ * type: "object",
129
+ * properties: {
130
+ * title: { type: "string", description: "Task title" },
131
+ * priority: { type: "string", enum: ["low", "medium", "high"] }
132
+ * },
133
+ * required: ["title"]
134
+ * },
135
+ * outputSchema: {
136
+ * type: "object",
137
+ * properties: {
138
+ * id: { type: "string" },
139
+ * title: { type: "string" },
140
+ * createdAt: { type: "string" }
141
+ * }
142
+ * },
143
+ * specification: `/**
144
+ * * Create a new task in the task management system.
145
+ * * @param input.title - The title of the task (required)
146
+ * * @param input.priority - Priority level (default: "medium")
147
+ * * @returns The created task object with generated ID
148
+ * *\/`
149
+ * };
150
+ * ```
151
+ */
152
+ export interface SandboxToolExport {
153
+ /** Tool name (used as function name in sandbox) */
154
+ name: string;
155
+
156
+ /** Short description for overview listings */
157
+ description: string;
158
+
159
+ /** JSON Schema for input parameters (required for proper introspection) */
160
+ inputSchema: JSONSchema;
161
+
162
+ /** JSON Schema for output/return value */
163
+ outputSchema?: JSONSchema;
164
+
165
+ /**
166
+ * Full specification/documentation for the tool.
167
+ * Can include JSDoc, examples, detailed behavior notes.
168
+ * This is shown when requesting full details for a specific tool.
169
+ */
170
+ specification?: string;
171
+ }
172
+
173
+ /**
174
+ * Complete capability export structure
175
+ *
176
+ * Capabilities export this as their default export from index.ts.
177
+ * All content fields are OPTIONAL and PROGRAMMATIC.
178
+ * Capabilities can also provide content via static files in their directory.
179
+ * Both approaches are supported and will be merged during sync.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * // Static files approach - just export CLI commands
184
+ * export default {
185
+ * cliCommands: { mycap: myRoutes },
186
+ * gitignore: ["mycap/"],
187
+ * sync
188
+ * } satisfies CapabilityExport;
189
+ * ```
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * // Programmatic approach - generate content dynamically
194
+ * export default {
195
+ * cliCommands: { mycap: myRoutes },
196
+ * docs: [{ title: "Guide", content: "# Guide\n..." }],
197
+ * rules: ["# Rule content..."],
198
+ * skills: [{ skillMd: "...", references: [...] }],
199
+ * gitignore: ["mycap/"],
200
+ * sync
201
+ * } satisfies CapabilityExport;
202
+ * ```
203
+ */
204
+ export interface CapabilityExport {
205
+ /** CLI commands provided by this capability */
206
+ cliCommands?: Record<string, unknown>; // stricli Command type
207
+
208
+ /** Sandbox tools provided by this capability (callable from omni_execute) */
209
+ sandboxTools?: Record<string, SandboxToolExport>;
210
+
211
+ /** Documentation (programmatic - optional, can also use docs/ directory) */
212
+ docs?: DocExport[];
213
+
214
+ /** Rules (programmatic - optional, can also use rules/ directory) */
215
+ rules?: string[]; // Array of markdown content strings
216
+
217
+ /** Skills (programmatic - optional, can also use skills/ directory) */
218
+ skills?: SkillExport[];
219
+
220
+ /** Subagents (programmatic - optional, can also use subagents/ directory) */
221
+ subagents?: SubagentExport[];
222
+
223
+ /** Commands (programmatic - optional, can also use commands/ directory) */
224
+ commands?: CommandExport[];
225
+
226
+ /** Gitignore patterns */
227
+ gitignore?: string[];
228
+
229
+ /** Custom sync hook function */
230
+ sync?: () => Promise<void>;
231
+
232
+ /** Additional exports for extensibility */
233
+ [key: string]: unknown;
234
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ProviderConfig } from "./index.js";
3
+ import { getActiveProviders } from "./index.js";
4
+
5
+ describe("getActiveProviders", () => {
6
+ test("returns providers array when present", () => {
7
+ const config: ProviderConfig = { providers: ["claude", "codex"] };
8
+ expect(getActiveProviders(config)).toEqual(["claude", "codex"]);
9
+ });
10
+
11
+ test("returns single provider as array when present", () => {
12
+ const config: ProviderConfig = { provider: "claude" };
13
+ expect(getActiveProviders(config)).toEqual(["claude"]);
14
+ });
15
+
16
+ test("prefers providers array over single provider", () => {
17
+ const config: ProviderConfig = {
18
+ provider: "claude",
19
+ providers: ["codex"],
20
+ };
21
+ expect(getActiveProviders(config)).toEqual(["codex"]);
22
+ });
23
+
24
+ test("returns claude as default when no provider specified", () => {
25
+ const config: ProviderConfig = {};
26
+ expect(getActiveProviders(config)).toEqual(["claude"]);
27
+ });
28
+ });