Files
clide/test/builtin/claude/at_commands_test.dart
jpmschweitzerandClaude a0636c2e5a add the team chat inbox over the broker
Renders broker traffic as a chat timeline and makes the user a first-class
participant. The broker grows a Stream<TeamMessage> and a recipient field,
auto-registers a virtual `user` member, and gains sendAsUser. A Flutter-free
TeamChatModel (owned by the orchestrator) accumulates the feed and exposes
postAsUser with @-routing (a new at_commands helper mirroring slash) and an
optional interrupt that cancels the target's turn before delivery. One model
backs two surfaces: a compact cockpit widget that pops out into a full
workspace chat pane. CLI parity via clide.team-chat.open / .post.

T-180.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-31 10:49:04 +02:00

159 lines
5.1 KiB
Dart

/// Unit tests for the @-name completion helpers (T-180, at_commands.dart).
library;
import 'package:clide/builtin/claude/src/at_commands.dart';
import 'package:test/test.dart';
void main() {
group('activeAtQuery', () {
test('returns null when text is empty', () {
expect(activeAtQuery('', 0), isNull);
});
test('returns null when cursor is at a non-@ run', () {
expect(activeAtQuery('hello world', 5), isNull);
});
test('detects @name at start of text', () {
const text = '@tyre';
final q = activeAtQuery(text, text.length);
expect(q, isNotNull);
expect(q!.start, 0);
expect(q.query, 'tyre');
});
test('detects @name after whitespace', () {
const text = 'hi @tyre';
final q = activeAtQuery(text, text.length);
expect(q, isNotNull);
expect(q!.start, 3);
expect(q.query, 'tyre');
});
test('returns null when @ is mid-word (e.g. email address)', () {
const text = 'user@example.com';
final q = activeAtQuery(text, text.length);
expect(q, isNull);
});
test('returns empty query when cursor is right after @', () {
const text = '@';
final q = activeAtQuery(text, 1);
expect(q, isNotNull);
expect(q!.query, '');
});
test('partial name is captured up to the cursor', () {
const text = '@ty';
final q = activeAtQuery(text, 3);
expect(q!.query, 'ty');
});
test('returns null after a space follows the tag (token complete)', () {
const text = '@tyre ';
// Cursor is past the space — no active @-token.
final q = activeAtQuery(text, text.length);
expect(q, isNull);
});
});
group('filterAtNames', () {
const names = ['lead', 'tyre', 'qatux'];
test('empty query returns team first then all names sorted', () {
final results = filterAtNames('', names);
expect(results.first, 'team');
// The rest are alphabetically sorted names.
expect(results.sublist(1).toSet(), containsAll(names));
});
test('query prefix filters names case-insensitively', () {
final results = filterAtNames('t', names);
expect(results, containsAll(['team', 'tyre']));
expect(results, isNot(contains('lead')));
expect(results, isNot(contains('qatux')));
});
test('team alias is always included when query matches', () {
final results = filterAtNames('te', names);
expect(results, contains('team'));
});
test('team alias is excluded when query does not match', () {
final results = filterAtNames('z', names);
expect(results, isNot(contains('team')));
});
test('de-duplicates names', () {
final results = filterAtNames('', ['lead', 'lead']);
expect(results.where((n) => n == 'lead'), hasLength(1));
});
test('caps results at limit', () {
final manyNames = List.generate(20, (i) => 'member$i');
final results = filterAtNames('', manyNames, limit: 5);
expect(results.length, lessThanOrEqualTo(5));
});
test('team is pinned first even when other names sort before it', () {
final results = filterAtNames('', ['alpha', 'beta']);
expect(results.first, 'team');
});
});
group('completeAt', () {
test('replaces the @ token with the selected name + trailing space', () {
// Cursor is right after "ty"; the space + rest of line come after.
// replaceRange(0, 3, '@tyre ') over '@ty rest' gives '@tyre rest'
// (keeps the existing space). That is the correct contract — the
// completion inserts its own trailing space; callers clear the space
// if they want to avoid double spacing.
const text = '@ty rest';
final q = activeAtQuery(text, 3)!; // cursor after "ty"
final result = completeAt(text, q, 'tyre');
expect(result.text, '@tyre rest'); // double-space: inserted + existing
expect(result.cursor, '@tyre '.length);
});
test('works at the start of the text', () {
const text = '@';
final q = activeAtQuery(text, 1)!;
final result = completeAt(text, q, 'lead');
expect(result.text, '@lead ');
expect(result.cursor, '@lead '.length);
});
});
group('parseAtTag', () {
test('no leading @ returns null recipient and full text as body', () {
final r = parseAtTag('hello world');
expect(r.recipient, isNull);
expect(r.body, 'hello world');
});
test('leading @name splits into recipient + body', () {
final r = parseAtTag('@tyre pick up T-9');
expect(r.recipient, 'tyre');
expect(r.body, 'pick up T-9');
});
test('@team normalises to null recipient (broadcast)', () {
final r = parseAtTag('@team hello everyone');
expect(r.recipient, isNull);
expect(r.body, 'hello everyone');
});
test('just @name with no body', () {
final r = parseAtTag('@lead');
expect(r.recipient, 'lead');
expect(r.body, '');
});
test('leading whitespace is trimmed before @-tag parsing', () {
final r = parseAtTag(' @tyre hello');
expect(r.recipient, 'tyre');
expect(r.body, 'hello');
});
});
}