mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 08:15:56 +01:00
plugin system: add support for rules/instructions (#301172)
* plugin system: add support for rules ('instructions') from Open Plugin spec
- Adds IAgentPluginInstruction interface and instructions property to IAgentPlugin
observable stream, following the same pattern as commands/skills/agents
- Implements _readRules() method in agentPluginServiceImpl to discover rule files
(.mdc, .md, .instructions.md) from the rules/ directory and supplemental paths
defined in the plugin manifest. Uses longest-match-first suffix stripping to
correctly derive rule names.
- Wires observeComponent('rules', ...) in _toPlugin() to integrate manifest
'rules' field configuration with the discovery mechanism
- Adds plugin instructions to the prompt file discovery system via watchPluginPromptFilesForType,
making instructions available alongside filesystem-discovered instructions
- Includes comprehensive test coverage for rule discovery patterns, suffix stripping,
deduplication, and reactive observable integration
(Commit message generated by Copilot)
* comments
This commit is contained in:
@@ -38,6 +38,11 @@ export interface IAgentPluginAgent {
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface IAgentPluginInstruction {
|
||||
readonly uri: URI;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface IAgentPluginMcpServerDefinition {
|
||||
readonly name: string;
|
||||
readonly configuration: IMcpServerConfiguration;
|
||||
@@ -54,6 +59,7 @@ export interface IAgentPlugin {
|
||||
readonly commands: IObservable<readonly IAgentPluginCommand[]>;
|
||||
readonly skills: IObservable<readonly IAgentPluginSkill[]>;
|
||||
readonly agents: IObservable<readonly IAgentPluginAgent[]>;
|
||||
readonly instructions: IObservable<readonly IAgentPluginInstruction[]>;
|
||||
readonly mcpServerDefinitions: IObservable<readonly IAgentPluginMcpServerDefinition[]>;
|
||||
/** Set when the plugin was installed from a marketplace repository. */
|
||||
readonly fromMarketplace?: IMarketplacePlugin;
|
||||
|
||||
@@ -37,11 +37,14 @@ import { parseClaudeHooks } from '../promptSyntax/hookClaudeCompat.js';
|
||||
import { parseCopilotHooks } from '../promptSyntax/hookCompatibility.js';
|
||||
import { IHookCommand } from '../promptSyntax/hookSchema.js';
|
||||
import { IAgentPluginRepositoryService } from './agentPluginRepositoryService.js';
|
||||
import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js';
|
||||
import { agentPluginDiscoveryRegistry, IAgentPlugin, IAgentPluginDiscovery, IAgentPluginHook, IAgentPluginInstruction, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from './agentPluginService.js';
|
||||
import { IMarketplacePlugin, IPluginMarketplaceService } from './pluginMarketplaceService.js';
|
||||
|
||||
const COMMAND_FILE_SUFFIX = '.md';
|
||||
|
||||
/** File suffixes accepted for rule/instruction files (longest first for correct name stripping). */
|
||||
const RULE_FILE_SUFFIXES = ['.instructions.md', '.mdc', '.md'];
|
||||
|
||||
const enum AgentPluginFormat {
|
||||
Copilot,
|
||||
Claude,
|
||||
@@ -509,6 +512,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements
|
||||
const commands = observeComponent('commands', d => this._readMarkdownComponents(d));
|
||||
const skills = observeComponent('skills', d => this._readSkills(d));
|
||||
const agents = observeComponent('agents', d => this._readMarkdownComponents(d));
|
||||
const instructions = observeComponent('rules', d => this._readRules(d));
|
||||
const hooks = observeComponent(
|
||||
'hooks',
|
||||
paths => this._readHooksFromPaths(uri, paths, adapter),
|
||||
@@ -549,6 +553,7 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements
|
||||
commands,
|
||||
skills,
|
||||
agents,
|
||||
instructions,
|
||||
mcpServerDefinitions,
|
||||
fromMarketplace,
|
||||
};
|
||||
@@ -737,6 +742,62 @@ export abstract class AbstractAgentPluginDiscovery extends Disposable implements
|
||||
return skills;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans directories for rule/instruction files (`.mdc`, `.md`,
|
||||
* `.instructions.md`), returning `{ uri, name }` entries where name is
|
||||
* derived from the filename minus the matched suffix.
|
||||
*/
|
||||
private async _readRules(dirs: readonly URI[]): Promise<readonly IAgentPluginInstruction[]> {
|
||||
const seen = new Set<string>();
|
||||
const items: IAgentPluginInstruction[] = [];
|
||||
|
||||
const matchSuffix = (filename: string): string | undefined => {
|
||||
const lower = filename.toLowerCase();
|
||||
return RULE_FILE_SUFFIXES.find(s => lower.endsWith(s));
|
||||
};
|
||||
|
||||
const addItem = (name: string, uri: URI) => {
|
||||
if (!seen.has(name)) {
|
||||
seen.add(name);
|
||||
items.push({ uri, name });
|
||||
}
|
||||
};
|
||||
|
||||
for (const dir of dirs) {
|
||||
let stat;
|
||||
try {
|
||||
stat = await this._fileService.resolve(dir);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stat.isFile) {
|
||||
const suffix = matchSuffix(basename(dir));
|
||||
if (suffix) {
|
||||
addItem(basename(dir).slice(0, -suffix.length), dir);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!stat.isDirectory || !stat.children) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const child of stat.children) {
|
||||
if (!child.isFile) {
|
||||
continue;
|
||||
}
|
||||
const suffix = matchSuffix(child.name);
|
||||
if (suffix) {
|
||||
addItem(child.name.slice(0, -suffix.length), child.resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans directories for `.md` files, returning `{ uri, name }` entries
|
||||
* where name is derived from the filename (minus the `.md` extension).
|
||||
|
||||
@@ -214,6 +214,7 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent),
|
||||
this._onDidContributedWhenChange.event,
|
||||
Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(PromptsConfig.USE_CUSTOM_AGENT_HOOKS)),
|
||||
this._onDidPluginPromptFilesChange.event,
|
||||
)
|
||||
));
|
||||
|
||||
@@ -259,6 +260,10 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
PromptsType.agent,
|
||||
(plugin, reader) => plugin.agents.read(reader),
|
||||
));
|
||||
this._register(this.watchPluginPromptFilesForType(
|
||||
PromptsType.instructions,
|
||||
(plugin, reader) => plugin.instructions.read(reader),
|
||||
));
|
||||
|
||||
this._register(autorun(reader => {
|
||||
const plugins = this.agentPluginService.plugins.read(reader);
|
||||
@@ -671,6 +676,7 @@ export class PromptsService extends Disposable implements IPromptsService {
|
||||
this.getFileLocatorEvent(PromptsType.instructions),
|
||||
this._onDidContributedWhenChange.event,
|
||||
this._onDidChangeInstructions.event,
|
||||
this._onDidPluginPromptFilesChange.event,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -401,6 +401,7 @@ suite('AgentPlugin format detection', () => {
|
||||
await writeFile('/plugins/no-manifest/commands/hello.md', '# Hello');
|
||||
await writeFile('/plugins/no-manifest/skills/my-skill/SKILL.md', '# My skill');
|
||||
await writeFile('/plugins/no-manifest/agents/helper.md', '# Helper');
|
||||
await writeFile('/plugins/no-manifest/rules/prefer-const.mdc', '---\ndescription: Prefer const\n---\nUse const.');
|
||||
|
||||
const discovery = createDiscovery();
|
||||
discovery.start(mockEnablementModel);
|
||||
@@ -418,6 +419,9 @@ suite('AgentPlugin format detection', () => {
|
||||
|
||||
await waitForState(plugins[0].agents, a => a.length > 0);
|
||||
assert.strictEqual(plugins[0].agents.get()[0].name, 'helper');
|
||||
|
||||
await waitForState(plugins[0].instructions, i => i.length > 0);
|
||||
assert.strictEqual(plugins[0].instructions.get()[0].name, 'prefer-const');
|
||||
}));
|
||||
|
||||
test('reads hooks from default hooks/hooks.json', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
|
||||
@@ -701,4 +705,132 @@ suite('AgentPlugin format detection', () => {
|
||||
await waitForState(plugins[0].mcpServerDefinitions, d => d.length > 0);
|
||||
assert.strictEqual(plugins[0].mcpServerDefinitions.get()[0].name, 'custom-server');
|
||||
}));
|
||||
|
||||
test('reads rules from rules/ directory with .mdc extension', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
|
||||
const uri = pluginUri('/plugins/rules-plugin');
|
||||
await writeFile('/plugins/rules-plugin/.plugin/plugin.json', JSON.stringify({ name: 'rules-plugin' }));
|
||||
await writeFile('/plugins/rules-plugin/rules/prefer-const.mdc', '---\ndescription: Prefer const\n---\nUse const.');
|
||||
await writeFile('/plugins/rules-plugin/rules/error-handling.mdc', '---\ndescription: Error handling\n---\nAlways handle errors.');
|
||||
|
||||
const discovery = createDiscovery();
|
||||
discovery.start(mockEnablementModel);
|
||||
await discovery.setSourcesAndRefresh([uri]);
|
||||
|
||||
const plugins = discovery.plugins.get();
|
||||
assert.strictEqual(plugins.length, 1);
|
||||
|
||||
await waitForState(plugins[0].instructions, i => i.length >= 2);
|
||||
assert.deepStrictEqual(
|
||||
plugins[0].instructions.get().map(i => i.name).sort(),
|
||||
['error-handling', 'prefer-const'],
|
||||
);
|
||||
}));
|
||||
|
||||
test('reads rules with .md and .instructions.md extensions', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
|
||||
const uri = pluginUri('/plugins/rules-mixed');
|
||||
await writeFile('/plugins/rules-mixed/.plugin/plugin.json', JSON.stringify({ name: 'rules-mixed' }));
|
||||
await writeFile('/plugins/rules-mixed/rules/rule-a.mdc', 'Rule A');
|
||||
await writeFile('/plugins/rules-mixed/rules/rule-b.md', 'Rule B');
|
||||
await writeFile('/plugins/rules-mixed/rules/rule-c.instructions.md', 'Rule C');
|
||||
|
||||
const discovery = createDiscovery();
|
||||
discovery.start(mockEnablementModel);
|
||||
await discovery.setSourcesAndRefresh([uri]);
|
||||
|
||||
const plugins = discovery.plugins.get();
|
||||
assert.strictEqual(plugins.length, 1);
|
||||
|
||||
await waitForState(plugins[0].instructions, i => i.length >= 3);
|
||||
assert.deepStrictEqual(
|
||||
plugins[0].instructions.get().map(i => i.name).sort(),
|
||||
['rule-a', 'rule-b', 'rule-c'],
|
||||
);
|
||||
}));
|
||||
|
||||
test('manifest rules field adds supplemental rule directories', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
|
||||
const uri = pluginUri('/plugins/custom-rules');
|
||||
await writeFile('/plugins/custom-rules/.plugin/plugin.json', JSON.stringify({
|
||||
name: 'custom-rules',
|
||||
rules: './extra-rules/',
|
||||
}));
|
||||
await writeFile('/plugins/custom-rules/rules/default-rule.mdc', 'Default rule');
|
||||
await writeFile('/plugins/custom-rules/extra-rules/bonus-rule.mdc', 'Bonus rule');
|
||||
|
||||
const discovery = createDiscovery();
|
||||
discovery.start(mockEnablementModel);
|
||||
await discovery.setSourcesAndRefresh([uri]);
|
||||
|
||||
const plugins = discovery.plugins.get();
|
||||
assert.strictEqual(plugins.length, 1);
|
||||
|
||||
await waitForState(plugins[0].instructions, i => i.length >= 2);
|
||||
assert.deepStrictEqual(
|
||||
plugins[0].instructions.get().map(i => i.name).sort(),
|
||||
['bonus-rule', 'default-rule'],
|
||||
);
|
||||
}));
|
||||
|
||||
test('manifest rules field with exclusive mode skips default directory', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
|
||||
const uri = pluginUri('/plugins/exclusive-rules');
|
||||
await writeFile('/plugins/exclusive-rules/.plugin/plugin.json', JSON.stringify({
|
||||
name: 'exclusive-rules',
|
||||
rules: { paths: ['./only-here/'], exclusive: true },
|
||||
}));
|
||||
await writeFile('/plugins/exclusive-rules/rules/ignored.mdc', 'Should be ignored');
|
||||
await writeFile('/plugins/exclusive-rules/only-here/visible.mdc', 'Should be visible');
|
||||
|
||||
const discovery = createDiscovery();
|
||||
discovery.start(mockEnablementModel);
|
||||
await discovery.setSourcesAndRefresh([uri]);
|
||||
|
||||
const plugins = discovery.plugins.get();
|
||||
assert.strictEqual(plugins.length, 1);
|
||||
|
||||
await waitForState(plugins[0].instructions, i => i.length === 1 && i[0].name === 'visible');
|
||||
assert.deepStrictEqual(
|
||||
plugins[0].instructions.get().map(i => i.name),
|
||||
['visible'],
|
||||
);
|
||||
}));
|
||||
|
||||
test('rule name strips longest matching suffix first', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
|
||||
const uri = pluginUri('/plugins/suffix-rules');
|
||||
await writeFile('/plugins/suffix-rules/.plugin/plugin.json', JSON.stringify({ name: 'suffix-rules' }));
|
||||
await writeFile('/plugins/suffix-rules/rules/coding-standards.instructions.md', 'Standards');
|
||||
|
||||
const discovery = createDiscovery();
|
||||
discovery.start(mockEnablementModel);
|
||||
await discovery.setSourcesAndRefresh([uri]);
|
||||
|
||||
const plugins = discovery.plugins.get();
|
||||
assert.strictEqual(plugins.length, 1);
|
||||
|
||||
await waitForState(plugins[0].instructions, i => i.length > 0);
|
||||
// Should strip '.instructions.md' (longest match), not just '.md'
|
||||
assert.strictEqual(plugins[0].instructions.get()[0].name, 'coding-standards');
|
||||
}));
|
||||
|
||||
test('deduplicates rules with the same base name', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
|
||||
const uri = pluginUri('/plugins/dup-rules');
|
||||
await writeFile('/plugins/dup-rules/.plugin/plugin.json', JSON.stringify({
|
||||
name: 'dup-rules',
|
||||
rules: './extra/',
|
||||
}));
|
||||
// Default directory has 'my-rule.mdc', supplemental has 'my-rule.md' — first wins
|
||||
await writeFile('/plugins/dup-rules/rules/my-rule.mdc', 'From default');
|
||||
await writeFile('/plugins/dup-rules/extra/my-rule.md', 'From extra');
|
||||
|
||||
const discovery = createDiscovery();
|
||||
discovery.start(mockEnablementModel);
|
||||
await discovery.setSourcesAndRefresh([uri]);
|
||||
|
||||
const plugins = discovery.plugins.get();
|
||||
assert.strictEqual(plugins.length, 1);
|
||||
|
||||
await waitForState(plugins[0].instructions, i => i.length > 0);
|
||||
assert.strictEqual(plugins[0].instructions.get().length, 1);
|
||||
const instruction = plugins[0].instructions.get()[0];
|
||||
assert.strictEqual(instruction.name, 'my-rule');
|
||||
assert.ok(instruction.uri.path.endsWith('/rules/my-rule.mdc'));
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ import { ChatModeKind } from '../../../../common/constants.js';
|
||||
import { HookType } from '../../../../common/promptSyntax/hookTypes.js';
|
||||
import { IContextKeyService, IContextKeyChangeEvent } from '../../../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { MockContextKeyService } from '../../../../../../../platform/keybinding/test/common/mockKeybindingService.js';
|
||||
import { IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginHook, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from '../../../../common/plugins/agentPluginService.js';
|
||||
import { IAgentPlugin, IAgentPluginAgent, IAgentPluginCommand, IAgentPluginHook, IAgentPluginInstruction, IAgentPluginMcpServerDefinition, IAgentPluginService, IAgentPluginSkill } from '../../../../common/plugins/agentPluginService.js';
|
||||
import { IWorkspaceTrustManagementService } from '../../../../../../../platform/workspace/common/workspaceTrust.js';
|
||||
|
||||
suite('PromptsService', () => {
|
||||
@@ -3684,6 +3684,7 @@ suite('PromptsService', () => {
|
||||
const commands = observableValue<readonly IAgentPluginCommand[]>('testPluginCommands', []);
|
||||
const skills = observableValue<readonly IAgentPluginSkill[]>('testPluginSkills', []);
|
||||
const agents = observableValue<readonly IAgentPluginAgent[]>('testPluginAgents', []);
|
||||
const instructions = observableValue<readonly IAgentPluginInstruction[]>('testPluginInstructions', []);
|
||||
const mcpServerDefinitions = observableValue<readonly IAgentPluginMcpServerDefinition[]>('testPluginMcpServerDefinitions', []);
|
||||
|
||||
return {
|
||||
@@ -3696,6 +3697,7 @@ suite('PromptsService', () => {
|
||||
commands,
|
||||
skills,
|
||||
agents,
|
||||
instructions,
|
||||
mcpServerDefinitions,
|
||||
},
|
||||
hooks,
|
||||
@@ -3855,4 +3857,110 @@ suite('PromptsService', () => {
|
||||
assert.strictEqual(result, undefined, 'Expected undefined hooks when workspace is untrusted, even with plugin hooks');
|
||||
});
|
||||
});
|
||||
|
||||
suite('plugin instructions', () => {
|
||||
function createPluginWithInstructions(
|
||||
path: string,
|
||||
initialInstructions: readonly IAgentPluginInstruction[],
|
||||
): { plugin: IAgentPlugin; instructions: ISettableObservable<readonly IAgentPluginInstruction[]> } {
|
||||
const enablement = observableValue('testPluginEnablement', 2 /* ContributionEnablementState.EnabledProfile */);
|
||||
const hooks = observableValue<readonly IAgentPluginHook[]>('testPluginHooks', []);
|
||||
const commands = observableValue<readonly IAgentPluginCommand[]>('testPluginCommands', []);
|
||||
const skills = observableValue<readonly IAgentPluginSkill[]>('testPluginSkills', []);
|
||||
const agents = observableValue<readonly IAgentPluginAgent[]>('testPluginAgents', []);
|
||||
const instructions = observableValue<readonly IAgentPluginInstruction[]>('testPluginInstructions', initialInstructions);
|
||||
const mcpServerDefinitions = observableValue<readonly IAgentPluginMcpServerDefinition[]>('testPluginMcpServerDefinitions', []);
|
||||
|
||||
return {
|
||||
plugin: {
|
||||
uri: URI.file(path),
|
||||
label: basename(URI.file(path)),
|
||||
enablement,
|
||||
remove: () => { },
|
||||
hooks,
|
||||
commands,
|
||||
skills,
|
||||
agents,
|
||||
instructions,
|
||||
mcpServerDefinitions,
|
||||
},
|
||||
instructions,
|
||||
};
|
||||
}
|
||||
|
||||
test('lists plugin instructions via listPromptFiles', async function () {
|
||||
const ruleUri = URI.file('/plugins/test-plugin/rules/prefer-const.mdc');
|
||||
const { plugin } = createPluginWithInstructions('/plugins/test-plugin', [
|
||||
{ uri: ruleUri, name: 'prefer-const' },
|
||||
]);
|
||||
|
||||
testPluginsObservable.set([plugin], undefined);
|
||||
|
||||
const result = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None);
|
||||
const pluginInstruction = result.find(p => p.uri.toString() === ruleUri.toString());
|
||||
assert.ok(pluginInstruction, 'Plugin instruction should appear in listPromptFiles');
|
||||
assert.strictEqual(pluginInstruction!.storage, PromptsStorage.plugin);
|
||||
});
|
||||
|
||||
test('updates listed instructions when plugin instructions change', async function () {
|
||||
const ruleUri1 = URI.file('/plugins/test-plugin/rules/rule-a.mdc');
|
||||
const ruleUri2 = URI.file('/plugins/test-plugin/rules/rule-b.mdc');
|
||||
const { plugin, instructions } = createPluginWithInstructions('/plugins/test-plugin', [
|
||||
{ uri: ruleUri1, name: 'rule-a' },
|
||||
]);
|
||||
|
||||
testPluginsObservable.set([plugin], undefined);
|
||||
|
||||
const before = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None);
|
||||
const beforePlugin = before.filter(p => p.storage === PromptsStorage.plugin);
|
||||
assert.strictEqual(beforePlugin.length, 1);
|
||||
|
||||
const eventFired = new Promise<void>(resolve => {
|
||||
const disposable = service.onDidChangeInstructions(() => {
|
||||
disposable.dispose();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
instructions.set([
|
||||
{ uri: ruleUri1, name: 'rule-a' },
|
||||
{ uri: ruleUri2, name: 'rule-b' },
|
||||
], undefined);
|
||||
|
||||
await eventFired;
|
||||
|
||||
const after = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None);
|
||||
const afterPlugin = after.filter(p => p.storage === PromptsStorage.plugin);
|
||||
assert.strictEqual(afterPlugin.length, 2);
|
||||
});
|
||||
|
||||
test('removes instructions when plugin is removed', async function () {
|
||||
const ruleUri = URI.file('/plugins/test-plugin/rules/rule-a.mdc');
|
||||
const { plugin } = createPluginWithInstructions('/plugins/test-plugin', [
|
||||
{ uri: ruleUri, name: 'rule-a' },
|
||||
]);
|
||||
|
||||
testPluginsObservable.set([plugin], undefined);
|
||||
const withPlugin = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None);
|
||||
assert.ok(withPlugin.some(p => p.storage === PromptsStorage.plugin));
|
||||
|
||||
testPluginsObservable.set([], undefined);
|
||||
const withoutPlugin = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None);
|
||||
assert.ok(!withoutPlugin.some(p => p.storage === PromptsStorage.plugin));
|
||||
});
|
||||
|
||||
test('namespaces plugin instruction names with plugin folder', async function () {
|
||||
const ruleUri = URI.file('/plugins/deploy-tools/rules/lint-check.mdc');
|
||||
const { plugin } = createPluginWithInstructions('/plugins/deploy-tools', [
|
||||
{ uri: ruleUri, name: 'lint-check' },
|
||||
]);
|
||||
|
||||
testPluginsObservable.set([plugin], undefined);
|
||||
|
||||
const result = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None);
|
||||
const pluginInstruction = result.find(p => p.uri.toString() === ruleUri.toString());
|
||||
assert.ok(pluginInstruction, 'Plugin instruction should be listed');
|
||||
assert.strictEqual(pluginInstruction!.name, 'deploy-tools:lint-check');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user