mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
buildAssistPrompt(action, selection) returns the {system, messages}
payload for rewrite/shorten/expand. Enforces 8KB selection cap and
typed errors with codes (unknown_action, bad_selection,
empty_selection, selection_too_large) so the IPC layer can map them
to user-friendly messages.
applyAssistResult(original, newText) trims the response and rejects
empty/unchanged payloads so a no-op streaming chunk doesn't churn
the editor.
15 new tests cover action coverage, prompt content, all four error
codes, cap-edge (exactly 8KB OK, 8KB+1 rejected), and trim semantics.
Amit Haridas
129 lines
4.0 KiB
JavaScript
129 lines
4.0 KiB
JavaScript
/**
|
|
* @jest-environment node
|
|
*
|
|
* Inline AI assist pure module.
|
|
*/
|
|
|
|
const {
|
|
buildAssistPrompt,
|
|
applyAssistResult,
|
|
ACTIONS,
|
|
ACTIONS_LIST,
|
|
MAX_SELECTION_CHARS,
|
|
} = require('../src/ai-assist/inline-assist');
|
|
|
|
describe('ACTIONS', () => {
|
|
test('the three actions are exposed', () => {
|
|
expect(ACTIONS_LIST).toEqual(['rewrite', 'shorten', 'expand']);
|
|
expect(ACTIONS.rewrite.label).toBe('Rewrite');
|
|
expect(ACTIONS.shorten.label).toBe('Shorten');
|
|
expect(ACTIONS.expand.label).toBe('Expand');
|
|
});
|
|
|
|
test('every action has a non-empty system prompt', () => {
|
|
for (const a of ACTIONS_LIST) {
|
|
expect(typeof ACTIONS[a].systemPrompt).toBe('string');
|
|
expect(ACTIONS[a].systemPrompt.length).toBeGreaterThan(20);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('buildAssistPrompt', () => {
|
|
test('returns {system, messages} for each action', () => {
|
|
for (const action of ACTIONS_LIST) {
|
|
const out = buildAssistPrompt(action, 'hello world');
|
|
expect(out.system).toBe(ACTIONS[action].systemPrompt);
|
|
expect(out.messages).toHaveLength(1);
|
|
expect(out.messages[0].role).toBe('user');
|
|
expect(out.messages[0].content).toBe('hello world');
|
|
}
|
|
});
|
|
|
|
test('preserves the exact selection content', () => {
|
|
const selection = 'A\nmultiline\nselection with unicode éàü ✓ and trailing spaces ';
|
|
const out = buildAssistPrompt('rewrite', selection);
|
|
expect(out.messages[0].content).toBe(selection);
|
|
});
|
|
|
|
test('throws on unknown action with unknown_action code', () => {
|
|
try {
|
|
buildAssistPrompt('polish', 'hi');
|
|
throw new Error('expected to throw');
|
|
} catch (err) {
|
|
expect(err.code).toBe('unknown_action');
|
|
}
|
|
});
|
|
|
|
test('throws on non-string selection with bad_selection code', () => {
|
|
try {
|
|
buildAssistPrompt('rewrite', null);
|
|
throw new Error('expected to throw');
|
|
} catch (err) {
|
|
expect(err.code).toBe('bad_selection');
|
|
}
|
|
try {
|
|
buildAssistPrompt('rewrite', 42);
|
|
throw new Error('expected to throw');
|
|
} catch (err) {
|
|
expect(err.code).toBe('bad_selection');
|
|
}
|
|
});
|
|
|
|
test('throws on empty selection with empty_selection code', () => {
|
|
try {
|
|
buildAssistPrompt('rewrite', '');
|
|
throw new Error('expected to throw');
|
|
} catch (err) {
|
|
expect(err.code).toBe('empty_selection');
|
|
}
|
|
});
|
|
|
|
test('throws on oversized selection with selection_too_large code', () => {
|
|
const huge = 'a'.repeat(MAX_SELECTION_CHARS + 1);
|
|
try {
|
|
buildAssistPrompt('rewrite', huge);
|
|
throw new Error('expected to throw');
|
|
} catch (err) {
|
|
expect(err.code).toBe('selection_too_large');
|
|
}
|
|
});
|
|
|
|
test('accepts a selection at exactly the cap', () => {
|
|
const exact = 'a'.repeat(MAX_SELECTION_CHARS);
|
|
expect(() => buildAssistPrompt('rewrite', exact)).not.toThrow();
|
|
});
|
|
|
|
test('MAX_SELECTION_CHARS is 8KB', () => {
|
|
expect(MAX_SELECTION_CHARS).toBe(8 * 1024);
|
|
});
|
|
});
|
|
|
|
describe('applyAssistResult', () => {
|
|
test('returns the trimmed new text when different', () => {
|
|
expect(applyAssistResult('hello', 'HELLO')).toBe('HELLO');
|
|
expect(applyAssistResult('hello', ' goodbye ')).toBe('goodbye');
|
|
});
|
|
|
|
test('returns null when newText is unchanged from original (after trim)', () => {
|
|
expect(applyAssistResult('hello', 'hello')).toBeNull();
|
|
expect(applyAssistResult('hello', ' hello ')).toBeNull();
|
|
});
|
|
|
|
test('returns null when newText is empty or whitespace-only', () => {
|
|
expect(applyAssistResult('hello', '')).toBeNull();
|
|
expect(applyAssistResult('hello', ' ')).toBeNull();
|
|
expect(applyAssistResult('hello', '\n\t ')).toBeNull();
|
|
});
|
|
|
|
test('returns null when newText is not a string', () => {
|
|
expect(applyAssistResult('hello', null)).toBeNull();
|
|
expect(applyAssistResult('hello', undefined)).toBeNull();
|
|
expect(applyAssistResult('hello', 42)).toBeNull();
|
|
});
|
|
|
|
test('trims surrounding whitespace before comparing/applying', () => {
|
|
// Even if the LLM adds stray newlines, we strip them before writing
|
|
expect(applyAssistResult('x', '\n\nresult\n\n')).toBe('result');
|
|
});
|
|
});
|