Skip to content

Commit 9a8befa

Browse files
test(devin-adapter): add comprehensive tests for Devin Desktop adapter
Add test coverage for the Devin Desktop adapter including: - Command reference transformation from colon to hyphen syntax - YAML frontmatter escaping for special characters and implicit scalars - File path generation for workflows - Integration with available tools detection - Init and update command workflows
1 parent fd974e1 commit 9a8befa

6 files changed

Lines changed: 144 additions & 2 deletions

File tree

src/core/command-generation/adapters/devin.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import path from 'path';
9+
import { transformToHyphenCommands } from '../../../utils/command-references.js';
910
import type { CommandContent, ToolCommandAdapter } from '../types.js';
1011

1112
/**
@@ -41,6 +42,8 @@ function formatTagsArray(tags: string[]): string {
4142
* Devin Desktop adapter for command generation.
4243
* File path: .devin/workflows/opsx-<id>.md
4344
* Frontmatter: name, description, category, tags
45+
*
46+
* Devin Desktop uses slash-hyphen syntax (/opsx-apply) instead of colon syntax (/opsx:apply).
4447
*/
4548
export const devinAdapter: ToolCommandAdapter = {
4649
toolId: 'devin',
@@ -50,14 +53,17 @@ export const devinAdapter: ToolCommandAdapter = {
5053
},
5154

5255
formatFile(content: CommandContent): string {
56+
// Transform command references from colon to hyphen syntax
57+
const transformedBody = transformToHyphenCommands(content.body);
58+
5359
return `---
5460
name: ${escapeYamlValue(content.name)}
5561
description: ${escapeYamlValue(content.description)}
5662
category: ${escapeYamlValue(content.category)}
5763
tags: ${formatTagsArray(content.tags)}
5864
---
5965
60-
${content.body}
66+
${transformedBody}
6167
`;
6268
},
6369
};

test/core/available-tools.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ describe('available-tools', () => {
4646
expect(tools).toHaveLength(3);
4747
});
4848

49+
it('should detect Devin Desktop when .devin directory exists', async () => {
50+
await fs.mkdir(path.join(testDir, '.devin'), { recursive: true });
51+
52+
const tools = getAvailableTools(testDir);
53+
const toolValues = tools.map((t) => t.value);
54+
expect(toolValues).toContain('devin');
55+
56+
const devinTool = tools.find((t) => t.value === 'devin');
57+
expect(devinTool).toBeDefined();
58+
expect(devinTool?.name).toBe('Devin Desktop');
59+
expect(devinTool?.skillsDir).toBe('.devin');
60+
});
61+
4962
it('should ignore files that are not directories', async () => {
5063
// Create a file named .claude instead of a directory
5164
await fs.writeFile(path.join(testDir, '.claude'), 'not a directory');

test/core/command-generation/adapters.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { continueAdapter } from '../../../src/core/command-generation/adapters/c
1313
import { costrictAdapter } from '../../../src/core/command-generation/adapters/costrict.js';
1414
import { crushAdapter } from '../../../src/core/command-generation/adapters/crush.js';
1515
import { cursorAdapter } from '../../../src/core/command-generation/adapters/cursor.js';
16+
import { devinAdapter } from '../../../src/core/command-generation/adapters/devin.js';
1617
import { factoryAdapter } from '../../../src/core/command-generation/adapters/factory.js';
1718
import { geminiAdapter } from '../../../src/core/command-generation/adapters/gemini.js';
1819
import { githubCopilotAdapter } from '../../../src/core/command-generation/adapters/github-copilot.js';
@@ -126,6 +127,76 @@ describe('command-generation/adapters', () => {
126127
});
127128
});
128129

130+
describe('devinAdapter', () => {
131+
it('should have correct toolId', () => {
132+
expect(devinAdapter.toolId).toBe('devin');
133+
});
134+
135+
it('should generate correct file path', () => {
136+
const filePath = devinAdapter.getFilePath('explore');
137+
expect(filePath).toBe(path.join('.devin', 'workflows', 'opsx-explore.md'));
138+
});
139+
140+
it('should generate correct file paths for different commands', () => {
141+
expect(devinAdapter.getFilePath('new')).toBe(path.join('.devin', 'workflows', 'opsx-new.md'));
142+
expect(devinAdapter.getFilePath('bulk-archive')).toBe(path.join('.devin', 'workflows', 'opsx-bulk-archive.md'));
143+
});
144+
145+
it('should format file with YAML frontmatter', () => {
146+
const output = devinAdapter.formatFile(sampleContent);
147+
148+
expect(output).toContain('---\n');
149+
expect(output).toContain('name: OpenSpec Explore');
150+
expect(output).toContain('description: Enter explore mode for thinking');
151+
expect(output).toContain('category: Workflow');
152+
expect(output).toContain('tags: [workflow, explore, experimental]');
153+
expect(output).toContain('---\n\n');
154+
expect(output).toContain('This is the command body.');
155+
});
156+
157+
it('should transform colon command references to hyphen format', () => {
158+
const contentWithRefs: CommandContent = {
159+
...sampleContent,
160+
body: 'Run /opsx:apply to implement. Then use /opsx:verify.',
161+
};
162+
const output = devinAdapter.formatFile(contentWithRefs);
163+
expect(output).toContain('/opsx-apply');
164+
expect(output).toContain('/opsx-verify');
165+
expect(output).not.toContain('/opsx:apply');
166+
expect(output).not.toContain('/opsx:verify');
167+
});
168+
169+
it('should escape YAML special characters in frontmatter', () => {
170+
const contentWithSpecialChars: CommandContent = {
171+
...sampleContent,
172+
name: 'Test: Command',
173+
description: 'Fix "auth" feature',
174+
};
175+
const output = devinAdapter.formatFile(contentWithSpecialChars);
176+
expect(output).toContain('name: "Test: Command"');
177+
expect(output).toContain('description: "Fix \\"auth\\" feature"');
178+
});
179+
180+
it('should escape implicit YAML scalars in frontmatter', () => {
181+
const contentWithImplicitScalar: CommandContent = {
182+
...sampleContent,
183+
name: 'true',
184+
description: 'null',
185+
category: 'on',
186+
};
187+
const output = devinAdapter.formatFile(contentWithImplicitScalar);
188+
expect(output).toContain('name: "true"');
189+
expect(output).toContain('description: "null"');
190+
expect(output).toContain('category: "on"');
191+
});
192+
193+
it('should handle empty tags', () => {
194+
const contentNoTags: CommandContent = { ...sampleContent, tags: [] };
195+
const output = devinAdapter.formatFile(contentNoTags);
196+
expect(output).toContain('tags: []');
197+
});
198+
});
199+
129200
describe('amazonQAdapter', () => {
130201
it('should have correct toolId', () => {
131202
expect(amazonQAdapter.toolId).toBe('amazon-q');

test/core/command-generation/registry.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ describe('command-generation/registry', () => {
2121
expect(adapter?.toolId).toBe('windsurf');
2222
});
2323

24+
it('should return Devin adapter for "devin"', () => {
25+
const adapter = CommandAdapterRegistry.get('devin');
26+
expect(adapter).toBeDefined();
27+
expect(adapter?.toolId).toBe('devin');
28+
});
29+
2430
it('should return Junie adapter for "junie"', () => {
2531
const adapter = CommandAdapterRegistry.get('junie');
2632
expect(adapter).toBeDefined();
@@ -45,13 +51,14 @@ describe('command-generation/registry', () => {
4551
expect(adapters.length).toBeGreaterThanOrEqual(3); // At least Claude, Cursor, Windsurf
4652
});
4753

48-
it('should include Claude, Cursor, and Windsurf adapters', () => {
54+
it('should include Claude, Cursor, Windsurf, and Devin adapters', () => {
4955
const adapters = CommandAdapterRegistry.getAll();
5056
const toolIds = adapters.map((a) => a.toolId);
5157

5258
expect(toolIds).toContain('claude');
5359
expect(toolIds).toContain('cursor');
5460
expect(toolIds).toContain('windsurf');
61+
expect(toolIds).toContain('devin');
5562
});
5663
});
5764

@@ -60,6 +67,7 @@ describe('command-generation/registry', () => {
6067
expect(CommandAdapterRegistry.has('claude')).toBe(true);
6168
expect(CommandAdapterRegistry.has('cursor')).toBe(true);
6269
expect(CommandAdapterRegistry.has('windsurf')).toBe(true);
70+
expect(CommandAdapterRegistry.has('devin')).toBe(true);
6371
expect(CommandAdapterRegistry.has('junie')).toBe(true);
6472
});
6573

@@ -74,10 +82,12 @@ describe('command-generation/registry', () => {
7482
const claudeAdapter = CommandAdapterRegistry.get('claude');
7583
const cursorAdapter = CommandAdapterRegistry.get('cursor');
7684
const windsurfAdapter = CommandAdapterRegistry.get('windsurf');
85+
const devinAdapter = CommandAdapterRegistry.get('devin');
7786

7887
expect(claudeAdapter?.getFilePath('test')).toContain('.claude');
7988
expect(cursorAdapter?.getFilePath('test')).toContain('.cursor');
8089
expect(windsurfAdapter?.getFilePath('test')).toContain('.windsurf');
90+
expect(devinAdapter?.getFilePath('test')).toContain('.devin');
8191
});
8292

8393
it('registered adapters should have working formatFile', () => {

test/core/init.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,21 @@ describe('InitCommand', () => {
457457
expect(await fileExists(cmdFile)).toBe(true);
458458
});
459459

460+
it('should generate Devin Desktop workflows', async () => {
461+
const initCommand = new InitCommand({ tools: 'devin', force: true });
462+
await initCommand.execute(testDir);
463+
464+
const cmdFile = path.join(testDir, '.devin', 'workflows', 'opsx-explore.md');
465+
expect(await fileExists(cmdFile)).toBe(true);
466+
467+
const content = await fs.readFile(cmdFile, 'utf-8');
468+
expect(content).toContain('---');
469+
expect(content).toContain('name:');
470+
expect(content).toContain('description:');
471+
// Verify command references are transformed to hyphen syntax
472+
expect(content).not.toContain('/opsx:');
473+
});
474+
460475
it('should generate Continue prompt files', async () => {
461476
const initCommand = new InitCommand({ tools: 'continue', force: true });
462477
await initCommand.execute(testDir);

test/core/update.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,33 @@ Old instructions content
251251
}
252252
});
253253

254+
it('should update Devin Desktop workflows with hyphen command references', async () => {
255+
// Set up Devin Desktop directory with a skill to indicate it's configured
256+
const skillsDir = path.join(testDir, '.devin', 'skills');
257+
await fs.mkdir(path.join(skillsDir, 'openspec-explore'), {
258+
recursive: true,
259+
});
260+
await fs.writeFile(
261+
path.join(skillsDir, 'openspec-explore', 'SKILL.md'),
262+
'old content'
263+
);
264+
265+
await updateCommand.execute(testDir);
266+
267+
// Verify workflows were created
268+
const workflowsDir = path.join(testDir, '.devin', 'workflows');
269+
const exploreWorkflow = path.join(workflowsDir, 'opsx-explore.md');
270+
const exists = await FileSystemUtils.fileExists(exploreWorkflow);
271+
expect(exists).toBe(true);
272+
273+
const content = await fs.readFile(exploreWorkflow, 'utf-8');
274+
expect(content).toContain('---');
275+
expect(content).toContain('name:');
276+
expect(content).toContain('description:');
277+
// Verify command references are transformed to hyphen syntax
278+
expect(content).not.toContain('/opsx:');
279+
});
280+
254281
});
255282

256283
describe('multi-tool support', () => {

0 commit comments

Comments
 (0)