@nospt/plugin-dev-ai-hub-backend 0.2.4 → 0.3.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 (46) hide show
  1. package/config.d.ts +22 -0
  2. package/dist/database/TelemetryStore.cjs.js +84 -0
  3. package/dist/database/TelemetryStore.cjs.js.map +1 -0
  4. package/dist/plugin.cjs.js +24 -45
  5. package/dist/plugin.cjs.js.map +1 -1
  6. package/dist/router.cjs.js +147 -202
  7. package/dist/router.cjs.js.map +1 -1
  8. package/dist/service/bodyResolver.cjs.js +56 -0
  9. package/dist/service/bodyResolver.cjs.js.map +1 -0
  10. package/dist/service/telemetryHash.cjs.js +14 -0
  11. package/dist/service/telemetryHash.cjs.js.map +1 -0
  12. package/dist/service/toResourceSummary.cjs.js +31 -0
  13. package/dist/service/toResourceSummary.cjs.js.map +1 -0
  14. package/migrations/001_init.js +13 -42
  15. package/migrations/002_install_count.js +7 -12
  16. package/migrations/003_install_paths.js +7 -14
  17. package/migrations/004_label.js +7 -12
  18. package/migrations/005_remove_unused_fields.js +6 -16
  19. package/migrations/006_resources_content.js +10 -13
  20. package/migrations/007_telemetry.js +28 -0
  21. package/migrations/008_drop_legacy_tables.js +24 -0
  22. package/migrations/009_telemetry_settings.js +25 -0
  23. package/package.json +18 -8
  24. package/.eslintrc.js +0 -1
  25. package/dev/index.ts +0 -8
  26. package/dist/database/AiAssetStore.cjs.js +0 -225
  27. package/dist/database/AiAssetStore.cjs.js.map +0 -1
  28. package/dist/service/AiAssetSyncService.cjs.js +0 -235
  29. package/dist/service/AiAssetSyncService.cjs.js.map +0 -1
  30. package/dist/service/AssetParser.cjs.js +0 -74
  31. package/dist/service/AssetParser.cjs.js.map +0 -1
  32. package/dist/service/McpServerService.cjs.js +0 -366
  33. package/dist/service/McpServerService.cjs.js.map +0 -1
  34. package/package.json-prepack +0 -71
  35. package/src/database/AiAssetStore.test.ts +0 -294
  36. package/src/database/AiAssetStore.ts +0 -256
  37. package/src/index.ts +0 -2
  38. package/src/plugin.ts +0 -84
  39. package/src/router.smoke.test.ts +0 -262
  40. package/src/router.ts +0 -338
  41. package/src/service/AiAssetSyncService.ts +0 -298
  42. package/src/service/AssetParser.test.ts +0 -329
  43. package/src/service/AssetParser.ts +0 -103
  44. package/src/service/McpServerService.ts +0 -464
  45. package/src/types.ts +0 -31
  46. package/tsconfig.json +0 -8
@@ -1,298 +0,0 @@
1
- import type {
2
- LoggerService,
3
- SchedulerService,
4
- UrlReaderService,
5
- } from '@backstage/backend-plugin-api';
6
- import type { AiAssetStore } from '../database/AiAssetStore';
7
- import type { ProviderConfig } from '../types';
8
- import type { AiAssetProvider } from '@nospt/plugin-dev-ai-hub-node';
9
- import { AssetParser } from './AssetParser';
10
-
11
- interface Options {
12
- logger: LoggerService;
13
- store: AiAssetStore;
14
- scheduler: SchedulerService;
15
- urlReader: UrlReaderService;
16
- providers: ProviderConfig[];
17
- externalProviders: AiAssetProvider[];
18
- }
19
-
20
- export class AiAssetSyncService {
21
- constructor(private readonly options: Options) {}
22
-
23
- async start(): Promise<void> {
24
- const { scheduler, logger, providers, externalProviders } = this.options;
25
-
26
- if (providers.length === 0 && externalProviders.length === 0) {
27
- logger.warn(
28
- 'dev-ai-hub: no providers configured under devAiHub.providers',
29
- );
30
- return;
31
- }
32
-
33
- for (const provider of providers) {
34
- const { frequency, timeout } = provider.schedule;
35
- const frequencyDuration = frequency.minutes
36
- ? { minutes: frequency.minutes }
37
- : { hours: frequency.hours ?? 1 };
38
- const timeoutDuration = timeout.minutes
39
- ? { minutes: timeout.minutes }
40
- : { hours: timeout.hours ?? 1 };
41
-
42
- await scheduler.scheduleTask({
43
- id: `dev-ai-hub-sync-${provider.id}`,
44
- frequency: frequencyDuration,
45
- timeout: timeoutDuration,
46
- initialDelay: { seconds: 15 },
47
- fn: async () => {
48
- await this.syncProvider(provider);
49
- },
50
- });
51
-
52
- logger.info(
53
- `dev-ai-hub: scheduled sync for provider "${provider.id}" every ${JSON.stringify(frequencyDuration)}`,
54
- );
55
- }
56
-
57
- for (const extProvider of externalProviders) {
58
- await scheduler.scheduleTask({
59
- id: `dev-ai-hub-sync-external-${extProvider.id}`,
60
- frequency: { minutes: 60 },
61
- timeout: { minutes: 5 },
62
- initialDelay: { seconds: 15 },
63
- fn: async () => {
64
- await this.syncExternalProvider(extProvider);
65
- },
66
- });
67
-
68
- logger.info(
69
- `dev-ai-hub: scheduled sync for external provider "${extProvider.id}"`,
70
- );
71
- }
72
- }
73
-
74
- async syncProvider(provider: ProviderConfig): Promise<void> {
75
- const { logger, urlReader, store } = this.options;
76
-
77
- logger.info(`dev-ai-hub: starting sync for provider "${provider.id}"`);
78
-
79
- await store.upsertSyncStatus({
80
- providerId: provider.id,
81
- status: 'syncing',
82
- assetCount: 0,
83
- });
84
-
85
- try {
86
- const treeUrl = buildTreeUrl(provider);
87
- const tree = await urlReader.readTree(treeUrl);
88
- const files = await tree.files();
89
-
90
- const fileMap = new Map<
91
- string,
92
- { content(): Promise<Buffer>; path: string }
93
- >();
94
- for (const file of files) {
95
- fileMap.set(normalizePath(file.path), file);
96
- }
97
-
98
- const syncedIds: string[] = [];
99
-
100
- for (const [filePath, file] of fileMap) {
101
- if (!filePath.endsWith('.yaml')) continue;
102
-
103
- const yamlContent = (await file.content()).toString('utf-8');
104
- const parsed = AssetParser.parseYaml(yamlContent, filePath);
105
-
106
- if (!parsed) {
107
- logger.warn(
108
- `dev-ai-hub: skipping invalid YAML at ${filePath} (provider ${provider.id})`,
109
- );
110
- continue;
111
- }
112
-
113
- if (
114
- provider.filters?.types &&
115
- !provider.filters.types.includes(parsed.meta.type)
116
- ) {
117
- continue;
118
- }
119
- if (provider.filters?.tools) {
120
- const hasMatchingTool = parsed.meta.tools.some(t =>
121
- provider.filters!.tools!.includes(t),
122
- );
123
- if (!hasMatchingTool) continue;
124
- }
125
-
126
- // Resolve the .md file:
127
- // - explicit: yaml has a content: field → use that exact path
128
- // - junction: find the single .md co-located in the same directory
129
- const yamlDir = filePath.includes('/')
130
- ? filePath.split('/').slice(0, -1).join('/')
131
- : '';
132
-
133
- let resolvedMdPath: string;
134
- let mdFile: { content(): Promise<Buffer>; path: string } | undefined;
135
-
136
- if (parsed.mdPath) {
137
- resolvedMdPath = normalizePath(parsed.mdPath);
138
- mdFile = fileMap.get(resolvedMdPath);
139
- if (!mdFile) {
140
- logger.warn(
141
- `dev-ai-hub: content file "${parsed.mdPath}" not found for ${filePath}, skipping`,
142
- );
143
- continue;
144
- }
145
- } else {
146
- const mdCandidates = [...fileMap.keys()].filter(p => {
147
- const dir = p.includes('/') ? p.split('/').slice(0, -1).join('/') : '';
148
- return dir === yamlDir && p.endsWith('.md');
149
- });
150
- if (mdCandidates.length === 1) {
151
- resolvedMdPath = mdCandidates[0];
152
- mdFile = fileMap.get(resolvedMdPath)!;
153
- } else if (mdCandidates.length > 1) {
154
- logger.warn(
155
- `dev-ai-hub: multiple .md files in "${yamlDir || '.'}" for ${filePath} — add a 'content' field to the YAML to specify which one, skipping`,
156
- );
157
- continue;
158
- } else {
159
- logger.warn(
160
- `dev-ai-hub: no .md file found for ${filePath}, skipping`,
161
- );
162
- continue;
163
- }
164
- }
165
-
166
- const mdContent = (await mdFile.content()).toString('utf-8');
167
-
168
- // For skills: read bundled resource files and store their content
169
- let resourcesContent: Record<string, string> | undefined;
170
- const resourcePaths = parsed.meta.resources;
171
- if (resourcePaths && resourcePaths.length > 0) {
172
- resourcesContent = {};
173
- for (const resourcePath of resourcePaths) {
174
- const fullPath = normalizePath(
175
- yamlDir ? `${yamlDir}/${resourcePath}` : resourcePath,
176
- );
177
- const resourceFile = fileMap.get(fullPath);
178
- if (resourceFile) {
179
- resourcesContent[resourcePath] = (await resourceFile.content()).toString('utf-8');
180
- } else {
181
- logger.warn(
182
- `dev-ai-hub: resource file not found: ${fullPath} (asset ${filePath})`,
183
- );
184
- }
185
- }
186
- if (Object.keys(resourcesContent).length === 0) resourcesContent = undefined;
187
- }
188
-
189
- const asset = AssetParser.buildAsset(
190
- parsed,
191
- mdContent,
192
- provider.id,
193
- provider.target,
194
- provider.branch,
195
- filePath,
196
- resolvedMdPath,
197
- resourcesContent,
198
- );
199
-
200
- await store.upsertAsset(asset);
201
- syncedIds.push(asset.id);
202
- }
203
-
204
- await store.deleteAssetsNotIn(provider.id, syncedIds);
205
-
206
- await store.upsertSyncStatus({
207
- providerId: provider.id,
208
- lastSync: new Date().toISOString(),
209
- status: 'idle',
210
- assetCount: syncedIds.length,
211
- });
212
-
213
- logger.info(
214
- `dev-ai-hub: sync complete for provider "${provider.id}" — ${syncedIds.length} assets`,
215
- );
216
- } catch (err) {
217
- const message = err instanceof Error ? err.message : String(err);
218
- this.options.logger.error(
219
- `dev-ai-hub: sync failed for provider "${provider.id}": ${message}`,
220
- );
221
- await store.upsertSyncStatus({
222
- providerId: provider.id,
223
- status: 'error',
224
- error: message,
225
- assetCount: 0,
226
- });
227
- }
228
- }
229
-
230
- private async syncExternalProvider(provider: AiAssetProvider): Promise<void> {
231
- const { logger, store } = this.options;
232
-
233
- logger.info(`dev-ai-hub: syncing external provider "${provider.id}"`);
234
-
235
- await store.upsertSyncStatus({
236
- providerId: provider.id,
237
- status: 'syncing',
238
- assetCount: 0,
239
- });
240
-
241
- try {
242
- const assets = await provider.getAssets();
243
- const syncedIds: string[] = [];
244
-
245
- for (const assetData of assets) {
246
- const id = AssetParser.buildId(provider.id, assetData.yamlPath);
247
- await store.upsertAsset({ ...assetData, id });
248
- syncedIds.push(id);
249
- }
250
-
251
- await store.deleteAssetsNotIn(provider.id, syncedIds);
252
-
253
- await store.upsertSyncStatus({
254
- providerId: provider.id,
255
- lastSync: new Date().toISOString(),
256
- status: 'idle',
257
- assetCount: syncedIds.length,
258
- });
259
-
260
- logger.info(
261
- `dev-ai-hub: external sync complete for "${provider.id}" — ${syncedIds.length} assets`,
262
- );
263
- } catch (err) {
264
- const message = err instanceof Error ? err.message : String(err);
265
- logger.error(
266
- `dev-ai-hub: external sync failed for "${provider.id}": ${message}`,
267
- );
268
- await store.upsertSyncStatus({
269
- providerId: provider.id,
270
- status: 'error',
271
- error: message,
272
- assetCount: 0,
273
- });
274
- }
275
- }
276
- }
277
-
278
- function normalizePath(p: string): string {
279
- return p.replace(/\\/g, '/').replace(/^\//, '');
280
- }
281
-
282
- function buildTreeUrl(provider: ProviderConfig): string {
283
- const base = provider.target.replace(/\.git$/, '');
284
- const { branch, type } = provider;
285
-
286
- switch (type) {
287
- case 'gitlab':
288
- return `${base}/-/tree/${branch}/`;
289
- case 'bitbucket':
290
- return `${base}/src/${branch}/`;
291
- case 'azure-devops':
292
- return `${base}?version=GB${branch}`;
293
- case 'github':
294
- case 'git':
295
- default:
296
- return `${base}/tree/${branch}/`;
297
- }
298
- }
@@ -1,329 +0,0 @@
1
- import { AssetParser } from './AssetParser';
2
-
3
- const VALID_YAML = `
4
- name: My Instruction
5
- description: A test instruction
6
- type: instruction
7
- tools:
8
- - claude-code
9
- tags:
10
- - testing
11
- author: Test Author
12
- version: 1.2.3
13
- `;
14
-
15
- describe('AssetParser.parseYaml', () => {
16
- it('parses a valid YAML envelope and returns metadata + mdPath', () => {
17
- const result = AssetParser.parseYaml(VALID_YAML, 'instructions/my-instruction.yaml');
18
- expect(result).not.toBeNull();
19
- expect(result!.meta.name).toBe('My Instruction');
20
- expect(result!.meta.description).toBe('A test instruction');
21
- expect(result!.meta.type).toBe('instruction');
22
- expect(result!.meta.tools).toEqual(['claude-code']);
23
- expect(result!.meta.tags).toEqual(['testing']);
24
- expect(result!.meta.author).toBe('Test Author');
25
- expect(result!.meta.version).toBe('1.2.3');
26
- expect(result!.yamlRaw).toBe(VALID_YAML);
27
- });
28
-
29
- it('leaves mdPath undefined when no content field is set', () => {
30
- const result = AssetParser.parseYaml(VALID_YAML, 'instructions/my-instruction.yaml');
31
- expect(result!.mdPath).toBeUndefined();
32
- });
33
-
34
- it('uses the content field to resolve mdPath when present', () => {
35
- const yaml = `${VALID_YAML}content: custom-content.md\n`;
36
- const result = AssetParser.parseYaml(yaml, 'instructions/my-instruction.yaml');
37
- expect(result!.mdPath).toBe('instructions/custom-content.md');
38
- });
39
-
40
- it('resolves content path relative to the yaml directory', () => {
41
- const yaml = `${VALID_YAML}content: sub/custom.md\n`;
42
- const result = AssetParser.parseYaml(yaml, 'agents/my-agent.yaml');
43
- expect(result!.mdPath).toBe('agents/sub/custom.md');
44
- });
45
-
46
- it('returns null for invalid YAML syntax', () => {
47
- expect(AssetParser.parseYaml(': bad: yaml: :', 'test.yaml')).toBeNull();
48
- });
49
-
50
- it('returns null when required field "name" is missing', () => {
51
- const yaml = `
52
- description: A description
53
- type: instruction
54
- tools:
55
- - claude-code
56
- `;
57
- expect(AssetParser.parseYaml(yaml, 'test.yaml')).toBeNull();
58
- });
59
-
60
- it('returns null when required field "type" is missing', () => {
61
- const yaml = `
62
- name: Test
63
- description: A description
64
- tools:
65
- - claude-code
66
- `;
67
- expect(AssetParser.parseYaml(yaml, 'test.yaml')).toBeNull();
68
- });
69
-
70
- it('returns null when type is not a recognised enum value', () => {
71
- const yaml = `
72
- name: Test
73
- description: A description
74
- type: invalid-type
75
- tools:
76
- - claude-code
77
- `;
78
- expect(AssetParser.parseYaml(yaml, 'test.yaml')).toBeNull();
79
- });
80
-
81
- it('returns null when tools array is empty', () => {
82
- const yaml = `
83
- name: Test
84
- description: A description
85
- type: instruction
86
- tools: []
87
- `;
88
- expect(AssetParser.parseYaml(yaml, 'test.yaml')).toBeNull();
89
- });
90
-
91
- it('applies schema defaults for optional fields', () => {
92
- const yaml = `
93
- name: Minimal
94
- description: Minimal description
95
- type: agent
96
- tools:
97
- - claude-code
98
- `;
99
- const result = AssetParser.parseYaml(yaml, 'agents/minimal.yaml');
100
- expect(result!.meta.author).toBe('Unknown');
101
- expect(result!.meta.version).toBe('1.0.0');
102
- expect(result!.meta.tags).toEqual([]);
103
- });
104
-
105
- it('passes through unrecognised fields (passthrough schema)', () => {
106
- const yaml = `${VALID_YAML}unknownField: some-value\n`;
107
- const result = AssetParser.parseYaml(yaml, 'test.yaml');
108
- expect(result).not.toBeNull();
109
- });
110
- });
111
-
112
- describe('AssetParser.buildId', () => {
113
- it('returns a base64url encoded string', () => {
114
- const id = AssetParser.buildId('provider-1', 'instructions/my-asset.yaml');
115
- expect(id).toBe(Buffer.from('provider-1:instructions/my-asset.yaml').toString('base64url'));
116
- });
117
-
118
- it('normalises backslashes to forward slashes', () => {
119
- const id1 = AssetParser.buildId('p', 'instructions\\my-asset.yaml');
120
- const id2 = AssetParser.buildId('p', 'instructions/my-asset.yaml');
121
- expect(id1).toBe(id2);
122
- });
123
-
124
- it('strips a leading slash', () => {
125
- const id1 = AssetParser.buildId('p', '/instructions/my-asset.yaml');
126
- const id2 = AssetParser.buildId('p', 'instructions/my-asset.yaml');
127
- expect(id1).toBe(id2);
128
- });
129
-
130
- it('is deterministic for the same inputs', () => {
131
- const id1 = AssetParser.buildId('provider-1', 'path/to/asset.yaml');
132
- const id2 = AssetParser.buildId('provider-1', 'path/to/asset.yaml');
133
- expect(id1).toBe(id2);
134
- });
135
-
136
- it('produces different IDs for different providers', () => {
137
- const id1 = AssetParser.buildId('provider-1', 'path/asset.yaml');
138
- const id2 = AssetParser.buildId('provider-2', 'path/asset.yaml');
139
- expect(id1).not.toBe(id2);
140
- });
141
-
142
- it('produces different IDs for different paths', () => {
143
- const id1 = AssetParser.buildId('p', 'instructions/a.yaml');
144
- const id2 = AssetParser.buildId('p', 'agents/a.yaml');
145
- expect(id1).not.toBe(id2);
146
- });
147
- });
148
-
149
- describe('AssetParser.buildAsset', () => {
150
- const parsed = AssetParser.parseYaml(VALID_YAML, 'instructions/my-instruction.yaml')!;
151
-
152
- it('builds a complete AiAssetInput with all expected fields', () => {
153
- const asset = AssetParser.buildAsset(
154
- parsed,
155
- '# Content',
156
- 'my-provider',
157
- 'https://github.com/org/repo',
158
- 'main',
159
- 'instructions/my-instruction.yaml',
160
- 'instructions/my-instruction.md',
161
- );
162
-
163
- expect(asset.id).toBe(AssetParser.buildId('my-provider', 'instructions/my-instruction.yaml'));
164
- expect(asset.name).toBe('My Instruction');
165
- expect(asset.type).toBe('instruction');
166
- expect(asset.tools).toEqual(['claude-code']);
167
- expect(asset.tags).toEqual(['testing']);
168
- expect(asset.content).toBe('# Content');
169
- expect(asset.yamlRaw).toBe(VALID_YAML);
170
- expect(asset.providerId).toBe('my-provider');
171
- expect(asset.repoUrl).toBe('https://github.com/org/repo');
172
- expect(asset.branch).toBe('main');
173
- expect(asset.yamlPath).toBe('instructions/my-instruction.yaml');
174
- expect(asset.mdPath).toBe('instructions/my-instruction.md');
175
- expect(asset.author).toBe('Test Author');
176
- expect(asset.version).toBe('1.2.3');
177
- });
178
-
179
- it('sets metadata.mcpServers when mcpServers is present in the YAML', () => {
180
- const yaml = `${VALID_YAML}mcpServers:\n my-server:\n url: http://localhost\n`;
181
- const parsedWithMcp = AssetParser.parseYaml(yaml, 'agents/agent.yaml')!;
182
- const asset = AssetParser.buildAsset(parsedWithMcp, '', 'p', 'url', 'main', 'agents/agent.yaml', 'agents/agent.md');
183
- expect(asset.metadata?.mcpServers).toEqual({ 'my-server': { url: 'http://localhost' } });
184
- });
185
-
186
- it('sets metadata.steps when steps is present in the YAML', () => {
187
- const yaml = `${VALID_YAML}steps:\n - name: step1\n action: do-something\n`;
188
- const parsedWithSteps = AssetParser.parseYaml(yaml, 'workflows/w.yaml')!;
189
- const asset = AssetParser.buildAsset(parsedWithSteps, '', 'p', 'url', 'main', 'workflows/w.yaml', 'workflows/w.md');
190
- expect(asset.metadata?.steps).toEqual([{ name: 'step1', action: 'do-something' }]);
191
- });
192
-
193
- it('leaves metadata undefined when no extra fields are present', () => {
194
- const asset = AssetParser.buildAsset(
195
- parsed,
196
- '',
197
- 'p',
198
- 'url',
199
- 'main',
200
- 'instructions/my-instruction.yaml',
201
- 'instructions/my-instruction.md',
202
- );
203
- expect(asset.metadata).toBeUndefined();
204
- });
205
-
206
- it('defaults tags to [] when not specified', () => {
207
- const yaml = `
208
- name: No Tags
209
- description: A description
210
- type: instruction
211
- tools:
212
- - claude-code
213
- `;
214
- const parsedNoTags = AssetParser.parseYaml(yaml, 'instructions/no-tags.yaml')!;
215
- const asset = AssetParser.buildAsset(parsedNoTags, '', 'p', 'url', 'main', 'instructions/no-tags.yaml', 'instructions/no-tags.md');
216
- expect(asset.tags).toEqual([]);
217
- });
218
-
219
- it('defaults author to "Unknown" when not specified', () => {
220
- const yaml = `
221
- name: No Author
222
- description: A description
223
- type: instruction
224
- tools:
225
- - claude-code
226
- `;
227
- const parsedNoAuthor = AssetParser.parseYaml(yaml, 'instructions/no-author.yaml')!;
228
- const asset = AssetParser.buildAsset(parsedNoAuthor, '', 'p', 'url', 'main', 'instructions/no-author.yaml', 'instructions/no-author.md');
229
- expect(asset.author).toBe('Unknown');
230
- });
231
- });
232
-
233
- describe('AssetParser.parseYaml — schema-as-gatekeeper (arbitrary path discovery)', () => {
234
- it('discovers a valid asset at an arbitrary custom directory', () => {
235
- const result = AssetParser.parseYaml(VALID_YAML, 'ai-artifacts/my-agent.yaml');
236
- expect(result).not.toBeNull();
237
- expect(result!.meta.name).toBe('My Instruction');
238
- expect(result!.mdPath).toBeUndefined();
239
- });
240
-
241
- it('discovers a valid asset at a deeply nested path', () => {
242
- const result = AssetParser.parseYaml(VALID_YAML, 'team/ai-assets/copilot/my-agent.yaml');
243
- expect(result).not.toBeNull();
244
- expect(result!.mdPath).toBeUndefined();
245
- });
246
-
247
- it('discovers a valid asset at the repo root (no subdirectory)', () => {
248
- const result = AssetParser.parseYaml(VALID_YAML, 'my-instruction.yaml');
249
- expect(result).not.toBeNull();
250
- expect(result!.mdPath).toBeUndefined();
251
- });
252
-
253
- it('rejects a GitHub Actions workflow YAML (missing required fields)', () => {
254
- const ghActionsYaml = `
255
- on:
256
- push:
257
- branches: [main]
258
- jobs:
259
- build:
260
- runs-on: ubuntu-latest
261
- steps:
262
- - uses: actions/checkout@v4
263
- `;
264
- expect(AssetParser.parseYaml(ghActionsYaml, '.github/workflows/ci.yaml')).toBeNull();
265
- });
266
-
267
- it('rejects a Helm values YAML (no name/type/tools)', () => {
268
- const helmYaml = `
269
- replicaCount: 2
270
- image:
271
- repository: nginx
272
- tag: latest
273
- service:
274
- type: ClusterIP
275
- port: 80
276
- `;
277
- expect(AssetParser.parseYaml(helmYaml, 'helm/values.yaml')).toBeNull();
278
- });
279
-
280
- it('rejects a Kubernetes manifest YAML (apiVersion/kind fields, no asset fields)', () => {
281
- const k8sYaml = `
282
- apiVersion: apps/v1
283
- kind: Deployment
284
- metadata:
285
- name: my-app
286
- spec:
287
- replicas: 1
288
- `;
289
- expect(AssetParser.parseYaml(k8sYaml, 'k8s/deployment.yaml')).toBeNull();
290
- });
291
-
292
- it('rejects a YAML with a valid-looking name but invalid type enum', () => {
293
- const yaml = `
294
- name: CI Pipeline
295
- description: Continuous integration pipeline config
296
- type: pipeline
297
- tools:
298
- - claude-code
299
- `;
300
- expect(AssetParser.parseYaml(yaml, 'ai-artifacts/pipeline.yaml')).toBeNull();
301
- });
302
-
303
- it('rejects a YAML with a valid-looking type but unrecognised tool', () => {
304
- const yaml = `
305
- name: My Agent
306
- description: An agent config
307
- type: agent
308
- tools:
309
- - unknown-tool
310
- `;
311
- expect(AssetParser.parseYaml(yaml, 'ai-artifacts/agent.yaml')).toBeNull();
312
- });
313
-
314
- it('buildAsset correctly sets yamlPath and mdPath for an arbitrary directory', () => {
315
- const parsed = AssetParser.parseYaml(VALID_YAML, 'ai-artifacts/my-agent.yaml')!;
316
- const asset = AssetParser.buildAsset(
317
- parsed,
318
- '# Content',
319
- 'my-provider',
320
- 'https://github.com/org/repo',
321
- 'main',
322
- 'ai-artifacts/my-agent.yaml',
323
- 'ai-artifacts/SKILL.md',
324
- );
325
- expect(asset.yamlPath).toBe('ai-artifacts/my-agent.yaml');
326
- expect(asset.mdPath).toBe('ai-artifacts/SKILL.md');
327
- expect(asset.id).toBe(AssetParser.buildId('my-provider', 'ai-artifacts/my-agent.yaml'));
328
- });
329
- });