@hanhnd/agent-kit 1.0.41 → 1.0.43
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.
- package/dist/cli.js +11 -10
- package/dist/mcp/confluence.test.d.ts +1 -0
- package/dist/mcp/confluence.test.js +258 -0
- package/dist/mcp/integration.d.ts +44 -2
- package/dist/mcp/integration.js +166 -9
- package/dist/mcp/jira.test.d.ts +1 -0
- package/dist/mcp/jira.test.js +37 -0
- package/dist/mcp/registration.test.js +12 -1
- package/dist/server.js +625 -27
- package/dist/services/digest/digest-llama-local.test.js +58 -15
- package/dist/services/digest/model-registry.js +9 -9
- package/dist/services/digest/providers/llama-local.d.ts +3 -0
- package/dist/services/digest/providers/llama-local.js +4 -3
- package/dist/services/integration/credentials.d.ts +1 -1
- package/dist/services/integration/credentials.js +1 -1
- package/dist/services/integration/parser/storage.d.ts +18 -0
- package/dist/services/integration/parser/storage.js +508 -0
- package/dist/services/integration/parser/storage.test.d.ts +1 -0
- package/dist/services/integration/parser/storage.test.js +211 -0
- package/package.json +1 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
import { DEPTH_TRUNCATION_MARKER, MAX_STORAGE_DEPTH, storageToMarkdown } from './storage.js';
|
|
4
|
+
describe('storageToMarkdown — never-throw guarantee (BC12)', () => {
|
|
5
|
+
test('S1: empty string returns a string without throwing', () => {
|
|
6
|
+
const output = storageToMarkdown('');
|
|
7
|
+
assert.equal(typeof output, 'string');
|
|
8
|
+
assert.equal(output, '');
|
|
9
|
+
});
|
|
10
|
+
test('S2: null and undefined return a string without throwing', () => {
|
|
11
|
+
assert.equal(storageToMarkdown(null), '');
|
|
12
|
+
assert.equal(storageToMarkdown(undefined), '');
|
|
13
|
+
});
|
|
14
|
+
test('S3: unbalanced tags do not throw and text survives', () => {
|
|
15
|
+
const output = storageToMarkdown('<p><strong>hi</p>');
|
|
16
|
+
assert.ok(output.includes('hi'), `expected "hi" in output, got: ${JSON.stringify(output)}`);
|
|
17
|
+
});
|
|
18
|
+
test('S4: unterminated CDATA does not throw and the tail is not swallowed', () => {
|
|
19
|
+
const output = storageToMarkdown('<ac:plain-text-body><![CDATA[abc');
|
|
20
|
+
assert.ok(output.includes('abc'), `expected "abc" in output, got: ${JSON.stringify(output)}`);
|
|
21
|
+
});
|
|
22
|
+
test('S5: stray angle brackets terminate without an infinite loop', () => {
|
|
23
|
+
assert.equal(typeof storageToMarkdown('<'), 'string');
|
|
24
|
+
assert.equal(typeof storageToMarkdown('>'), 'string');
|
|
25
|
+
assert.equal(storageToMarkdown('<<>>'), '<<>>');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('storageToMarkdown — unhandled markup is visible, never dropped (BC13)', () => {
|
|
29
|
+
test('S6: unknown macro renders a visible placeholder', () => {
|
|
30
|
+
const output = storageToMarkdown('<ac:structured-macro ac:name="chart"><ac:parameter ac:name="type">pie</ac:parameter></ac:structured-macro>');
|
|
31
|
+
assert.ok(output.includes('[macro: chart]'), `expected "[macro: chart]", got: ${JSON.stringify(output)}`);
|
|
32
|
+
});
|
|
33
|
+
test('S7: unknown non-macro tag still renders its children', () => {
|
|
34
|
+
const output = storageToMarkdown('<foo>visible text</foo>');
|
|
35
|
+
assert.ok(output.includes('visible text'), `children must never be dropped, got: ${JSON.stringify(output)}`);
|
|
36
|
+
});
|
|
37
|
+
test('S8: macro with no ac:name renders [macro: unknown]', () => {
|
|
38
|
+
const output = storageToMarkdown('<ac:structured-macro>body</ac:structured-macro>');
|
|
39
|
+
assert.ok(output.includes('[macro: unknown]'), `expected "[macro: unknown]", got: ${JSON.stringify(output)}`);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('storageToMarkdown — code blocks and CDATA are verbatim (BC14)', () => {
|
|
43
|
+
const CDATA_BODY = '<div>if (a < b) { return "x & y"; }</div>';
|
|
44
|
+
test('S9: code macro fences with its language and keeps CDATA byte-for-byte', () => {
|
|
45
|
+
const output = storageToMarkdown(`<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">ts</ac:parameter>` +
|
|
46
|
+
`<ac:plain-text-body><![CDATA[${CDATA_BODY}]]></ac:plain-text-body></ac:structured-macro>`);
|
|
47
|
+
assert.ok(output.startsWith('```ts\n'), `expected a \`\`\`ts fence, got: ${JSON.stringify(output)}`);
|
|
48
|
+
assert.ok(output.includes(CDATA_BODY), 'CDATA payload must appear verbatim');
|
|
49
|
+
// MUST NOT: entities inside CDATA are not decoded, and <div> was not parsed as a tag.
|
|
50
|
+
assert.ok(!output.includes('"x & y"'), '& inside CDATA must not be decoded');
|
|
51
|
+
assert.ok(output.includes('<div>'), '<div> inside CDATA must not be consumed as markup');
|
|
52
|
+
});
|
|
53
|
+
test('S10: pre renders a fenced block with no language', () => {
|
|
54
|
+
const output = storageToMarkdown('<pre>x</pre>');
|
|
55
|
+
assert.equal(output, '```\nx\n```');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('storageToMarkdown — lists nest rather than flatten (BC15)', () => {
|
|
59
|
+
test('S11: ul inside li is indented by exactly 2 spaces and not flattened', () => {
|
|
60
|
+
const output = storageToMarkdown('<ul><li>a<ul><li>b</li></ul></li></ul>');
|
|
61
|
+
assert.ok(output.includes('- a'), `expected "- a", got: ${JSON.stringify(output)}`);
|
|
62
|
+
assert.ok(output.includes('\n - b'), `expected "b" indented 2 spaces, got: ${JSON.stringify(output)}`);
|
|
63
|
+
// MUST NOT: flattened to the same level.
|
|
64
|
+
assert.ok(!output.includes('\n- b'), `"b" must not sit at the outer level, got: ${JSON.stringify(output)}`);
|
|
65
|
+
});
|
|
66
|
+
test('S12: ol renders numbered bullets', () => {
|
|
67
|
+
const output = storageToMarkdown('<ol><li>a</li><li>b</li></ol>');
|
|
68
|
+
assert.equal(output, '1. a\n1. b');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('storageToMarkdown — GFM tables (BC16)', () => {
|
|
72
|
+
test('S13: th-headed table emits a separator row and escapes cell pipes', () => {
|
|
73
|
+
const output = storageToMarkdown('<table><tbody><tr><th>Key</th><th>Value</th></tr><tr><td>a|b</td><td>c</td></tr></tbody></table>');
|
|
74
|
+
assert.ok(output.includes('| Key | Value |'), `expected a header row, got: ${JSON.stringify(output)}`);
|
|
75
|
+
assert.ok(output.includes('| --- | --- |'), `expected one --- per header cell, got: ${JSON.stringify(output)}`);
|
|
76
|
+
assert.ok(output.includes('| a\\|b | c |'), `expected the cell pipe escaped, got: ${JSON.stringify(output)}`);
|
|
77
|
+
});
|
|
78
|
+
test('S14: table with no rows renders empty without throwing', () => {
|
|
79
|
+
assert.equal(storageToMarkdown('<table></table>'), '');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('storageToMarkdown — entity decoding outside CDATA (BC17)', () => {
|
|
83
|
+
test('S15: named entities decode and the raw sequences disappear', () => {
|
|
84
|
+
const output = storageToMarkdown('<p>A&B C <tag> "q" 'a'</p>');
|
|
85
|
+
assert.ok(output.includes('A&B'), `& must decode, got: ${JSON.stringify(output)}`);
|
|
86
|
+
assert.ok(output.includes('<tag>'), '</> must decode');
|
|
87
|
+
assert.ok(output.includes('"q"'), '" must decode');
|
|
88
|
+
assert.ok(output.includes("'a'"), '' must decode');
|
|
89
|
+
assert.ok(output.includes('B\u00A0C'), ` must decode to U+00A0, got: ${JSON.stringify(output)}`);
|
|
90
|
+
for (const raw of [' ', '&', '<', '>', '"', ''']) {
|
|
91
|
+
assert.ok(!output.includes(raw), `raw entity ${raw} must not survive, got: ${JSON.stringify(output)}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
test('S16: decimal and hexadecimal numeric references both decode', () => {
|
|
95
|
+
const output = storageToMarkdown('<p>— and —</p>');
|
|
96
|
+
assert.equal(output, '— and —');
|
|
97
|
+
assert.ok(!output.includes('—'), 'raw decimal reference must not survive');
|
|
98
|
+
assert.ok(!output.includes('—'), 'raw hexadecimal reference must not survive');
|
|
99
|
+
});
|
|
100
|
+
test('S17: unrecognised entity-shaped text passes through unchanged', () => {
|
|
101
|
+
assert.equal(storageToMarkdown('<p>&fooz;</p>'), '&fooz;');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('storageToMarkdown — depth cap (BC18)', () => {
|
|
105
|
+
test('S18: nesting past MAX_STORAGE_DEPTH truncates visibly without a RangeError', () => {
|
|
106
|
+
const depth = MAX_STORAGE_DEPTH + 50;
|
|
107
|
+
const xhtml = '<div>'.repeat(depth) + 'deep' + '</div>'.repeat(depth);
|
|
108
|
+
const output = storageToMarkdown(xhtml);
|
|
109
|
+
assert.ok(output.includes(DEPTH_TRUNCATION_MARKER), `expected the truncation marker, got: ${JSON.stringify(output.slice(0, 200))}`);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('storageToMarkdown — standard and Confluence constructs (BC3)', () => {
|
|
113
|
+
test('S19: headings, inline marks and block constructs map per the mapping table', () => {
|
|
114
|
+
assert.equal(storageToMarkdown('<h1>a</h1>'), '# a');
|
|
115
|
+
assert.equal(storageToMarkdown('<h2>a</h2>'), '## a');
|
|
116
|
+
assert.equal(storageToMarkdown('<h3>a</h3>'), '### a');
|
|
117
|
+
assert.equal(storageToMarkdown('<h4>a</h4>'), '#### a');
|
|
118
|
+
assert.equal(storageToMarkdown('<h5>a</h5>'), '##### a');
|
|
119
|
+
assert.equal(storageToMarkdown('<h6>a</h6>'), '###### a');
|
|
120
|
+
assert.equal(storageToMarkdown('<p><strong>a</strong></p>'), '**a**');
|
|
121
|
+
assert.equal(storageToMarkdown('<p><em>a</em></p>'), '*a*');
|
|
122
|
+
assert.equal(storageToMarkdown('<p><code>a</code></p>'), '`a`');
|
|
123
|
+
assert.equal(storageToMarkdown('<p><s>a</s></p>'), '~~a~~');
|
|
124
|
+
assert.equal(storageToMarkdown('<p><u>a</u></p>'), 'a');
|
|
125
|
+
assert.equal(storageToMarkdown('<p><sup>a</sup></p>'), '^a^');
|
|
126
|
+
assert.equal(storageToMarkdown('<p><sub>a</sub></p>'), '~a~');
|
|
127
|
+
assert.equal(storageToMarkdown('<blockquote><p>a</p></blockquote>'), '> a');
|
|
128
|
+
assert.equal(storageToMarkdown('<hr/>'), '---');
|
|
129
|
+
assert.equal(storageToMarkdown('<p>a<br/>b</p>'), 'a\nb');
|
|
130
|
+
assert.equal(storageToMarkdown('<p><a href="https://x.test/y">link</a></p>'), '[link](https://x.test/y)');
|
|
131
|
+
});
|
|
132
|
+
test('S20: panel, expand and jira macros render their known shapes', () => {
|
|
133
|
+
const info = storageToMarkdown('<ac:structured-macro ac:name="info"><ac:rich-text-body><p>heads up</p></ac:rich-text-body></ac:structured-macro>');
|
|
134
|
+
assert.equal(info, '> **INFO**\n> heads up');
|
|
135
|
+
const warning = storageToMarkdown('<ac:structured-macro ac:name="warning"><ac:rich-text-body><p>careful</p></ac:rich-text-body></ac:structured-macro>');
|
|
136
|
+
assert.equal(warning, '> **WARNING**\n> careful');
|
|
137
|
+
const expand = storageToMarkdown('<ac:structured-macro ac:name="expand"><ac:parameter ac:name="title">More</ac:parameter>' +
|
|
138
|
+
'<ac:rich-text-body><p>hidden</p></ac:rich-text-body></ac:structured-macro>');
|
|
139
|
+
assert.equal(expand, '**More**\n\nhidden');
|
|
140
|
+
const jira = storageToMarkdown('<ac:structured-macro ac:name="jira"><ac:parameter ac:name="key">PROJ-123</ac:parameter></ac:structured-macro>');
|
|
141
|
+
assert.equal(jira, '`PROJ-123`');
|
|
142
|
+
});
|
|
143
|
+
test('S21: Confluence links, images, tasks, emoticons and layouts render', () => {
|
|
144
|
+
const pageLink = storageToMarkdown('<ac:link><ri:page ri:content-title="Design Notes" /></ac:link>');
|
|
145
|
+
assert.ok(pageLink.includes('Design Notes'), `expected the page title as link text, got: ${pageLink}`);
|
|
146
|
+
const attachmentLink = storageToMarkdown('<ac:link><ri:attachment ri:filename="spec.pdf" /></ac:link>');
|
|
147
|
+
assert.equal(attachmentLink, '[attachment: spec.pdf]');
|
|
148
|
+
const image = storageToMarkdown('<ac:image><ri:attachment ri:filename="diagram.png" /></ac:image>');
|
|
149
|
+
assert.equal(image, '[image: diagram.png]');
|
|
150
|
+
const tasks = storageToMarkdown('<ac:task-list>' +
|
|
151
|
+
'<ac:task><ac:task-status>incomplete</ac:task-status><ac:task-body>todo</ac:task-body></ac:task>' +
|
|
152
|
+
'<ac:task><ac:task-status>complete</ac:task-status><ac:task-body>done</ac:task-body></ac:task>' +
|
|
153
|
+
'</ac:task-list>');
|
|
154
|
+
assert.equal(tasks, '- [ ] todo\n- [x] done');
|
|
155
|
+
assert.equal(storageToMarkdown('<p><ac:emoticon ac:name="smile" /></p>'), ':smile:');
|
|
156
|
+
const layout = storageToMarkdown('<ac:layout-section><ac:layout-cell><p>inside</p></ac:layout-cell></ac:layout-section>');
|
|
157
|
+
assert.equal(layout, 'inside');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('storageToMarkdown — large body completes (F12)', () => {
|
|
161
|
+
test('S22: a ~300 KB mixed body converts to a non-empty string', () => {
|
|
162
|
+
const block = '<h2>Section</h2><p>Some <strong>bold</strong> and <em>italic</em> text with & entities.</p>' +
|
|
163
|
+
'<ul><li>one<ul><li>nested</li></ul></li><li>two</li></ul>' +
|
|
164
|
+
'<table><tbody><tr><th>K</th><th>V</th></tr><tr><td>a</td><td>b|c</td></tr></tbody></table>' +
|
|
165
|
+
'<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">ts</ac:parameter>' +
|
|
166
|
+
'<ac:plain-text-body><![CDATA[const x: number = 1; // < > &]]></ac:plain-text-body></ac:structured-macro>' +
|
|
167
|
+
'<ac:structured-macro ac:name="chart"/>';
|
|
168
|
+
const xhtml = block.repeat(Math.ceil(300_000 / block.length));
|
|
169
|
+
assert.ok(xhtml.length >= 300_000, 'fixture must be at least 300 KB');
|
|
170
|
+
const output = storageToMarkdown(xhtml);
|
|
171
|
+
assert.ok(output.length > 0, 'conversion must produce output');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('storageToMarkdown — extended HTML named entities (BC17)', () => {
|
|
175
|
+
test('S23: common HTML entities decode and the raw sequences disappear', () => {
|
|
176
|
+
const output = storageToMarkdown('<p>A → B ← C — D – E … © ® ™ ° × € ‘x’ “y”</p>');
|
|
177
|
+
for (const [entity, char] of [
|
|
178
|
+
['→', '→'],
|
|
179
|
+
['←', '←'],
|
|
180
|
+
['—', '—'],
|
|
181
|
+
['–', '–'],
|
|
182
|
+
['…', '…'],
|
|
183
|
+
['©', '©'],
|
|
184
|
+
['®', '®'],
|
|
185
|
+
['™', '™'],
|
|
186
|
+
['°', '°'],
|
|
187
|
+
['×', '×'],
|
|
188
|
+
['€', '€'],
|
|
189
|
+
]) {
|
|
190
|
+
assert.ok(output.includes(char), `${entity} must decode to ${char}, got: ${JSON.stringify(output)}`);
|
|
191
|
+
assert.ok(!output.includes(entity), `raw ${entity} must not survive`);
|
|
192
|
+
}
|
|
193
|
+
assert.ok(output.includes('‘x’'), 'smart single quotes must decode');
|
|
194
|
+
assert.ok(output.includes('“y”'), 'smart double quotes must decode');
|
|
195
|
+
});
|
|
196
|
+
test('S24: case-sensitive entity names are respected', () => {
|
|
197
|
+
// Prime vs prime differ; a case-insensitive map would collapse them.
|
|
198
|
+
assert.equal(storageToMarkdown('<p>″ ′</p>'), '″ ′');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
describe('storageToMarkdown — block-level images sit on their own line', () => {
|
|
202
|
+
test('S25: an image between two blocks is separated, not glued to the next block', () => {
|
|
203
|
+
const output = storageToMarkdown('<ac:image><ri:attachment ri:filename="a.png" /></ac:image><p>after</p>');
|
|
204
|
+
assert.equal(output, '[image: a.png]\n\nafter');
|
|
205
|
+
assert.ok(!output.includes(']after'), 'the image marker must not be glued to the following text');
|
|
206
|
+
});
|
|
207
|
+
test('S26: an image inside a paragraph stays inline', () => {
|
|
208
|
+
const output = storageToMarkdown('<p>before <ac:image><ri:attachment ri:filename="a.png" /></ac:image> after</p>');
|
|
209
|
+
assert.equal(output, 'before [image: a.png] after');
|
|
210
|
+
});
|
|
211
|
+
});
|