|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const { describe, it } = require('node:test'); |
| 4 | +const assert = require('node:assert/strict'); |
| 5 | +const fs = require('fs'); |
| 6 | +const path = require('path'); |
| 7 | +const { |
| 8 | + reportSkillInstallSuccess, |
| 9 | + skillInstallCommitted, |
| 10 | +} = require('../src/gep/skillInstallSuccess'); |
| 11 | + |
| 12 | +describe('skillInstallCommitted', () => { |
| 13 | + it('requires content or a non-empty bundled file', () => { |
| 14 | + assert.equal(skillInstallCommitted(null), false); |
| 15 | + assert.equal(skillInstallCommitted({}), false); |
| 16 | + assert.equal(skillInstallCommitted({ content: '' }), false); |
| 17 | + assert.equal(skillInstallCommitted({ content: '# Skill' }), true); |
| 18 | + assert.equal( |
| 19 | + skillInstallCommitted({ |
| 20 | + bundled_files: [{ name: 'helper.js', content: 'module.exports = 1;' }], |
| 21 | + }), |
| 22 | + true, |
| 23 | + ); |
| 24 | + assert.equal( |
| 25 | + skillInstallCommitted({ bundled_files: [{ name: 'helper.js', content: '' }] }), |
| 26 | + false, |
| 27 | + ); |
| 28 | + }); |
| 29 | +}); |
| 30 | + |
| 31 | +describe('reportSkillInstallSuccess', () => { |
| 32 | + it('POSTs /install-success with sender_id after local commit', async () => { |
| 33 | + /** @type {{ url?: string, init?: object }} */ |
| 34 | + const seen = {}; |
| 35 | + const result = await reportSkillInstallSuccess({ |
| 36 | + hubUrl: 'https://evomap.ai/', |
| 37 | + skillId: 'skill_demo', |
| 38 | + nodeId: 'node_abc', |
| 39 | + buildHeaders: () => ({ Authorization: 'Bearer secret', 'Content-Type': 'application/json' }), |
| 40 | + hubFetch: async (url, init) => { |
| 41 | + seen.url = url; |
| 42 | + seen.init = init; |
| 43 | + return { ok: true, status: 200, text: async () => '{}' }; |
| 44 | + }, |
| 45 | + }); |
| 46 | + |
| 47 | + assert.equal(result.ok, true); |
| 48 | + assert.equal(result.recorded, true); |
| 49 | + assert.equal(seen.url, 'https://evomap.ai/a2a/skill/store/skill_demo/install-success'); |
| 50 | + assert.equal(seen.init.method, 'POST'); |
| 51 | + assert.equal(seen.init.headers.Authorization, 'Bearer secret'); |
| 52 | + assert.deepEqual(JSON.parse(seen.init.body), { sender_id: 'node_abc' }); |
| 53 | + }); |
| 54 | + |
| 55 | + it('fail-soft on HTTP errors and network failures', async () => { |
| 56 | + const httpFail = await reportSkillInstallSuccess({ |
| 57 | + hubUrl: 'https://evomap.ai', |
| 58 | + skillId: 'skill_x', |
| 59 | + nodeId: 'node_1', |
| 60 | + buildHeaders: () => ({}), |
| 61 | + hubFetch: async () => ({ ok: false, status: 401, text: async () => 'nope' }), |
| 62 | + }); |
| 63 | + assert.equal(httpFail.ok, false); |
| 64 | + assert.equal(httpFail.status, 401); |
| 65 | + |
| 66 | + const netFail = await reportSkillInstallSuccess({ |
| 67 | + hubUrl: 'https://evomap.ai', |
| 68 | + skillId: 'skill_x', |
| 69 | + nodeId: 'node_1', |
| 70 | + buildHeaders: () => ({}), |
| 71 | + hubFetch: async () => { |
| 72 | + throw new Error('boom'); |
| 73 | + }, |
| 74 | + }); |
| 75 | + assert.equal(netFail.ok, false); |
| 76 | + assert.match(netFail.error, /boom/); |
| 77 | + }); |
| 78 | + |
| 79 | + it('rejects missing identity without calling hubFetch', async () => { |
| 80 | + let called = false; |
| 81 | + const result = await reportSkillInstallSuccess({ |
| 82 | + hubUrl: 'https://evomap.ai', |
| 83 | + skillId: 'skill_x', |
| 84 | + nodeId: '', |
| 85 | + buildHeaders: () => ({}), |
| 86 | + hubFetch: async () => { |
| 87 | + called = true; |
| 88 | + return { ok: true, status: 200, text: async () => '{}' }; |
| 89 | + }, |
| 90 | + }); |
| 91 | + assert.equal(result.ok, false); |
| 92 | + assert.equal(result.error, 'no_node_id'); |
| 93 | + assert.equal(called, false); |
| 94 | + }); |
| 95 | +}); |
| 96 | + |
| 97 | +describe('fetch command wires install-success after disk commit', () => { |
| 98 | + const indexSrc = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8'); |
| 99 | + |
| 100 | + it('requires skillInstallSuccess and reports only after local write', () => { |
| 101 | + assert.ok( |
| 102 | + /require\('\.\/src\/gep\/skillInstallSuccess'\)/.test(indexSrc), |
| 103 | + 'fetch must load skillInstallSuccess helper', |
| 104 | + ); |
| 105 | + assert.ok( |
| 106 | + /reportSkillInstallSuccess\(/.test(indexSrc), |
| 107 | + 'fetch must call reportSkillInstallSuccess after commit', |
| 108 | + ); |
| 109 | + assert.ok( |
| 110 | + /skillInstallCommitted\(/.test(indexSrc), |
| 111 | + 'fetch must gate report on skillInstallCommitted', |
| 112 | + ); |
| 113 | + const downloadIdx = indexSrc.indexOf("/a2a/skill/store/' + encodeURIComponent(skillId) + '/download'"); |
| 114 | + const reportIdx = indexSrc.indexOf('reportSkillInstallSuccess('); |
| 115 | + assert.ok(downloadIdx !== -1 && reportIdx !== -1 && reportIdx > downloadIdx, |
| 116 | + 'install-success must run after download path, not replace it'); |
| 117 | + }); |
| 118 | +}); |
0 commit comments