Skip to content

Commit 0cc7b62

Browse files
intel352claude
andcommitted
test: add 32 tests covering resources, commands, MCP, discovery, and platform
Extension Resources (9): validates webview-dist, schema, snippets, and language config are bundled. Plugin Discovery (6): go.mod regex parsing and cache logic. MCP Config (6): JSON read/write/merge and error handling. Commands (3): package.json command declarations and menu contributions. Platform Detection (3): binary download platform mapping. Visual Editor (5): content detection, partial match rejection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f151e4f commit 0cc7b62

6 files changed

Lines changed: 439 additions & 23 deletions

File tree

src/test/suite/commands.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as assert from 'assert';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
5+
/**
6+
* Validates that all commands declared in package.json exist in the
7+
* extension source. This is a static check that doesn't require
8+
* activation (which would prompt for binary downloads in CI).
9+
*/
10+
suite('Commands', () => {
11+
const extensionRoot = path.resolve(__dirname, '..', '..', '..');
12+
13+
test('all declared commands have matching registrations in extension.ts', () => {
14+
const packageJson = JSON.parse(
15+
fs.readFileSync(path.join(extensionRoot, 'package.json'), 'utf-8'),
16+
);
17+
const declaredCommands: string[] = packageJson.contributes.commands.map(
18+
(c: { command: string }) => c.command,
19+
);
20+
21+
assert.ok(declaredCommands.length > 0, 'Must have declared commands');
22+
23+
// Read the compiled extension source to verify commands are registered
24+
const extensionSrc = fs.readFileSync(
25+
path.join(extensionRoot, 'out', 'extension.js'),
26+
'utf-8',
27+
);
28+
const commandsSrc = fs.readFileSync(
29+
path.join(extensionRoot, 'out', 'commands.js'),
30+
'utf-8',
31+
);
32+
const allSrc = extensionSrc + commandsSrc;
33+
34+
for (const cmd of declaredCommands) {
35+
assert.ok(
36+
allSrc.includes(cmd),
37+
`Command ${cmd} declared in package.json but not found in compiled source`,
38+
);
39+
}
40+
});
41+
42+
test('package.json commands have title and command fields', () => {
43+
const packageJson = JSON.parse(
44+
fs.readFileSync(path.join(extensionRoot, 'package.json'), 'utf-8'),
45+
);
46+
for (const cmd of packageJson.contributes.commands) {
47+
assert.ok(cmd.command, `Command missing "command" field`);
48+
assert.ok(cmd.title, `Command ${cmd.command} missing "title" field`);
49+
assert.ok(
50+
cmd.title.startsWith('Workflow:'),
51+
`Command ${cmd.command} title should start with "Workflow:", got "${cmd.title}"`,
52+
);
53+
}
54+
});
55+
56+
test('visual editor menu contribution has correct when clause', () => {
57+
const packageJson = JSON.parse(
58+
fs.readFileSync(path.join(extensionRoot, 'package.json'), 'utf-8'),
59+
);
60+
const editorTitleMenus = packageJson.contributes.menus['editor/title'];
61+
assert.ok(editorTitleMenus, 'Must have editor/title menu contributions');
62+
63+
const visualEditorMenu = editorTitleMenus.find(
64+
(m: { command: string }) => m.command === 'workflow.openVisualEditor',
65+
);
66+
assert.ok(visualEditorMenu, 'Visual editor must be in editor/title menu');
67+
assert.ok(
68+
visualEditorMenu.when.includes('.yaml') || visualEditorMenu.when.includes('.yml'),
69+
'Visual editor menu should only show for YAML files',
70+
);
71+
});
72+
});

src/test/suite/mcp-config.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import * as assert from 'assert';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
import * as os from 'os';
5+
6+
/**
7+
* Tests MCP config file read/write/merge logic.
8+
* Validates the same patterns used in mcp-config.ts without needing
9+
* the full VS Code API for the pure I/O functions.
10+
*/
11+
suite('MCP Config', () => {
12+
let tmpDir: string;
13+
14+
setup(() => {
15+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workflow-mcp-test-'));
16+
});
17+
18+
teardown(() => {
19+
fs.rmSync(tmpDir, { recursive: true, force: true });
20+
});
21+
22+
test('creates new mcp.json with workflow server', () => {
23+
const mcpPath = path.join(tmpDir, '.vscode', 'mcp.json');
24+
const dir = path.dirname(mcpPath);
25+
fs.mkdirSync(dir, { recursive: true });
26+
27+
const config = {
28+
servers: {
29+
workflow: {
30+
command: '/usr/local/bin/wfctl',
31+
args: ['mcp'],
32+
},
33+
},
34+
};
35+
fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
36+
37+
const loaded = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
38+
assert.ok(loaded.servers.workflow, 'Must have workflow server');
39+
assert.strictEqual(loaded.servers.workflow.command, '/usr/local/bin/wfctl');
40+
assert.deepStrictEqual(loaded.servers.workflow.args, ['mcp']);
41+
});
42+
43+
test('merges workflow server into existing mcp.json', () => {
44+
const mcpPath = path.join(tmpDir, 'mcp.json');
45+
const existing = {
46+
servers: {
47+
other: { command: '/usr/local/bin/other', args: ['serve'] },
48+
},
49+
};
50+
fs.writeFileSync(mcpPath, JSON.stringify(existing));
51+
52+
// Simulate the merge logic from mcp-config.ts
53+
const current = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
54+
const updated = {
55+
...current,
56+
servers: {
57+
...(current.servers ?? {}),
58+
workflow: { command: '/usr/local/bin/wfctl', args: ['mcp'] },
59+
},
60+
};
61+
fs.writeFileSync(mcpPath, JSON.stringify(updated, null, 2) + '\n');
62+
63+
const result = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
64+
assert.ok(result.servers.other, 'Must preserve existing server');
65+
assert.ok(result.servers.workflow, 'Must add workflow server');
66+
assert.strictEqual(result.servers.other.command, '/usr/local/bin/other');
67+
assert.strictEqual(result.servers.workflow.command, '/usr/local/bin/wfctl');
68+
});
69+
70+
test('detects existing workflow server', () => {
71+
const config = {
72+
servers: {
73+
workflow: { command: '/usr/local/bin/wfctl', args: ['mcp'] },
74+
},
75+
};
76+
const hasWorkflow = !!(config.servers && config.servers['workflow']);
77+
assert.ok(hasWorkflow);
78+
});
79+
80+
test('detects missing workflow server', () => {
81+
const config = {
82+
servers: {
83+
other: { command: '/usr/local/bin/other' },
84+
},
85+
};
86+
const hasWorkflow = !!(config.servers && (config.servers as Record<string, unknown>)['workflow']);
87+
assert.ok(!hasWorkflow);
88+
});
89+
90+
test('handles empty/missing config gracefully', () => {
91+
const mcpPath = path.join(tmpDir, 'nonexistent.json');
92+
let config = {};
93+
if (fs.existsSync(mcpPath)) {
94+
try {
95+
config = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
96+
} catch {
97+
config = {};
98+
}
99+
}
100+
assert.deepStrictEqual(config, {});
101+
});
102+
103+
test('handles corrupt JSON gracefully', () => {
104+
const mcpPath = path.join(tmpDir, 'corrupt.json');
105+
fs.writeFileSync(mcpPath, '{not valid json!!!');
106+
107+
let config = {};
108+
try {
109+
config = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
110+
} catch {
111+
config = {};
112+
}
113+
assert.deepStrictEqual(config, {});
114+
});
115+
});

src/test/suite/platform.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as assert from 'assert';
2+
import * as os from 'os';
3+
4+
/**
5+
* Tests the platform detection logic used by wfctl.ts and lsp-client.ts
6+
* for binary downloads. Validates the current platform is supported and
7+
* maps correctly.
8+
*/
9+
suite('Platform Detection', () => {
10+
// Reimplements getPlatformSuffix() from wfctl.ts for testing
11+
function getPlatformSuffix(): string {
12+
const platform = os.platform();
13+
const arch = os.arch();
14+
if (platform === 'darwin') {
15+
return arch === 'arm64' ? 'darwin-arm64' : 'darwin-amd64';
16+
}
17+
if (platform === 'linux') {
18+
return arch === 'arm64' ? 'linux-arm64' : 'linux-amd64';
19+
}
20+
if (platform === 'win32') {
21+
return 'windows-amd64';
22+
}
23+
throw new Error(`Unsupported platform: ${platform}/${arch}`);
24+
}
25+
26+
test('current platform is supported', () => {
27+
// Should not throw
28+
const suffix = getPlatformSuffix();
29+
assert.ok(suffix.length > 0, 'Platform suffix must not be empty');
30+
});
31+
32+
test('platform suffix contains expected segments', () => {
33+
const suffix = getPlatformSuffix();
34+
const [osName, arch] = suffix.split('-');
35+
36+
assert.ok(
37+
['darwin', 'linux', 'windows'].includes(osName),
38+
`OS must be darwin/linux/windows, got ${osName}`,
39+
);
40+
assert.ok(
41+
['amd64', 'arm64'].includes(arch),
42+
`Arch must be amd64/arm64, got ${arch}`,
43+
);
44+
});
45+
46+
test('binary filename has .exe on windows', () => {
47+
const platform = os.platform();
48+
const binaryFileName = platform === 'win32' ? 'wfctl.exe' : 'wfctl';
49+
if (platform === 'win32') {
50+
assert.ok(binaryFileName.endsWith('.exe'));
51+
} else {
52+
assert.ok(!binaryFileName.endsWith('.exe'));
53+
}
54+
});
55+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import * as assert from 'assert';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
import * as os from 'os';
5+
6+
/**
7+
* Test the parseGoModPlugins function by importing the module's logic.
8+
* Since parseGoModPlugins is not exported, we test the regex pattern directly.
9+
*/
10+
suite('Plugin Discovery', () => {
11+
let tmpDir: string;
12+
13+
setup(() => {
14+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workflow-pd-test-'));
15+
});
16+
17+
teardown(() => {
18+
fs.rmSync(tmpDir, { recursive: true, force: true });
19+
});
20+
21+
test('go.mod plugin regex matches workflow-plugin-* dependencies', () => {
22+
const goMod = `module github.com/example/myapp
23+
24+
go 1.22
25+
26+
require (
27+
github.com/GoCodeAlone/workflow v0.3.30
28+
github.com/GoCodeAlone/workflow-plugin-agent v0.3.1
29+
github.com/GoCodeAlone/workflow-plugin-authz v0.2.0
30+
github.com/GoCodeAlone/workflow-plugin-payments v0.1.0
31+
github.com/GoCodeAlone/modular v1.12.0
32+
github.com/some-other/module v1.0.0
33+
)`;
34+
35+
const plugins: string[] = [];
36+
for (const line of goMod.split('\n')) {
37+
const match = line.match(/github\.com\/GoCodeAlone\/(workflow-plugin-\S+)/);
38+
if (match) plugins.push(match[1]);
39+
}
40+
41+
assert.deepStrictEqual(plugins, [
42+
'workflow-plugin-agent',
43+
'workflow-plugin-authz',
44+
'workflow-plugin-payments',
45+
]);
46+
});
47+
48+
test('go.mod regex does not match non-plugin workflow modules', () => {
49+
const goMod = `require (
50+
github.com/GoCodeAlone/workflow v0.3.30
51+
github.com/GoCodeAlone/modular v1.12.0
52+
github.com/GoCodeAlone/workflow-editor v0.1.0
53+
)`;
54+
55+
const plugins: string[] = [];
56+
for (const line of goMod.split('\n')) {
57+
const match = line.match(/github\.com\/GoCodeAlone\/(workflow-plugin-\S+)/);
58+
if (match) plugins.push(match[1]);
59+
}
60+
61+
assert.deepStrictEqual(plugins, []);
62+
});
63+
64+
test('go.mod regex handles version suffixes correctly', () => {
65+
const goMod = `require github.com/GoCodeAlone/workflow-plugin-agent v0.3.1 // indirect`;
66+
67+
const plugins: string[] = [];
68+
for (const line of goMod.split('\n')) {
69+
const match = line.match(/github\.com\/GoCodeAlone\/(workflow-plugin-\S+)/);
70+
if (match) plugins.push(match[1]);
71+
}
72+
73+
// The regex captures "workflow-plugin-agent" and stops at whitespace
74+
assert.strictEqual(plugins.length, 1);
75+
assert.strictEqual(plugins[0], 'workflow-plugin-agent');
76+
});
77+
78+
test('go.mod regex returns empty for empty content', () => {
79+
const plugins: string[] = [];
80+
for (const line of ''.split('\n')) {
81+
const match = line.match(/github\.com\/GoCodeAlone\/(workflow-plugin-\S+)/);
82+
if (match) plugins.push(match[1]);
83+
}
84+
assert.deepStrictEqual(plugins, []);
85+
});
86+
87+
test('cache directory creation works', () => {
88+
const cacheDir = path.join(tmpDir, 'plugin-manifests');
89+
assert.ok(!fs.existsSync(cacheDir));
90+
fs.mkdirSync(cacheDir, { recursive: true });
91+
assert.ok(fs.existsSync(cacheDir));
92+
});
93+
94+
test('cached manifest read/write round-trips', () => {
95+
const cacheDir = path.join(tmpDir, 'plugin-manifests');
96+
fs.mkdirSync(cacheDir, { recursive: true });
97+
98+
const manifest = {
99+
name: 'workflow-plugin-agent',
100+
stepTypes: [{ name: 'step.agent_execute' }],
101+
moduleTypes: [{ name: 'agent.provider' }],
102+
};
103+
104+
const cacheFile = path.join(cacheDir, 'workflow-plugin-agent.json');
105+
fs.writeFileSync(cacheFile, JSON.stringify(manifest));
106+
107+
const loaded = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
108+
assert.deepStrictEqual(loaded, manifest);
109+
});
110+
});

0 commit comments

Comments
 (0)