* feat(chat): render code comments as cards instead of fenced text * fix(vscode): route Add Comment to the active session editor panel * fix(chat): persist queued inline comments and tighten file-chip path matching * feat(vscode): comment on code from the editor * fix(chat): keep attached context in the message and broadcast comment removal * fix(vscode): hold every pending comment and gate both entry points on the workspace * fix(vscode): let only the owning surface decide its comment threads * fix(vscode): drop a comment removed while its delivery was still in flight * test(vscode): cover the in-flight comment removal guard * test(vscode): cover comment removal reaching every chat surface * fix(vscode): give up on a comment the chat never confirmed holding * fix(vscode): retract a comment everywhere before reporting it discarded * fix(chat): preserve queued comment cards * fix: preserve inline comment context across send paths * fix(chat): preserve command routing with context * fix(chat): keep unavailable actions on normal send path --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import { useInlineCommentDraftStore } from './useInlineCommentDraftStore';
|
|
|
|
const comment = {
|
|
source: 'file' as const,
|
|
fileLabel: 'src/app.ts:12',
|
|
startLine: 12,
|
|
endLine: 12,
|
|
code: 'const x = 1',
|
|
language: 'typescript',
|
|
text: 'fix this',
|
|
};
|
|
const target = { directory: '/repo', sessionKey: 'session-1' };
|
|
|
|
const store = () => useInlineCommentDraftStore.getState();
|
|
|
|
describe('caller-provided draft ids', () => {
|
|
afterEach(() => { useInlineCommentDraftStore.setState({ drafts: {}, touchedAt: {} }); });
|
|
|
|
test('a caller that owns its own view of the draft chooses the id', () => {
|
|
// The VS Code editor thread mints the id so it can track its draft without
|
|
// waiting for a round trip.
|
|
const id = store().addDraft(target, { ...comment, id: 'icd-editor-thread' });
|
|
expect(id).toBe('icd-editor-thread');
|
|
expect(store().getDrafts(target)[0].id).toBe('icd-editor-thread');
|
|
});
|
|
|
|
test('the chosen id is what removal and lookup accept', () => {
|
|
store().addDraft(target, { ...comment, id: 'icd-editor-thread' });
|
|
store().removeDraft(target, 'icd-editor-thread');
|
|
expect(store().getDrafts(target)).toEqual([]);
|
|
});
|
|
|
|
test('omitting the id still generates one', () => {
|
|
const id = store().addDraft(target, comment);
|
|
expect(/^icd-\d+-\w+$/.test(id ?? '')).toBe(true);
|
|
});
|
|
|
|
test('a blank id is ignored rather than stored', () => {
|
|
const id = store().addDraft(target, { ...comment, id: ' ' });
|
|
expect(/^icd-\d+-\w+$/.test(id ?? '')).toBe(true);
|
|
});
|
|
|
|
test('a colliding id is refused, so edits cannot retarget another draft', () => {
|
|
const first = store().addDraft(target, { ...comment, id: 'icd-duplicate' });
|
|
const second = store().addDraft(target, { ...comment, id: 'icd-duplicate', text: 'different' });
|
|
|
|
expect(first).toBe('icd-duplicate');
|
|
expect(second).not.toBe('icd-duplicate');
|
|
|
|
const drafts = store().getDrafts(target);
|
|
expect(drafts).toHaveLength(2);
|
|
expect(new Set(drafts.map((draft) => draft.id)).size).toBe(2);
|
|
});
|
|
|
|
test('the same id may be reused once its draft is gone', () => {
|
|
store().addDraft(target, { ...comment, id: 'icd-reused' });
|
|
store().removeDraft(target, 'icd-reused');
|
|
expect(store().addDraft(target, { ...comment, id: 'icd-reused' })).toBe('icd-reused');
|
|
});
|
|
|
|
test('an id taken in another session does not collide', () => {
|
|
const other = { directory: '/repo', sessionKey: 'session-2' };
|
|
store().addDraft(target, { ...comment, id: 'icd-shared' });
|
|
expect(store().addDraft(other, { ...comment, id: 'icd-shared' })).toBe('icd-shared');
|
|
});
|
|
});
|