@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.
- package/package.json +31 -0
- package/src/capability/AGENTS.md +58 -0
- package/src/capability/commands.test.ts +414 -0
- package/src/capability/commands.ts +70 -0
- package/src/capability/docs.test.ts +199 -0
- package/src/capability/docs.ts +46 -0
- package/src/capability/index.ts +20 -0
- package/src/capability/loader.test.ts +815 -0
- package/src/capability/loader.ts +492 -0
- package/src/capability/registry.test.ts +473 -0
- package/src/capability/registry.ts +55 -0
- package/src/capability/rules.test.ts +145 -0
- package/src/capability/rules.ts +133 -0
- package/src/capability/skills.test.ts +316 -0
- package/src/capability/skills.ts +56 -0
- package/src/capability/sources.test.ts +338 -0
- package/src/capability/sources.ts +966 -0
- package/src/capability/subagents.test.ts +478 -0
- package/src/capability/subagents.ts +103 -0
- package/src/capability/yaml-parser.ts +81 -0
- package/src/config/AGENTS.md +46 -0
- package/src/config/capabilities.ts +82 -0
- package/src/config/env.test.ts +286 -0
- package/src/config/env.ts +96 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +282 -0
- package/src/config/loader.ts +137 -0
- package/src/config/parser.test.ts +281 -0
- package/src/config/parser.ts +55 -0
- package/src/config/profiles.test.ts +259 -0
- package/src/config/profiles.ts +75 -0
- package/src/config/provider.test.ts +79 -0
- package/src/config/provider.ts +55 -0
- package/src/debug.ts +20 -0
- package/src/gitignore/manager.test.ts +219 -0
- package/src/gitignore/manager.ts +167 -0
- package/src/index.test.ts +26 -0
- package/src/index.ts +39 -0
- package/src/mcp-json/index.ts +1 -0
- package/src/mcp-json/manager.test.ts +415 -0
- package/src/mcp-json/manager.ts +118 -0
- package/src/state/active-profile.test.ts +131 -0
- package/src/state/active-profile.ts +41 -0
- package/src/state/index.ts +2 -0
- package/src/state/manifest.test.ts +548 -0
- package/src/state/manifest.ts +164 -0
- package/src/sync.ts +213 -0
- package/src/templates/agents.test.ts +23 -0
- package/src/templates/agents.ts +14 -0
- package/src/templates/claude.test.ts +48 -0
- package/src/templates/claude.ts +122 -0
- package/src/test-utils/helpers.test.ts +196 -0
- package/src/test-utils/helpers.ts +187 -0
- package/src/test-utils/index.ts +30 -0
- package/src/test-utils/mocks.test.ts +83 -0
- package/src/test-utils/mocks.ts +101 -0
- package/src/types/capability-export.ts +234 -0
- package/src/types/index.test.ts +28 -0
- package/src/types/index.ts +270 -0
package/src/sync.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { buildCapabilityRegistry } from "./capability/registry";
|
|
5
|
+
import { writeRules } from "./capability/rules";
|
|
6
|
+
import { fetchAllCapabilitySources } from "./capability/sources";
|
|
7
|
+
import { loadConfig } from "./config/loader";
|
|
8
|
+
import { rebuildGitignore } from "./gitignore/manager";
|
|
9
|
+
import { syncMcpJson } from "./mcp-json/manager";
|
|
10
|
+
import {
|
|
11
|
+
buildManifestFromCapabilities,
|
|
12
|
+
cleanupStaleResources,
|
|
13
|
+
loadManifest,
|
|
14
|
+
saveManifest,
|
|
15
|
+
} from "./state/manifest";
|
|
16
|
+
|
|
17
|
+
export interface SyncResult {
|
|
18
|
+
capabilities: string[];
|
|
19
|
+
skillCount: number;
|
|
20
|
+
ruleCount: number;
|
|
21
|
+
docCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Install dependencies for capabilities in .omni/capabilities/
|
|
26
|
+
* Only installs for capabilities that have a package.json
|
|
27
|
+
*/
|
|
28
|
+
export async function installCapabilityDependencies(silent: boolean): Promise<void> {
|
|
29
|
+
const { existsSync, readdirSync } = await import("node:fs");
|
|
30
|
+
const { join } = await import("node:path");
|
|
31
|
+
|
|
32
|
+
const capabilitiesDir = ".omni/capabilities";
|
|
33
|
+
|
|
34
|
+
// Check if .omni/capabilities exists
|
|
35
|
+
if (!existsSync(capabilitiesDir)) {
|
|
36
|
+
return; // Nothing to install
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const entries = readdirSync(capabilitiesDir, { withFileTypes: true });
|
|
40
|
+
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (!entry.isDirectory()) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const capabilityPath = join(capabilitiesDir, entry.name);
|
|
47
|
+
const packageJsonPath = join(capabilityPath, "package.json");
|
|
48
|
+
|
|
49
|
+
// Skip if no package.json
|
|
50
|
+
if (!existsSync(packageJsonPath)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!silent) {
|
|
55
|
+
console.log(`Installing dependencies for ${capabilityPath}...`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Run bun install in the capability directory
|
|
59
|
+
await new Promise<void>((resolve, reject) => {
|
|
60
|
+
const proc = spawn("bun", ["install"], {
|
|
61
|
+
cwd: capabilityPath,
|
|
62
|
+
stdio: silent ? "ignore" : "inherit",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
proc.on("close", (code) => {
|
|
66
|
+
if (code === 0) {
|
|
67
|
+
resolve();
|
|
68
|
+
} else {
|
|
69
|
+
reject(new Error(`Failed to install dependencies for ${capabilityPath}`));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
proc.on("error", (error) => {
|
|
74
|
+
reject(error);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Central sync function that regenerates all agent configuration files
|
|
82
|
+
* Called automatically after any config change (init, capability enable/disable, profile change)
|
|
83
|
+
*/
|
|
84
|
+
export async function syncAgentConfiguration(options?: { silent?: boolean }): Promise<SyncResult> {
|
|
85
|
+
const silent = options?.silent ?? false;
|
|
86
|
+
|
|
87
|
+
if (!silent) {
|
|
88
|
+
console.log("Syncing agent configuration...");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fetch capability sources from git repos FIRST (before discovery)
|
|
92
|
+
const config = await loadConfig();
|
|
93
|
+
await fetchAllCapabilitySources(config, { silent });
|
|
94
|
+
|
|
95
|
+
// Install capability dependencies before building registry
|
|
96
|
+
await installCapabilityDependencies(silent);
|
|
97
|
+
|
|
98
|
+
// Build registry
|
|
99
|
+
const registry = await buildCapabilityRegistry();
|
|
100
|
+
const capabilities = registry.getAllCapabilities();
|
|
101
|
+
const skills = registry.getAllSkills();
|
|
102
|
+
const rules = registry.getAllRules();
|
|
103
|
+
const docs = registry.getAllDocs();
|
|
104
|
+
|
|
105
|
+
// Load previous manifest and cleanup stale resources from disabled capabilities
|
|
106
|
+
const previousManifest = await loadManifest();
|
|
107
|
+
const currentCapabilityIds = new Set(capabilities.map((c) => c.id));
|
|
108
|
+
|
|
109
|
+
const cleanupResult = await cleanupStaleResources(previousManifest, currentCapabilityIds);
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
!silent &&
|
|
113
|
+
(cleanupResult.deletedSkills.length > 0 || cleanupResult.deletedRules.length > 0)
|
|
114
|
+
) {
|
|
115
|
+
console.log("Cleaned up stale resources:");
|
|
116
|
+
if (cleanupResult.deletedSkills.length > 0) {
|
|
117
|
+
console.log(
|
|
118
|
+
` - Removed ${cleanupResult.deletedSkills.length} skill(s): ${cleanupResult.deletedSkills.join(", ")}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (cleanupResult.deletedRules.length > 0) {
|
|
122
|
+
console.log(
|
|
123
|
+
` - Removed ${cleanupResult.deletedRules.length} rule(s): ${cleanupResult.deletedRules.join(", ")}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Rebuild .omni/.gitignore with all enabled capability patterns
|
|
129
|
+
const gitignorePatterns = new Map<string, string[]>();
|
|
130
|
+
for (const capability of capabilities) {
|
|
131
|
+
if (capability.gitignore && capability.gitignore.length > 0) {
|
|
132
|
+
gitignorePatterns.set(capability.id, capability.gitignore);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
await rebuildGitignore(gitignorePatterns);
|
|
136
|
+
|
|
137
|
+
// Call sync hooks for capabilities that have them
|
|
138
|
+
for (const capability of capabilities) {
|
|
139
|
+
// Check for structured export sync function first (new approach)
|
|
140
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic module exports need runtime type checking
|
|
141
|
+
const defaultExport = (capability.exports as any).default;
|
|
142
|
+
if (defaultExport && typeof defaultExport.sync === "function") {
|
|
143
|
+
try {
|
|
144
|
+
await defaultExport.sync();
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(`Error running sync hook for ${capability.id}:`, error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Fall back to TOML-based sync hook (legacy approach)
|
|
150
|
+
else if (capability.config.sync?.on_sync) {
|
|
151
|
+
const syncFnName = capability.config.sync.on_sync;
|
|
152
|
+
const syncFn = capability.exports[syncFnName];
|
|
153
|
+
|
|
154
|
+
if (typeof syncFn === "function") {
|
|
155
|
+
try {
|
|
156
|
+
await syncFn();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(`Error running sync hook for ${capability.id}:`, error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Ensure directories exist
|
|
165
|
+
mkdirSync(".claude/skills", { recursive: true });
|
|
166
|
+
mkdirSync(".cursor/rules", { recursive: true });
|
|
167
|
+
|
|
168
|
+
// Write rules and docs to .omni/instructions.md
|
|
169
|
+
await writeRules(rules, docs);
|
|
170
|
+
|
|
171
|
+
// Write skills to .claude/skills/
|
|
172
|
+
for (const skill of skills) {
|
|
173
|
+
const skillDir = `.claude/skills/${skill.name}`;
|
|
174
|
+
mkdirSync(skillDir, { recursive: true });
|
|
175
|
+
await Bun.write(
|
|
176
|
+
join(skillDir, "SKILL.md"),
|
|
177
|
+
`---
|
|
178
|
+
name: ${skill.name}
|
|
179
|
+
description: "${skill.description}"
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
${skill.instructions}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Write rules to .cursor/rules/
|
|
187
|
+
for (const rule of rules) {
|
|
188
|
+
await Bun.write(`.cursor/rules/omnidev-${rule.name}.mdc`, rule.content);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Save updated manifest for future cleanup
|
|
192
|
+
const newManifest = buildManifestFromCapabilities(capabilities);
|
|
193
|
+
await saveManifest(newManifest);
|
|
194
|
+
|
|
195
|
+
// Sync .mcp.json based on sandbox mode
|
|
196
|
+
const sandboxEnabled = config.sandbox_enabled !== false;
|
|
197
|
+
await syncMcpJson(capabilities, sandboxEnabled, { silent });
|
|
198
|
+
|
|
199
|
+
if (!silent) {
|
|
200
|
+
console.log("✓ Synced:");
|
|
201
|
+
console.log(" - .omni/.gitignore (capability patterns)");
|
|
202
|
+
console.log(` - .omni/instructions.md (${docs.length} docs, ${rules.length} rules)`);
|
|
203
|
+
console.log(` - .claude/skills/ (${skills.length} skills)`);
|
|
204
|
+
console.log(` - .cursor/rules/ (${rules.length} rules)`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
capabilities: capabilities.map((c) => c.id),
|
|
209
|
+
skillCount: skills.length,
|
|
210
|
+
ruleCount: rules.length,
|
|
211
|
+
docCount: docs.length,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { generateAgentsTemplate } from "./agents";
|
|
3
|
+
|
|
4
|
+
describe("generateAgentsTemplate", () => {
|
|
5
|
+
test("generates AGENTS.md template with reference to instructions", () => {
|
|
6
|
+
const template = generateAgentsTemplate();
|
|
7
|
+
|
|
8
|
+
expect(template).toContain("# Project Instructions");
|
|
9
|
+
expect(template).toContain("@import .omni/instructions.md");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("includes placeholder for project-specific instructions", () => {
|
|
13
|
+
const template = generateAgentsTemplate();
|
|
14
|
+
|
|
15
|
+
expect(template).toContain("<!-- Add your project-specific instructions here -->");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("includes OmniDev section", () => {
|
|
19
|
+
const template = generateAgentsTemplate();
|
|
20
|
+
|
|
21
|
+
expect(template).toContain("## OmniDev");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template for AGENTS.md (Codex provider)
|
|
3
|
+
* Creates a minimal file with reference to OmniDev instructions
|
|
4
|
+
*/
|
|
5
|
+
export function generateAgentsTemplate(): string {
|
|
6
|
+
return `# Project Instructions
|
|
7
|
+
|
|
8
|
+
<!-- Add your project-specific instructions here -->
|
|
9
|
+
|
|
10
|
+
## OmniDev
|
|
11
|
+
|
|
12
|
+
@import .omni/instructions.md
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { generateClaudeTemplate, generateInstructionsTemplate } from "./claude";
|
|
3
|
+
|
|
4
|
+
describe("generateClaudeTemplate", () => {
|
|
5
|
+
test("generates CLAUDE.md template with reference to instructions", () => {
|
|
6
|
+
const template = generateClaudeTemplate();
|
|
7
|
+
|
|
8
|
+
expect(template).toContain("# Project Instructions");
|
|
9
|
+
expect(template).toContain("@import .omni/instructions.md");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("includes placeholder for project-specific instructions", () => {
|
|
13
|
+
const template = generateClaudeTemplate();
|
|
14
|
+
|
|
15
|
+
expect(template).toContain("<!-- Add your project-specific instructions here -->");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("includes OmniDev section", () => {
|
|
19
|
+
const template = generateClaudeTemplate();
|
|
20
|
+
|
|
21
|
+
expect(template).toContain("## OmniDev");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("generateInstructionsTemplate", () => {
|
|
26
|
+
test("generates instructions with project description placeholder", () => {
|
|
27
|
+
const template = generateInstructionsTemplate();
|
|
28
|
+
|
|
29
|
+
expect(template).toContain("# OmniDev Instructions");
|
|
30
|
+
expect(template).toContain("## Project Description");
|
|
31
|
+
expect(template).toContain("<!-- TODO: Add 2-3 sentences describing your project -->");
|
|
32
|
+
expect(template).toContain("[Describe what this project does and its main purpose]");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("includes capabilities section with placeholder", () => {
|
|
36
|
+
const template = generateInstructionsTemplate();
|
|
37
|
+
|
|
38
|
+
expect(template).toContain("## Capabilities");
|
|
39
|
+
expect(template).toContain("No capabilities enabled yet");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("includes BEGIN/END markers for generated content", () => {
|
|
43
|
+
const template = generateInstructionsTemplate();
|
|
44
|
+
|
|
45
|
+
expect(template).toContain("BEGIN OMNIDEV GENERATED CONTENT");
|
|
46
|
+
expect(template).toContain("END OMNIDEV GENERATED CONTENT");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template for CLAUDE.md (Claude provider)
|
|
3
|
+
* Creates a minimal file with reference to OmniDev instructions
|
|
4
|
+
*/
|
|
5
|
+
export function generateClaudeTemplate(): string {
|
|
6
|
+
return `# Project Instructions
|
|
7
|
+
|
|
8
|
+
<!-- Add your project-specific instructions here -->
|
|
9
|
+
|
|
10
|
+
## OmniDev
|
|
11
|
+
|
|
12
|
+
@import .omni/instructions.md
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Template for .omni/instructions.md
|
|
18
|
+
* Contains OmniDev-specific instructions and capability rules
|
|
19
|
+
*/
|
|
20
|
+
export function generateInstructionsTemplate(): string {
|
|
21
|
+
return `# OmniDev Instructions
|
|
22
|
+
|
|
23
|
+
## Project Description
|
|
24
|
+
<!-- TODO: Add 2-3 sentences describing your project -->
|
|
25
|
+
[Describe what this project does and its main purpose]
|
|
26
|
+
|
|
27
|
+
## How OmniDev Works
|
|
28
|
+
|
|
29
|
+
OmniDev provides **three MCP tools** that give you programmatic access to capabilities:
|
|
30
|
+
|
|
31
|
+
### \`omni_query\` - Search and Discovery
|
|
32
|
+
|
|
33
|
+
Search across enabled capabilities, documentation, skills, and rules.
|
|
34
|
+
|
|
35
|
+
\`\`\`json
|
|
36
|
+
{
|
|
37
|
+
"query": "search query" // Empty query returns summary of enabled capabilities
|
|
38
|
+
}
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
Returns short snippets with source tags. Use for finding relevant capabilities and documentation.
|
|
42
|
+
|
|
43
|
+
### \`omni_sandbox_environment\` - Tool Introspection
|
|
44
|
+
|
|
45
|
+
Discover available sandbox tools with three levels of detail:
|
|
46
|
+
|
|
47
|
+
\`\`\`json
|
|
48
|
+
// Level 1: Overview of all modules
|
|
49
|
+
{}
|
|
50
|
+
|
|
51
|
+
// Level 2: Module details with schemas
|
|
52
|
+
{ "capability": "my-capability" }
|
|
53
|
+
|
|
54
|
+
// Level 3: Full tool specification
|
|
55
|
+
{ "capability": "my-capability", "tool": "myTool" }
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
Use this to discover what tools are available and how to call them.
|
|
59
|
+
|
|
60
|
+
### \`omni_execute\` - Programmatic Execution
|
|
61
|
+
|
|
62
|
+
Execute TypeScript code in a sandboxed environment with access to all enabled capabilities.
|
|
63
|
+
|
|
64
|
+
\`\`\`json
|
|
65
|
+
{
|
|
66
|
+
"code": "full contents of main.ts file"
|
|
67
|
+
}
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
Write complete TypeScript programs that import capability modules:
|
|
71
|
+
|
|
72
|
+
\`\`\`typescript
|
|
73
|
+
import * as myCapability from 'my-capability';
|
|
74
|
+
import * as fs from 'fs';
|
|
75
|
+
|
|
76
|
+
export async function main(): Promise<number> {
|
|
77
|
+
// Your code here
|
|
78
|
+
console.log('Hello from OmniDev sandbox!');
|
|
79
|
+
|
|
80
|
+
return 0; // Success
|
|
81
|
+
}
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
**Response includes:**
|
|
85
|
+
- \`stdout\` - Standard output
|
|
86
|
+
- \`stderr\` - Standard error
|
|
87
|
+
- \`exit_code\` - Exit code (0 = success)
|
|
88
|
+
- \`changed_files\` - List of files modified
|
|
89
|
+
- \`diff_stat\` - Summary of changes
|
|
90
|
+
|
|
91
|
+
## The Sandbox
|
|
92
|
+
|
|
93
|
+
The sandbox is a TypeScript execution environment located in \`.omni/sandbox/\`. It provides:
|
|
94
|
+
|
|
95
|
+
1. **Isolated Execution** - Code runs in a controlled environment
|
|
96
|
+
2. **Capability Access** - Import enabled capabilities as TypeScript modules
|
|
97
|
+
3. **File System Access** - Read and write files in the project
|
|
98
|
+
4. **Type Safety** - Full TypeScript support with IntelliSense
|
|
99
|
+
|
|
100
|
+
**Key features:**
|
|
101
|
+
- Runs using Bun runtime for speed
|
|
102
|
+
- Auto-generated module wrappers for MCP capabilities
|
|
103
|
+
- Direct symlinks to native capability code
|
|
104
|
+
- All capability tools are available as typed functions
|
|
105
|
+
|
|
106
|
+
**Example workflow:**
|
|
107
|
+
1. Use \`omni_query\` (empty query) to see what capabilities are enabled
|
|
108
|
+
2. Use \`omni_sandbox_environment\` to discover available tools and their schemas
|
|
109
|
+
3. Use \`omni_sandbox_environment\` with capability + tool params for detailed specs
|
|
110
|
+
4. Write TypeScript code that imports and uses capability tools
|
|
111
|
+
5. Execute with \`omni_execute\`
|
|
112
|
+
|
|
113
|
+
<!-- BEGIN OMNIDEV GENERATED CONTENT - DO NOT EDIT BELOW THIS LINE -->
|
|
114
|
+
<!-- This section is automatically updated by 'omnidev agents sync' -->
|
|
115
|
+
|
|
116
|
+
## Capabilities
|
|
117
|
+
|
|
118
|
+
No capabilities enabled yet. Run \`omnidev capability enable <name>\` to enable capabilities.
|
|
119
|
+
|
|
120
|
+
<!-- END OMNIDEV GENERATED CONTENT -->
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
captureConsole,
|
|
4
|
+
createDeferredPromise,
|
|
5
|
+
createMockFn,
|
|
6
|
+
createSpy,
|
|
7
|
+
delay,
|
|
8
|
+
expectToThrowAsync,
|
|
9
|
+
waitForCondition,
|
|
10
|
+
} from "./helpers";
|
|
11
|
+
|
|
12
|
+
describe("expectToThrowAsync", () => {
|
|
13
|
+
test("should pass when function throws", async () => {
|
|
14
|
+
await expectToThrowAsync(async () => {
|
|
15
|
+
throw new Error("Test error");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("should match error message with string", async () => {
|
|
20
|
+
await expectToThrowAsync(async () => {
|
|
21
|
+
throw new Error("Test error message");
|
|
22
|
+
}, "Test error");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("should match error message with regex", async () => {
|
|
26
|
+
await expectToThrowAsync(async () => {
|
|
27
|
+
throw new Error("Test error message");
|
|
28
|
+
}, /error/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("should fail when function does not throw", async () => {
|
|
32
|
+
let failed = false;
|
|
33
|
+
try {
|
|
34
|
+
await expectToThrowAsync(async () => {
|
|
35
|
+
// Does not throw
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
failed = true;
|
|
39
|
+
}
|
|
40
|
+
expect(failed).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("waitForCondition", () => {
|
|
45
|
+
test("should resolve when condition is met immediately", async () => {
|
|
46
|
+
await waitForCondition(() => true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("should resolve when condition becomes true", async () => {
|
|
50
|
+
let counter = 0;
|
|
51
|
+
await waitForCondition(() => {
|
|
52
|
+
counter++;
|
|
53
|
+
return counter > 2;
|
|
54
|
+
});
|
|
55
|
+
expect(counter).toBeGreaterThan(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("should timeout when condition is never met", async () => {
|
|
59
|
+
await expectToThrowAsync(async () => {
|
|
60
|
+
await waitForCondition(() => false, 100);
|
|
61
|
+
}, "Condition not met within 100ms");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("delay", () => {
|
|
66
|
+
test("should delay execution", async () => {
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
await delay(50);
|
|
69
|
+
const elapsed = Date.now() - start;
|
|
70
|
+
expect(elapsed).toBeGreaterThanOrEqual(45); // Small tolerance
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("createSpy", () => {
|
|
75
|
+
test("should track function calls", () => {
|
|
76
|
+
const spy = createSpy<[number, string], void>();
|
|
77
|
+
spy(1, "test");
|
|
78
|
+
spy(2, "test2");
|
|
79
|
+
|
|
80
|
+
expect(spy.callCount).toBe(2);
|
|
81
|
+
expect(spy.calls).toEqual([
|
|
82
|
+
[1, "test"],
|
|
83
|
+
[2, "test2"],
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("should support custom implementation", () => {
|
|
88
|
+
const spy = createSpy((a: number, b: number) => a + b);
|
|
89
|
+
const result = spy(1, 2);
|
|
90
|
+
|
|
91
|
+
expect(result).toBe(3);
|
|
92
|
+
expect(spy.callCount).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("should support reset", () => {
|
|
96
|
+
const spy = createSpy<[number], void>();
|
|
97
|
+
spy(1);
|
|
98
|
+
spy(2);
|
|
99
|
+
expect(spy.callCount).toBe(2);
|
|
100
|
+
|
|
101
|
+
spy.reset();
|
|
102
|
+
expect(spy.callCount).toBe(0);
|
|
103
|
+
expect(spy.calls).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("createMockFn", () => {
|
|
108
|
+
test("should return values in order", () => {
|
|
109
|
+
const mock = createMockFn("first", "second", "third");
|
|
110
|
+
expect(mock()).toBe("first");
|
|
111
|
+
expect(mock()).toBe("second");
|
|
112
|
+
expect(mock()).toBe("third");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("should throw when called more times than values", () => {
|
|
116
|
+
const mock = createMockFn("only");
|
|
117
|
+
mock();
|
|
118
|
+
expect(() => mock()).toThrow("Mock function called more times than return values provided");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("createDeferredPromise", () => {
|
|
123
|
+
test("should allow manual resolution", async () => {
|
|
124
|
+
const deferred = createDeferredPromise<string>();
|
|
125
|
+
deferred.resolve("test");
|
|
126
|
+
const result = await deferred.promise;
|
|
127
|
+
expect(result).toBe("test");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("should allow manual rejection", async () => {
|
|
131
|
+
const deferred = createDeferredPromise<string>();
|
|
132
|
+
deferred.reject(new Error("test error"));
|
|
133
|
+
await expectToThrowAsync(async () => {
|
|
134
|
+
await deferred.promise;
|
|
135
|
+
}, "test error");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("captureConsole", () => {
|
|
140
|
+
test("should capture console.log output", async () => {
|
|
141
|
+
const { stdout, result } = await captureConsole(() => {
|
|
142
|
+
console.log("test message");
|
|
143
|
+
return "return value";
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(stdout).toEqual(["test message"]);
|
|
147
|
+
expect(result).toBe("return value");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("should capture console.error output", async () => {
|
|
151
|
+
const { stderr } = await captureConsole(() => {
|
|
152
|
+
console.error("error message");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(stderr).toEqual(["error message"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("should capture console.warn output", async () => {
|
|
159
|
+
const { stderr } = await captureConsole(() => {
|
|
160
|
+
console.warn("warning message");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(stderr).toEqual(["warning message"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("should work with async functions", async () => {
|
|
167
|
+
const { stdout, result } = await captureConsole(async () => {
|
|
168
|
+
await delay(10);
|
|
169
|
+
console.log("async message");
|
|
170
|
+
return 42;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(stdout).toEqual(["async message"]);
|
|
174
|
+
expect(result).toBe(42);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("should restore console after execution", async () => {
|
|
178
|
+
const originalLog = console.log;
|
|
179
|
+
await captureConsole(() => {
|
|
180
|
+
console.log("test");
|
|
181
|
+
});
|
|
182
|
+
expect(console.log).toBe(originalLog);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("should restore console even if function throws", async () => {
|
|
186
|
+
const originalLog = console.log;
|
|
187
|
+
try {
|
|
188
|
+
await captureConsole(() => {
|
|
189
|
+
throw new Error("test");
|
|
190
|
+
});
|
|
191
|
+
} catch {
|
|
192
|
+
// Expected
|
|
193
|
+
}
|
|
194
|
+
expect(console.log).toBe(originalLog);
|
|
195
|
+
});
|
|
196
|
+
});
|