rename team lifecycle events to Joined/Left
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 28s
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 28s
TeamMemberBorn -> TeamMemberJoined, TeamMemberDied -> TeamMemberLeft (kinds member-joined/member-left). Less morbid and a better fit for teammates coming and going. No consumers yet, so a plain rename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2150,3 +2150,4 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
|
|||||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:41:28', '2026-05-22 22:41:28', '2026-05-22 22:41:28', NULL, 'eda6f75e88401ee5b445e7edfc312f82', 1) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:41:28', '2026-05-22 22:41:28', '2026-05-22 22:41:28', NULL, 'eda6f75e88401ee5b445e7edfc312f82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'status', 'in_progress', 'done', NULL, '2026-05-22 22:43:45', '2026-05-22 22:43:45', '2026-05-22 22:43:45', NULL, 'da17b2710e17b1821882d47724790cbb', 1) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'status', 'in_progress', 'done', NULL, '2026-05-22 22:43:45', '2026-05-22 22:43:45', '2026-05-22 22:43:45', NULL, 'da17b2710e17b1821882d47724790cbb', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-139', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:50:41', '2026-05-22 22:50:41', '2026-05-22 22:50:41', NULL, 'ad063f0c3c65e5b35d3dcea036412ad9', 1) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-139', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:50:41', '2026-05-22 22:50:41', '2026-05-22 22:50:41', NULL, 'ad063f0c3c65e5b35d3dcea036412ad9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-139', 'status', 'in_progress', 'done', NULL, '2026-05-22 23:16:28', '2026-05-22 23:16:28', '2026-05-22 23:16:28', NULL, 'add82dc118e2081500f19cbb321272d4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
|||||||
@@ -2495,3 +2495,4 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority,
|
|||||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'task', 'T-132', 'Claude message card with brand-orange accent', 'Extend the distinct-card treatment (T-143) to Claude''s text responses with a different accent: Claude''s brand coral-orange (#D97757), vs the user card''s focus-colour stripe. Refactor _userCard into a shared _messageCard(label, accent, body). conversation_view.dart.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-22 22:40:42', '2026-05-22 22:41:28', NULL, '4c0eebba4edb4394fb47ba2d81ef42ad', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'task', 'T-132', 'Claude message card with brand-orange accent', 'Extend the distinct-card treatment (T-143) to Claude''s text responses with a different accent: Claude''s brand coral-orange (#D97757), vs the user card''s focus-colour stripe. Refactor _userCard into a shared _messageCard(label, accent, body). conversation_view.dart.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-22 22:40:42', '2026-05-22 22:41:28', NULL, '4c0eebba4edb4394fb47ba2d81ef42ad', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'task', 'T-132', 'Claude message card with brand-orange accent', 'Extend the distinct-card treatment (T-143) to Claude''s text responses with a different accent: Claude''s brand coral-orange (#D97757), vs the user card''s focus-colour stripe. Refactor _userCard into a shared _messageCard(label, accent, body). conversation_view.dart.', 'done', 'low', NULL, NULL, NULL, '2026-05-22 22:40:42', '2026-05-22 22:43:45', NULL, '1ce891624631502279b0356c5cdffae2', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'task', 'T-132', 'Claude message card with brand-orange accent', 'Extend the distinct-card treatment (T-143) to Claude''s text responses with a different accent: Claude''s brand coral-orange (#D97757), vs the user card''s focus-colour stripe. Refactor _userCard into a shared _messageCard(label, accent, body). conversation_view.dart.', 'done', 'low', NULL, NULL, NULL, '2026-05-22 22:40:42', '2026-05-22 22:43:45', NULL, '1ce891624631502279b0356c5cdffae2', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-139', 'task', 'T-132', 'tmux team observer (lifecycle + identity + per-teammate transcripts)', 'New lib/builtin/claude/src/team_observer.dart: detect teammate spawn/exit (control-mode or list-panes -a poll on -L clide), map pane->teammate via ~/.claude/teams config + agentId, spin a TranscriptReader per teammate, emit new TeamMemberBorn/TeamMemberDied ClideEvents (lib/kernel/src/events/types.dart). ALL fragile CC-internals parsing isolated here (single drift-containment point; key off the version field). Cite D-75. Acceptance: a real team run emits born/died with correct identity; per-teammate content streams. Blocked by T2, T4.', 'in_progress', 'high', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 22:50:41', NULL, 'a5f4b20a0a37332ab35de8fa07e22298', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-139', 'task', 'T-132', 'tmux team observer (lifecycle + identity + per-teammate transcripts)', 'New lib/builtin/claude/src/team_observer.dart: detect teammate spawn/exit (control-mode or list-panes -a poll on -L clide), map pane->teammate via ~/.claude/teams config + agentId, spin a TranscriptReader per teammate, emit new TeamMemberBorn/TeamMemberDied ClideEvents (lib/kernel/src/events/types.dart). ALL fragile CC-internals parsing isolated here (single drift-containment point; key off the version field). Cite D-75. Acceptance: a real team run emits born/died with correct identity; per-teammate content streams. Blocked by T2, T4.', 'in_progress', 'high', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 22:50:41', NULL, 'a5f4b20a0a37332ab35de8fa07e22298', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-139', 'task', 'T-132', 'tmux team observer (lifecycle + identity + per-teammate transcripts)', 'New lib/builtin/claude/src/team_observer.dart: detect teammate spawn/exit (control-mode or list-panes -a poll on -L clide), map pane->teammate via ~/.claude/teams config + agentId, spin a TranscriptReader per teammate, emit new TeamMemberBorn/TeamMemberDied ClideEvents (lib/kernel/src/events/types.dart). ALL fragile CC-internals parsing isolated here (single drift-containment point; key off the version field). Cite D-75. Acceptance: a real team run emits born/died with correct identity; per-teammate content streams. Blocked by T2, T4.', 'done', 'high', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 23:16:28', NULL, 'bd5e9cc5ae0e8bdbac4ad8cc57b33b58', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
/// `tmuxPaneId` (`%N`, empty for the lead), `name`, `agentType`, `model`,
|
/// `tmuxPaneId` (`%N`, empty for the lead), `name`, `agentType`, `model`,
|
||||||
/// `color`, `cwd`, `joinedAt`. Polling `tmux -L clide list-panes -a` and
|
/// `color`, `cwd`, `joinedAt`. Polling `tmux -L clide list-panes -a` and
|
||||||
/// correlating live pane ids with `tmuxPaneId` gives a dependable
|
/// correlating live pane ids with `tmuxPaneId` gives a dependable
|
||||||
/// born/died signal and full identity — no transcript needed.
|
/// joined/left signal and full identity — no transcript needed.
|
||||||
/// - **Fragile — per-teammate transcript join.** A teammate's transcript is
|
/// - **Fragile — per-teammate transcript join.** A teammate's transcript is
|
||||||
/// a subagent file `<munged-cwd>/<leadSessionId>/subagents/agent-<hex>.jsonl`
|
/// a subagent file `<munged-cwd>/<leadSessionId>/subagents/agent-<hex>.jsonl`
|
||||||
/// whose only ids are a random hex (the filename) and a `slug`; it carries
|
/// whose only ids are a random hex (the filename) and a `slug`; it carries
|
||||||
@@ -137,8 +137,8 @@ class _LiveMember {
|
|||||||
final TranscriptPublisher? publisher;
|
final TranscriptPublisher? publisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Watches a workspace's tmux team and emits [TeamMemberBorn] /
|
/// Watches a workspace's tmux team and emits [TeamMemberJoined] /
|
||||||
/// [TeamMemberDied] as panes appear/disappear, publishing each teammate's
|
/// [TeamMemberLeft] as panes appear/disappear, publishing each teammate's
|
||||||
/// transcript onto the [MessageBus] under its per-agent channel.
|
/// transcript onto the [MessageBus] under its per-agent channel.
|
||||||
class TeamObserver {
|
class TeamObserver {
|
||||||
TeamObserver({
|
TeamObserver({
|
||||||
@@ -200,7 +200,7 @@ class TeamObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One poll cycle (public for tests). Diffs the config roster against the
|
/// One poll cycle (public for tests). Diffs the config roster against the
|
||||||
/// live panes and emits born/died.
|
/// live panes and emits joined/left.
|
||||||
Future<void> tick() async {
|
Future<void> tick() async {
|
||||||
final config = await discoverTeam(workspacePath, teamsBase: _teamsBase);
|
final config = await discoverTeam(workspacePath, teamsBase: _teamsBase);
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
@@ -215,19 +215,19 @@ class TeamObserver {
|
|||||||
final paneLive = livePanes.contains(m.tmuxPaneId);
|
final paneLive = livePanes.contains(m.tmuxPaneId);
|
||||||
final tracked = _live.containsKey(m.agentId);
|
final tracked = _live.containsKey(m.agentId);
|
||||||
if (paneLive && !tracked) {
|
if (paneLive && !tracked) {
|
||||||
await _born(config, m);
|
await _joined(config, m);
|
||||||
} else if (!paneLive && tracked) {
|
} else if (!paneLive && tracked) {
|
||||||
await _died(m.agentId);
|
await _left(m.agentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A member dropped from the config (team reshaped) also counts as died.
|
// A member dropped from the config (team reshaped) also counts as left.
|
||||||
for (final id in _live.keys.toList()) {
|
for (final id in _live.keys.toList()) {
|
||||||
if (!configIds.contains(id)) await _died(id);
|
if (!configIds.contains(id)) await _left(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _born(TeamConfig config, TeamMember m) async {
|
Future<void> _joined(TeamConfig config, TeamMember m) async {
|
||||||
final path = await _resolveTranscript(config, m);
|
final path = await _resolveTranscript(config, m);
|
||||||
TranscriptPublisher? pub;
|
TranscriptPublisher? pub;
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
@@ -238,7 +238,7 @@ class TeamObserver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
_live[m.agentId] = _LiveMember(m, config.team, pub);
|
_live[m.agentId] = _LiveMember(m, config.team, pub);
|
||||||
_events.emit(TeamMemberBorn(
|
_events.emit(TeamMemberJoined(
|
||||||
team: config.team,
|
team: config.team,
|
||||||
agentId: m.agentId,
|
agentId: m.agentId,
|
||||||
name: m.name,
|
name: m.name,
|
||||||
@@ -251,16 +251,16 @@ class TeamObserver {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _died(String agentId) async {
|
Future<void> _left(String agentId) async {
|
||||||
final live = _live.remove(agentId);
|
final live = _live.remove(agentId);
|
||||||
if (live == null) return;
|
if (live == null) return;
|
||||||
await live.publisher?.dispose();
|
await live.publisher?.dispose();
|
||||||
_events.emit(TeamMemberDied(team: live.team, agentId: agentId, paneId: live.member.tmuxPaneId));
|
_events.emit(TeamMemberLeft(team: live.team, agentId: agentId, paneId: live.member.tmuxPaneId));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _killAll() async {
|
Future<void> _killAll() async {
|
||||||
for (final id in _live.keys.toList()) {
|
for (final id in _live.keys.toList()) {
|
||||||
await _died(id);
|
await _left(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ class DaemonEvent extends ClideEvent {
|
|||||||
/// taken from the team config, so it is reliable regardless of transcript
|
/// taken from the team config, so it is reliable regardless of transcript
|
||||||
/// drift (T-139, D-75). [transcriptPath] is the best-effort resolved
|
/// drift (T-139, D-75). [transcriptPath] is the best-effort resolved
|
||||||
/// subagent transcript, or null if it could not be joined yet.
|
/// subagent transcript, or null if it could not be joined yet.
|
||||||
class TeamMemberBorn extends ClideEvent {
|
class TeamMemberJoined extends ClideEvent {
|
||||||
const TeamMemberBorn({
|
const TeamMemberJoined({
|
||||||
required this.team,
|
required this.team,
|
||||||
required this.agentId,
|
required this.agentId,
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -142,7 +142,7 @@ class TeamMemberBorn extends ClideEvent {
|
|||||||
@override
|
@override
|
||||||
String get subsystem => 'team';
|
String get subsystem => 'team';
|
||||||
@override
|
@override
|
||||||
String get kind => 'member-born';
|
String get kind => 'member-joined';
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> payload() => {
|
Map<String, Object?> payload() => {
|
||||||
'team': team,
|
'team': team,
|
||||||
@@ -159,8 +159,8 @@ class TeamMemberBorn extends ClideEvent {
|
|||||||
|
|
||||||
/// A Claude Code tmux teammate's pane went away (it exited or the team
|
/// A Claude Code tmux teammate's pane went away (it exited or the team
|
||||||
/// dissolved) — T-139.
|
/// dissolved) — T-139.
|
||||||
class TeamMemberDied extends ClideEvent {
|
class TeamMemberLeft extends ClideEvent {
|
||||||
const TeamMemberDied({
|
const TeamMemberLeft({
|
||||||
required this.team,
|
required this.team,
|
||||||
required this.agentId,
|
required this.agentId,
|
||||||
required this.paneId,
|
required this.paneId,
|
||||||
@@ -173,7 +173,7 @@ class TeamMemberDied extends ClideEvent {
|
|||||||
@override
|
@override
|
||||||
String get subsystem => 'team';
|
String get subsystem => 'team';
|
||||||
@override
|
@override
|
||||||
String get kind => 'member-died';
|
String get kind => 'member-left';
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> payload() => {'team': team, 'agentId': agentId, 'paneId': paneId};
|
Map<String, Object?> payload() => {'team': team, 'agentId': agentId, 'paneId': paneId};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/// Tests for the tmux team observer (T-139). Pure Dart (no Flutter):
|
/// Tests for the tmux team observer (T-139). Pure Dart (no Flutter):
|
||||||
/// config parsing/discovery, the config-driven born/died lifecycle, and
|
/// config parsing/discovery, the config-driven joined/left lifecycle, and
|
||||||
/// the best-effort subagent-transcript join — all exercised against
|
/// the best-effort subagent-transcript join — all exercised against
|
||||||
/// on-disk fixtures, mirroring how the T-134 spike validated CC's
|
/// on-disk fixtures, mirroring how the T-134 spike validated CC's
|
||||||
/// undocumented team artifacts.
|
/// undocumented team artifacts.
|
||||||
@@ -54,8 +54,8 @@ Future<Directory> _writeTeam(Directory teamsBase, String team, String json) asyn
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('team events', () {
|
group('team events', () {
|
||||||
test('TeamMemberBorn payload carries identity + optional fields', () {
|
test('TeamMemberJoined payload carries identity + optional fields', () {
|
||||||
const e = TeamMemberBorn(
|
const e = TeamMemberJoined(
|
||||||
team: 'myteam',
|
team: 'myteam',
|
||||||
agentId: 'alice@myteam',
|
agentId: 'alice@myteam',
|
||||||
name: 'alice',
|
name: 'alice',
|
||||||
@@ -67,7 +67,7 @@ void main() {
|
|||||||
transcriptPath: '/t/agent-a.jsonl',
|
transcriptPath: '/t/agent-a.jsonl',
|
||||||
);
|
);
|
||||||
expect(e.subsystem, 'team');
|
expect(e.subsystem, 'team');
|
||||||
expect(e.kind, 'member-born');
|
expect(e.kind, 'member-joined');
|
||||||
expect(e.payload(), {
|
expect(e.payload(), {
|
||||||
'team': 'myteam',
|
'team': 'myteam',
|
||||||
'agentId': 'alice@myteam',
|
'agentId': 'alice@myteam',
|
||||||
@@ -81,15 +81,15 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('TeamMemberBorn omits null optional fields', () {
|
test('TeamMemberJoined omits null optional fields', () {
|
||||||
const e = TeamMemberBorn(team: 't', agentId: 'a@t', name: 'a', agentType: 'a', paneId: '%1');
|
const e = TeamMemberJoined(team: 't', agentId: 'a@t', name: 'a', agentType: 'a', paneId: '%1');
|
||||||
expect(e.payload().keys, ['team', 'agentId', 'name', 'agentType', 'paneId']);
|
expect(e.payload().keys, ['team', 'agentId', 'name', 'agentType', 'paneId']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('TeamMemberDied payload', () {
|
test('TeamMemberLeft payload', () {
|
||||||
const e = TeamMemberDied(team: 't', agentId: 'a@t', paneId: '%1');
|
const e = TeamMemberLeft(team: 't', agentId: 'a@t', paneId: '%1');
|
||||||
expect(e.subsystem, 'team');
|
expect(e.subsystem, 'team');
|
||||||
expect(e.kind, 'member-died');
|
expect(e.kind, 'member-left');
|
||||||
expect(e.payload(), {'team': 't', 'agentId': 'a@t', 'paneId': '%1'});
|
expect(e.payload(), {'team': 't', 'agentId': 'a@t', 'paneId': '%1'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -145,18 +145,18 @@ void main() {
|
|||||||
late Directory projectsBase;
|
late Directory projectsBase;
|
||||||
late DaemonBus events;
|
late DaemonBus events;
|
||||||
late MessageBus messages;
|
late MessageBus messages;
|
||||||
late List<TeamMemberBorn> born;
|
late List<TeamMemberJoined> joined;
|
||||||
late List<TeamMemberDied> died;
|
late List<TeamMemberLeft> left;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
teamsBase = await Directory.systemTemp.createTemp('teams_');
|
teamsBase = await Directory.systemTemp.createTemp('teams_');
|
||||||
projectsBase = await Directory.systemTemp.createTemp('projects_');
|
projectsBase = await Directory.systemTemp.createTemp('projects_');
|
||||||
events = DaemonBus();
|
events = DaemonBus();
|
||||||
messages = MessageBus();
|
messages = MessageBus();
|
||||||
born = [];
|
joined = [];
|
||||||
died = [];
|
left = [];
|
||||||
events.on<TeamMemberBorn>().listen(born.add);
|
events.on<TeamMemberJoined>().listen(joined.add);
|
||||||
events.on<TeamMemberDied>().listen(died.add);
|
events.on<TeamMemberLeft>().listen(left.add);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
@@ -168,7 +168,7 @@ void main() {
|
|||||||
|
|
||||||
Future<void> settle() => Future<void>.delayed(const Duration(milliseconds: 10));
|
Future<void> settle() => Future<void>.delayed(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
test('emits born when a teammate pane is live, died when it goes', () async {
|
test('emits joined when a teammate pane is live, left when it goes', () async {
|
||||||
await _writeTeam(teamsBase, 'myteam', _configJson(teammates: [_member('alice', '%5')]));
|
await _writeTeam(teamsBase, 'myteam', _configJson(teammates: [_member('alice', '%5')]));
|
||||||
var panes = {'%5'};
|
var panes = {'%5'};
|
||||||
final obs = TeamObserver(
|
final obs = TeamObserver(
|
||||||
@@ -183,21 +183,21 @@ void main() {
|
|||||||
|
|
||||||
await obs.tick();
|
await obs.tick();
|
||||||
await settle();
|
await settle();
|
||||||
expect(born.map((b) => b.name), ['alice']);
|
expect(joined.map((b) => b.name), ['alice']);
|
||||||
expect(born.single.paneId, '%5');
|
expect(joined.single.paneId, '%5');
|
||||||
expect(born.single.agentId, 'alice@myteam');
|
expect(joined.single.agentId, 'alice@myteam');
|
||||||
expect(died, isEmpty);
|
expect(left, isEmpty);
|
||||||
|
|
||||||
// Same pane still live -> no duplicate born.
|
// Same pane still live -> no duplicate joined.
|
||||||
await obs.tick();
|
await obs.tick();
|
||||||
await settle();
|
await settle();
|
||||||
expect(born, hasLength(1));
|
expect(joined, hasLength(1));
|
||||||
|
|
||||||
// Pane gone -> died.
|
// Pane gone -> left.
|
||||||
panes = {};
|
panes = {};
|
||||||
await obs.tick();
|
await obs.tick();
|
||||||
await settle();
|
await settle();
|
||||||
expect(died.map((d) => d.agentId), ['alice@myteam']);
|
expect(left.map((d) => d.agentId), ['alice@myteam']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('start() polls on a timer and dispose() stops it', () async {
|
test('start() polls on a timer and dispose() stops it', () async {
|
||||||
@@ -212,16 +212,16 @@ void main() {
|
|||||||
pollInterval: const Duration(milliseconds: 20),
|
pollInterval: const Duration(milliseconds: 20),
|
||||||
);
|
);
|
||||||
obs.start();
|
obs.start();
|
||||||
// Poll until the timer-driven tick emits born (or time out).
|
// Poll until the timer-driven tick emits joined (or time out).
|
||||||
final deadline = DateTime.now().add(const Duration(seconds: 2));
|
final deadline = DateTime.now().add(const Duration(seconds: 2));
|
||||||
while (born.isEmpty && DateTime.now().isBefore(deadline)) {
|
while (joined.isEmpty && DateTime.now().isBefore(deadline)) {
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||||
}
|
}
|
||||||
expect(born.map((b) => b.name), ['alice']);
|
expect(joined.map((b) => b.name), ['alice']);
|
||||||
await obs.dispose();
|
await obs.dispose();
|
||||||
// dispose emits died for the tracked member.
|
// dispose emits left for the tracked member.
|
||||||
await settle();
|
await settle();
|
||||||
expect(died.map((d) => d.agentId), ['alice@myteam']);
|
expect(left.map((d) => d.agentId), ['alice@myteam']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('constructs with default base dirs / pane lister', () {
|
test('constructs with default base dirs / pane lister', () {
|
||||||
@@ -242,8 +242,8 @@ void main() {
|
|||||||
addTearDown(obs.dispose);
|
addTearDown(obs.dispose);
|
||||||
await obs.tick();
|
await obs.tick();
|
||||||
await settle();
|
await settle();
|
||||||
expect(born, isEmpty);
|
expect(joined, isEmpty);
|
||||||
expect(died, isEmpty);
|
expect(left, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('joins the teammate transcript via a matching .meta.json', () async {
|
test('joins the teammate transcript via a matching .meta.json', () async {
|
||||||
@@ -266,7 +266,7 @@ void main() {
|
|||||||
|
|
||||||
await obs.tick();
|
await obs.tick();
|
||||||
await settle();
|
await settle();
|
||||||
expect(born.single.transcriptPath, endsWith('agent-aaa111.jsonl'));
|
expect(joined.single.transcriptPath, endsWith('agent-aaa111.jsonl'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('falls back to joinedAt<->mtime order when no .meta.json', () async {
|
test('falls back to joinedAt<->mtime order when no .meta.json', () async {
|
||||||
@@ -300,7 +300,7 @@ void main() {
|
|||||||
|
|
||||||
await obs.tick();
|
await obs.tick();
|
||||||
await settle();
|
await settle();
|
||||||
final byName = {for (final b in born) b.name: b.transcriptPath};
|
final byName = {for (final b in joined) b.name: b.transcriptPath};
|
||||||
expect(byName['first'], endsWith('agent-older.jsonl'));
|
expect(byName['first'], endsWith('agent-older.jsonl'));
|
||||||
expect(byName['second'], endsWith('agent-newer.jsonl'));
|
expect(byName['second'], endsWith('agent-newer.jsonl'));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user