refactor(adapters): brand btmsg/bttask/groups bridge interfaces with GroupId/AgentId

Apply branded types to all IPC bridge interfaces and function
parameters. Update test mock data with branded constructors.
This commit is contained in:
Hibryda 2026-03-11 22:56:52 +01:00
parent f928abd6ce
commit 0742309595
5 changed files with 82 additions and 77 deletions

View file

@ -29,6 +29,7 @@ import {
type BtmsgChannel, type BtmsgChannel,
type BtmsgChannelMessage, type BtmsgChannelMessage,
} from './btmsg-bridge'; } from './btmsg-bridge';
import { GroupId, AgentId } from '../types/ids';
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@ -42,10 +43,10 @@ describe('btmsg-bridge', () => {
describe('BtmsgAgent camelCase fields', () => { describe('BtmsgAgent camelCase fields', () => {
it('receives camelCase fields from Rust backend', async () => { it('receives camelCase fields from Rust backend', async () => {
const agent: BtmsgAgent = { const agent: BtmsgAgent = {
id: 'a1', id: AgentId('a1'),
name: 'Coder', name: 'Coder',
role: 'developer', role: 'developer',
groupId: 'g1', // was: group_id groupId: GroupId('g1'), // was: group_id
tier: 1, tier: 1,
model: 'claude-4', model: 'claude-4',
status: 'active', status: 'active',
@ -53,7 +54,7 @@ describe('btmsg-bridge', () => {
}; };
mockInvoke.mockResolvedValue([agent]); mockInvoke.mockResolvedValue([agent]);
const result = await getGroupAgents('g1'); const result = await getGroupAgents(GroupId('g1'));
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0].groupId).toBe('g1'); expect(result[0].groupId).toBe('g1');
@ -65,7 +66,7 @@ describe('btmsg-bridge', () => {
it('invokes btmsg_get_agents with groupId', async () => { it('invokes btmsg_get_agents with groupId', async () => {
mockInvoke.mockResolvedValue([]); mockInvoke.mockResolvedValue([]);
await getGroupAgents('g1'); await getGroupAgents(GroupId('g1'));
expect(mockInvoke).toHaveBeenCalledWith('btmsg_get_agents', { groupId: 'g1' }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_get_agents', { groupId: 'g1' });
}); });
}); });
@ -74,8 +75,8 @@ describe('btmsg-bridge', () => {
it('receives camelCase fields from Rust backend', async () => { it('receives camelCase fields from Rust backend', async () => {
const msg: BtmsgMessage = { const msg: BtmsgMessage = {
id: 'm1', id: 'm1',
fromAgent: 'a1', // was: from_agent fromAgent: AgentId('a1'), // was: from_agent
toAgent: 'a2', // was: to_agent toAgent: AgentId('a2'), // was: to_agent
content: 'hello', content: 'hello',
read: false, read: false,
replyTo: null, // was: reply_to replyTo: null, // was: reply_to
@ -85,7 +86,7 @@ describe('btmsg-bridge', () => {
}; };
mockInvoke.mockResolvedValue([msg]); mockInvoke.mockResolvedValue([msg]);
const result = await getUnreadMessages('a2'); const result = await getUnreadMessages(AgentId('a2'));
expect(result[0].fromAgent).toBe('a1'); expect(result[0].fromAgent).toBe('a1');
expect(result[0].toAgent).toBe('a2'); expect(result[0].toAgent).toBe('a2');
@ -100,8 +101,8 @@ describe('btmsg-bridge', () => {
it('receives camelCase fields including recipient info', async () => { it('receives camelCase fields including recipient info', async () => {
const feed: BtmsgFeedMessage = { const feed: BtmsgFeedMessage = {
id: 'm1', id: 'm1',
fromAgent: 'a1', fromAgent: AgentId('a1'),
toAgent: 'a2', toAgent: AgentId('a2'),
content: 'review this', content: 'review this',
createdAt: '2026-01-01', createdAt: '2026-01-01',
replyTo: null, replyTo: null,
@ -112,7 +113,7 @@ describe('btmsg-bridge', () => {
}; };
mockInvoke.mockResolvedValue([feed]); mockInvoke.mockResolvedValue([feed]);
const result = await getAllFeed('g1'); const result = await getAllFeed(GroupId('g1'));
expect(result[0].senderName).toBe('Coder'); expect(result[0].senderName).toBe('Coder');
expect(result[0].recipientName).toBe('Reviewer'); expect(result[0].recipientName).toBe('Reviewer');
@ -125,14 +126,14 @@ describe('btmsg-bridge', () => {
const channel: BtmsgChannel = { const channel: BtmsgChannel = {
id: 'ch1', id: 'ch1',
name: 'general', name: 'general',
groupId: 'g1', // was: group_id groupId: GroupId('g1'), // was: group_id
createdBy: 'admin', // was: created_by createdBy: AgentId('admin'), // was: created_by
memberCount: 5, // was: member_count memberCount: 5, // was: member_count
createdAt: '2026-01-01', createdAt: '2026-01-01',
}; };
mockInvoke.mockResolvedValue([channel]); mockInvoke.mockResolvedValue([channel]);
const result = await getChannels('g1'); const result = await getChannels(GroupId('g1'));
expect(result[0].groupId).toBe('g1'); expect(result[0].groupId).toBe('g1');
expect(result[0].createdBy).toBe('admin'); expect(result[0].createdBy).toBe('admin');
@ -145,7 +146,7 @@ describe('btmsg-bridge', () => {
const msg: BtmsgChannelMessage = { const msg: BtmsgChannelMessage = {
id: 'cm1', id: 'cm1',
channelId: 'ch1', // was: channel_id channelId: 'ch1', // was: channel_id
fromAgent: 'a1', fromAgent: AgentId('a1'),
content: 'hello', content: 'hello',
createdAt: '2026-01-01', createdAt: '2026-01-01',
senderName: 'Coder', senderName: 'Coder',
@ -166,65 +167,65 @@ describe('btmsg-bridge', () => {
describe('IPC commands', () => { describe('IPC commands', () => {
it('getUnreadCount invokes btmsg_unread_count', async () => { it('getUnreadCount invokes btmsg_unread_count', async () => {
mockInvoke.mockResolvedValue(5); mockInvoke.mockResolvedValue(5);
const result = await getUnreadCount('a1'); const result = await getUnreadCount(AgentId('a1'));
expect(result).toBe(5); expect(result).toBe(5);
expect(mockInvoke).toHaveBeenCalledWith('btmsg_unread_count', { agentId: 'a1' }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_unread_count', { agentId: 'a1' });
}); });
it('getHistory invokes btmsg_history', async () => { it('getHistory invokes btmsg_history', async () => {
mockInvoke.mockResolvedValue([]); mockInvoke.mockResolvedValue([]);
await getHistory('a1', 'a2', 50); await getHistory(AgentId('a1'), AgentId('a2'), 50);
expect(mockInvoke).toHaveBeenCalledWith('btmsg_history', { agentId: 'a1', otherId: 'a2', limit: 50 }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_history', { agentId: 'a1', otherId: 'a2', limit: 50 });
}); });
it('getHistory defaults limit to 20', async () => { it('getHistory defaults limit to 20', async () => {
mockInvoke.mockResolvedValue([]); mockInvoke.mockResolvedValue([]);
await getHistory('a1', 'a2'); await getHistory(AgentId('a1'), AgentId('a2'));
expect(mockInvoke).toHaveBeenCalledWith('btmsg_history', { agentId: 'a1', otherId: 'a2', limit: 20 }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_history', { agentId: 'a1', otherId: 'a2', limit: 20 });
}); });
it('sendMessage invokes btmsg_send', async () => { it('sendMessage invokes btmsg_send', async () => {
mockInvoke.mockResolvedValue('msg-id'); mockInvoke.mockResolvedValue('msg-id');
const result = await sendMessage('a1', 'a2', 'hello'); const result = await sendMessage(AgentId('a1'), AgentId('a2'), 'hello');
expect(result).toBe('msg-id'); expect(result).toBe('msg-id');
expect(mockInvoke).toHaveBeenCalledWith('btmsg_send', { fromAgent: 'a1', toAgent: 'a2', content: 'hello' }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_send', { fromAgent: 'a1', toAgent: 'a2', content: 'hello' });
}); });
it('setAgentStatus invokes btmsg_set_status', async () => { it('setAgentStatus invokes btmsg_set_status', async () => {
mockInvoke.mockResolvedValue(undefined); mockInvoke.mockResolvedValue(undefined);
await setAgentStatus('a1', 'active'); await setAgentStatus(AgentId('a1'), 'active');
expect(mockInvoke).toHaveBeenCalledWith('btmsg_set_status', { agentId: 'a1', status: 'active' }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_set_status', { agentId: 'a1', status: 'active' });
}); });
it('ensureAdmin invokes btmsg_ensure_admin', async () => { it('ensureAdmin invokes btmsg_ensure_admin', async () => {
mockInvoke.mockResolvedValue(undefined); mockInvoke.mockResolvedValue(undefined);
await ensureAdmin('g1'); await ensureAdmin(GroupId('g1'));
expect(mockInvoke).toHaveBeenCalledWith('btmsg_ensure_admin', { groupId: 'g1' }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_ensure_admin', { groupId: 'g1' });
}); });
it('markRead invokes btmsg_mark_read', async () => { it('markRead invokes btmsg_mark_read', async () => {
mockInvoke.mockResolvedValue(undefined); mockInvoke.mockResolvedValue(undefined);
await markRead('a2', 'a1'); await markRead(AgentId('a2'), AgentId('a1'));
expect(mockInvoke).toHaveBeenCalledWith('btmsg_mark_read', { readerId: 'a2', senderId: 'a1' }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_mark_read', { readerId: 'a2', senderId: 'a1' });
}); });
it('sendChannelMessage invokes btmsg_channel_send', async () => { it('sendChannelMessage invokes btmsg_channel_send', async () => {
mockInvoke.mockResolvedValue('cm-id'); mockInvoke.mockResolvedValue('cm-id');
const result = await sendChannelMessage('ch1', 'a1', 'hello channel'); const result = await sendChannelMessage('ch1', AgentId('a1'), 'hello channel');
expect(result).toBe('cm-id'); expect(result).toBe('cm-id');
expect(mockInvoke).toHaveBeenCalledWith('btmsg_channel_send', { channelId: 'ch1', fromAgent: 'a1', content: 'hello channel' }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_channel_send', { channelId: 'ch1', fromAgent: 'a1', content: 'hello channel' });
}); });
it('createChannel invokes btmsg_create_channel', async () => { it('createChannel invokes btmsg_create_channel', async () => {
mockInvoke.mockResolvedValue('ch-id'); mockInvoke.mockResolvedValue('ch-id');
const result = await createChannel('general', 'g1', 'admin'); const result = await createChannel('general', GroupId('g1'), AgentId('admin'));
expect(result).toBe('ch-id'); expect(result).toBe('ch-id');
expect(mockInvoke).toHaveBeenCalledWith('btmsg_create_channel', { name: 'general', groupId: 'g1', createdBy: 'admin' }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_create_channel', { name: 'general', groupId: 'g1', createdBy: 'admin' });
}); });
it('addChannelMember invokes btmsg_add_channel_member', async () => { it('addChannelMember invokes btmsg_add_channel_member', async () => {
mockInvoke.mockResolvedValue(undefined); mockInvoke.mockResolvedValue(undefined);
await addChannelMember('ch1', 'a1'); await addChannelMember('ch1', AgentId('a1'));
expect(mockInvoke).toHaveBeenCalledWith('btmsg_add_channel_member', { channelId: 'ch1', agentId: 'a1' }); expect(mockInvoke).toHaveBeenCalledWith('btmsg_add_channel_member', { channelId: 'ch1', agentId: 'a1' });
}); });
}); });
@ -232,7 +233,7 @@ describe('btmsg-bridge', () => {
describe('error propagation', () => { describe('error propagation', () => {
it('propagates invoke errors', async () => { it('propagates invoke errors', async () => {
mockInvoke.mockRejectedValue(new Error('btmsg database not found')); mockInvoke.mockRejectedValue(new Error('btmsg database not found'));
await expect(getGroupAgents('g1')).rejects.toThrow('btmsg database not found'); await expect(getGroupAgents(GroupId('g1'))).rejects.toThrow('btmsg database not found');
}); });
}); });
}); });

View file

@ -5,12 +5,13 @@
*/ */
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import type { GroupId, AgentId } from '../types/ids';
export interface BtmsgAgent { export interface BtmsgAgent {
id: string; id: AgentId;
name: string; name: string;
role: string; role: string;
groupId: string; groupId: GroupId;
tier: number; tier: number;
model: string | null; model: string | null;
status: string; status: string;
@ -19,8 +20,8 @@ export interface BtmsgAgent {
export interface BtmsgMessage { export interface BtmsgMessage {
id: string; id: string;
fromAgent: string; fromAgent: AgentId;
toAgent: string; toAgent: AgentId;
content: string; content: string;
read: boolean; read: boolean;
replyTo: string | null; replyTo: string | null;
@ -31,8 +32,8 @@ export interface BtmsgMessage {
export interface BtmsgFeedMessage { export interface BtmsgFeedMessage {
id: string; id: string;
fromAgent: string; fromAgent: AgentId;
toAgent: string; toAgent: AgentId;
content: string; content: string;
createdAt: string; createdAt: string;
replyTo: string | null; replyTo: string | null;
@ -45,8 +46,8 @@ export interface BtmsgFeedMessage {
export interface BtmsgChannel { export interface BtmsgChannel {
id: string; id: string;
name: string; name: string;
groupId: string; groupId: GroupId;
createdBy: string; createdBy: AgentId;
memberCount: number; memberCount: number;
createdAt: string; createdAt: string;
} }
@ -54,7 +55,7 @@ export interface BtmsgChannel {
export interface BtmsgChannelMessage { export interface BtmsgChannelMessage {
id: string; id: string;
channelId: string; channelId: string;
fromAgent: string; fromAgent: AgentId;
content: string; content: string;
createdAt: string; createdAt: string;
senderName: string; senderName: string;
@ -64,70 +65,70 @@ export interface BtmsgChannelMessage {
/** /**
* Get all agents in a group with their unread counts. * Get all agents in a group with their unread counts.
*/ */
export async function getGroupAgents(groupId: string): Promise<BtmsgAgent[]> { export async function getGroupAgents(groupId: GroupId): Promise<BtmsgAgent[]> {
return invoke('btmsg_get_agents', { groupId }); return invoke('btmsg_get_agents', { groupId });
} }
/** /**
* Get unread message count for an agent. * Get unread message count for an agent.
*/ */
export async function getUnreadCount(agentId: string): Promise<number> { export async function getUnreadCount(agentId: AgentId): Promise<number> {
return invoke('btmsg_unread_count', { agentId }); return invoke('btmsg_unread_count', { agentId });
} }
/** /**
* Get unread messages for an agent. * Get unread messages for an agent.
*/ */
export async function getUnreadMessages(agentId: string): Promise<BtmsgMessage[]> { export async function getUnreadMessages(agentId: AgentId): Promise<BtmsgMessage[]> {
return invoke('btmsg_unread_messages', { agentId }); return invoke('btmsg_unread_messages', { agentId });
} }
/** /**
* Get conversation history between two agents. * Get conversation history between two agents.
*/ */
export async function getHistory(agentId: string, otherId: string, limit: number = 20): Promise<BtmsgMessage[]> { export async function getHistory(agentId: AgentId, otherId: AgentId, limit: number = 20): Promise<BtmsgMessage[]> {
return invoke('btmsg_history', { agentId, otherId, limit }); return invoke('btmsg_history', { agentId, otherId, limit });
} }
/** /**
* Send a message from one agent to another. * Send a message from one agent to another.
*/ */
export async function sendMessage(fromAgent: string, toAgent: string, content: string): Promise<string> { export async function sendMessage(fromAgent: AgentId, toAgent: AgentId, content: string): Promise<string> {
return invoke('btmsg_send', { fromAgent, toAgent, content }); return invoke('btmsg_send', { fromAgent, toAgent, content });
} }
/** /**
* Update agent status (active/sleeping/stopped). * Update agent status (active/sleeping/stopped).
*/ */
export async function setAgentStatus(agentId: string, status: string): Promise<void> { export async function setAgentStatus(agentId: AgentId, status: string): Promise<void> {
return invoke('btmsg_set_status', { agentId, status }); return invoke('btmsg_set_status', { agentId, status });
} }
/** /**
* Ensure admin agent exists with contacts to all agents. * Ensure admin agent exists with contacts to all agents.
*/ */
export async function ensureAdmin(groupId: string): Promise<void> { export async function ensureAdmin(groupId: GroupId): Promise<void> {
return invoke('btmsg_ensure_admin', { groupId }); return invoke('btmsg_ensure_admin', { groupId });
} }
/** /**
* Get all messages in group (admin global feed). * Get all messages in group (admin global feed).
*/ */
export async function getAllFeed(groupId: string, limit: number = 100): Promise<BtmsgFeedMessage[]> { export async function getAllFeed(groupId: GroupId, limit: number = 100): Promise<BtmsgFeedMessage[]> {
return invoke('btmsg_all_feed', { groupId, limit }); return invoke('btmsg_all_feed', { groupId, limit });
} }
/** /**
* Mark all messages from sender to reader as read. * Mark all messages from sender to reader as read.
*/ */
export async function markRead(readerId: string, senderId: string): Promise<void> { export async function markRead(readerId: AgentId, senderId: AgentId): Promise<void> {
return invoke('btmsg_mark_read', { readerId, senderId }); return invoke('btmsg_mark_read', { readerId, senderId });
} }
/** /**
* Get channels in a group. * Get channels in a group.
*/ */
export async function getChannels(groupId: string): Promise<BtmsgChannel[]> { export async function getChannels(groupId: GroupId): Promise<BtmsgChannel[]> {
return invoke('btmsg_get_channels', { groupId }); return invoke('btmsg_get_channels', { groupId });
} }
@ -141,20 +142,20 @@ export async function getChannelMessages(channelId: string, limit: number = 100)
/** /**
* Send a message to a channel. * Send a message to a channel.
*/ */
export async function sendChannelMessage(channelId: string, fromAgent: string, content: string): Promise<string> { export async function sendChannelMessage(channelId: string, fromAgent: AgentId, content: string): Promise<string> {
return invoke('btmsg_channel_send', { channelId, fromAgent, content }); return invoke('btmsg_channel_send', { channelId, fromAgent, content });
} }
/** /**
* Create a new channel. * Create a new channel.
*/ */
export async function createChannel(name: string, groupId: string, createdBy: string): Promise<string> { export async function createChannel(name: string, groupId: GroupId, createdBy: AgentId): Promise<string> {
return invoke('btmsg_create_channel', { name, groupId, createdBy }); return invoke('btmsg_create_channel', { name, groupId, createdBy });
} }
/** /**
* Add a member to a channel. * Add a member to a channel.
*/ */
export async function addChannelMember(channelId: string, agentId: string): Promise<void> { export async function addChannelMember(channelId: string, agentId: AgentId): Promise<void> {
return invoke('btmsg_add_channel_member', { channelId, agentId }); return invoke('btmsg_add_channel_member', { channelId, agentId });
} }

View file

@ -18,6 +18,7 @@ import {
type Task, type Task,
type TaskComment, type TaskComment,
} from './bttask-bridge'; } from './bttask-bridge';
import { GroupId, AgentId } from '../types/ids';
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@ -34,9 +35,9 @@ describe('bttask-bridge', () => {
description: 'Critical fix', description: 'Critical fix',
status: 'progress', status: 'progress',
priority: 'high', priority: 'high',
assignedTo: 'a1', // was: assigned_to assignedTo: AgentId('a1'), // was: assigned_to
createdBy: 'admin', // was: created_by createdBy: AgentId('admin'), // was: created_by
groupId: 'g1', // was: group_id groupId: GroupId('g1'), // was: group_id
parentTaskId: null, // was: parent_task_id parentTaskId: null, // was: parent_task_id
sortOrder: 1, // was: sort_order sortOrder: 1, // was: sort_order
createdAt: '2026-01-01', // was: created_at createdAt: '2026-01-01', // was: created_at
@ -44,7 +45,7 @@ describe('bttask-bridge', () => {
}; };
mockInvoke.mockResolvedValue([task]); mockInvoke.mockResolvedValue([task]);
const result = await listTasks('g1'); const result = await listTasks(GroupId('g1'));
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0].assignedTo).toBe('a1'); expect(result[0].assignedTo).toBe('a1');
@ -64,7 +65,7 @@ describe('bttask-bridge', () => {
const comment: TaskComment = { const comment: TaskComment = {
id: 'c1', id: 'c1',
taskId: 't1', // was: task_id taskId: 't1', // was: task_id
agentId: 'a1', // was: agent_id agentId: AgentId('a1'), // was: agent_id
content: 'Working on it', content: 'Working on it',
createdAt: '2026-01-01', createdAt: '2026-01-01',
}; };
@ -84,7 +85,7 @@ describe('bttask-bridge', () => {
describe('IPC commands', () => { describe('IPC commands', () => {
it('listTasks invokes bttask_list', async () => { it('listTasks invokes bttask_list', async () => {
mockInvoke.mockResolvedValue([]); mockInvoke.mockResolvedValue([]);
await listTasks('g1'); await listTasks(GroupId('g1'));
expect(mockInvoke).toHaveBeenCalledWith('bttask_list', { groupId: 'g1' }); expect(mockInvoke).toHaveBeenCalledWith('bttask_list', { groupId: 'g1' });
}); });
@ -102,14 +103,14 @@ describe('bttask-bridge', () => {
it('addTaskComment invokes bttask_add_comment', async () => { it('addTaskComment invokes bttask_add_comment', async () => {
mockInvoke.mockResolvedValue('c-id'); mockInvoke.mockResolvedValue('c-id');
const result = await addTaskComment('t1', 'a1', 'Done!'); const result = await addTaskComment('t1', AgentId('a1'), 'Done!');
expect(result).toBe('c-id'); expect(result).toBe('c-id');
expect(mockInvoke).toHaveBeenCalledWith('bttask_add_comment', { taskId: 't1', agentId: 'a1', content: 'Done!' }); expect(mockInvoke).toHaveBeenCalledWith('bttask_add_comment', { taskId: 't1', agentId: 'a1', content: 'Done!' });
}); });
it('createTask invokes bttask_create with all fields', async () => { it('createTask invokes bttask_create with all fields', async () => {
mockInvoke.mockResolvedValue('t-id'); mockInvoke.mockResolvedValue('t-id');
const result = await createTask('Fix bug', 'desc', 'high', 'g1', 'admin', 'a1'); const result = await createTask('Fix bug', 'desc', 'high', GroupId('g1'), AgentId('admin'), AgentId('a1'));
expect(result).toBe('t-id'); expect(result).toBe('t-id');
expect(mockInvoke).toHaveBeenCalledWith('bttask_create', { expect(mockInvoke).toHaveBeenCalledWith('bttask_create', {
title: 'Fix bug', title: 'Fix bug',
@ -123,7 +124,7 @@ describe('bttask-bridge', () => {
it('createTask invokes bttask_create without assignedTo', async () => { it('createTask invokes bttask_create without assignedTo', async () => {
mockInvoke.mockResolvedValue('t-id'); mockInvoke.mockResolvedValue('t-id');
await createTask('Add tests', '', 'medium', 'g1', 'a1'); await createTask('Add tests', '', 'medium', GroupId('g1'), AgentId('a1'));
expect(mockInvoke).toHaveBeenCalledWith('bttask_create', { expect(mockInvoke).toHaveBeenCalledWith('bttask_create', {
title: 'Add tests', title: 'Add tests',
description: '', description: '',
@ -144,7 +145,7 @@ describe('bttask-bridge', () => {
describe('error propagation', () => { describe('error propagation', () => {
it('propagates invoke errors', async () => { it('propagates invoke errors', async () => {
mockInvoke.mockRejectedValue(new Error('btmsg database not found')); mockInvoke.mockRejectedValue(new Error('btmsg database not found'));
await expect(listTasks('g1')).rejects.toThrow('btmsg database not found'); await expect(listTasks(GroupId('g1'))).rejects.toThrow('btmsg database not found');
}); });
}); });
}); });

View file

@ -1,6 +1,7 @@
// bttask Bridge — Tauri IPC adapter for task board // bttask Bridge — Tauri IPC adapter for task board
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import type { GroupId, AgentId } from '../types/ids';
export interface Task { export interface Task {
id: string; id: string;
@ -8,9 +9,9 @@ export interface Task {
description: string; description: string;
status: 'todo' | 'progress' | 'review' | 'done' | 'blocked'; status: 'todo' | 'progress' | 'review' | 'done' | 'blocked';
priority: 'low' | 'medium' | 'high' | 'critical'; priority: 'low' | 'medium' | 'high' | 'critical';
assignedTo: string | null; assignedTo: AgentId | null;
createdBy: string; createdBy: AgentId;
groupId: string; groupId: GroupId;
parentTaskId: string | null; parentTaskId: string | null;
sortOrder: number; sortOrder: number;
createdAt: string; createdAt: string;
@ -20,12 +21,12 @@ export interface Task {
export interface TaskComment { export interface TaskComment {
id: string; id: string;
taskId: string; taskId: string;
agentId: string; agentId: AgentId;
content: string; content: string;
createdAt: string; createdAt: string;
} }
export async function listTasks(groupId: string): Promise<Task[]> { export async function listTasks(groupId: GroupId): Promise<Task[]> {
return invoke<Task[]>('bttask_list', { groupId }); return invoke<Task[]>('bttask_list', { groupId });
} }
@ -37,7 +38,7 @@ export async function updateTaskStatus(taskId: string, status: string): Promise<
return invoke('bttask_update_status', { taskId, status }); return invoke('bttask_update_status', { taskId, status });
} }
export async function addTaskComment(taskId: string, agentId: string, content: string): Promise<string> { export async function addTaskComment(taskId: string, agentId: AgentId, content: string): Promise<string> {
return invoke<string>('bttask_add_comment', { taskId, agentId, content }); return invoke<string>('bttask_add_comment', { taskId, agentId, content });
} }
@ -45,9 +46,9 @@ export async function createTask(
title: string, title: string,
description: string, description: string,
priority: string, priority: string,
groupId: string, groupId: GroupId,
createdBy: string, createdBy: AgentId,
assignedTo?: string, assignedTo?: AgentId,
): Promise<string> { ): Promise<string> {
return invoke<string>('bttask_create', { title, description, priority, groupId, createdBy, assignedTo }); return invoke<string>('bttask_create', { title, description, priority, groupId, createdBy, assignedTo });
} }

View file

@ -1,5 +1,6 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import type { GroupsFile, ProjectConfig, GroupConfig } from '../types/groups'; import type { GroupsFile, ProjectConfig, GroupConfig } from '../types/groups';
import type { SessionId, ProjectId } from '../types/ids';
export type { GroupsFile, ProjectConfig, GroupConfig }; export type { GroupsFile, ProjectConfig, GroupConfig };
@ -11,8 +12,8 @@ export interface MdFileEntry {
export interface AgentMessageRecord { export interface AgentMessageRecord {
id: number; id: number;
session_id: string; session_id: SessionId;
project_id: string; project_id: ProjectId;
sdk_session_id: string | null; sdk_session_id: string | null;
message_type: string; message_type: string;
content: string; content: string;
@ -21,8 +22,8 @@ export interface AgentMessageRecord {
} }
export interface ProjectAgentState { export interface ProjectAgentState {
project_id: string; project_id: ProjectId;
last_session_id: string; last_session_id: SessionId;
sdk_session_id: string | null; sdk_session_id: string | null;
status: string; status: string;
cost_usd: number; cost_usd: number;
@ -51,8 +52,8 @@ export async function discoverMarkdownFiles(cwd: string): Promise<MdFileEntry[]>
// --- Agent message persistence --- // --- Agent message persistence ---
export async function saveAgentMessages( export async function saveAgentMessages(
sessionId: string, sessionId: SessionId,
projectId: string, projectId: ProjectId,
sdkSessionId: string | undefined, sdkSessionId: string | undefined,
messages: AgentMessageRecord[], messages: AgentMessageRecord[],
): Promise<void> { ): Promise<void> {
@ -64,7 +65,7 @@ export async function saveAgentMessages(
}); });
} }
export async function loadAgentMessages(projectId: string): Promise<AgentMessageRecord[]> { export async function loadAgentMessages(projectId: ProjectId): Promise<AgentMessageRecord[]> {
return invoke('agent_messages_load', { projectId }); return invoke('agent_messages_load', { projectId });
} }
@ -74,7 +75,7 @@ export async function saveProjectAgentState(state: ProjectAgentState): Promise<v
return invoke('project_agent_state_save', { state }); return invoke('project_agent_state_save', { state });
} }
export async function loadProjectAgentState(projectId: string): Promise<ProjectAgentState | null> { export async function loadProjectAgentState(projectId: ProjectId): Promise<ProjectAgentState | null> {
return invoke('project_agent_state_load', { projectId }); return invoke('project_agent_state_load', { projectId });
} }
@ -82,8 +83,8 @@ export async function loadProjectAgentState(projectId: string): Promise<ProjectA
export interface SessionMetric { export interface SessionMetric {
id: number; id: number;
project_id: string; project_id: ProjectId;
session_id: string; session_id: SessionId;
start_time: number; start_time: number;
end_time: number; end_time: number;
peak_tokens: number; peak_tokens: number;
@ -99,7 +100,7 @@ export async function saveSessionMetric(metric: Omit<SessionMetric, 'id'>): Prom
return invoke('session_metric_save', { metric: { id: 0, ...metric } }); return invoke('session_metric_save', { metric: { id: 0, ...metric } });
} }
export async function loadSessionMetrics(projectId: string, limit = 20): Promise<SessionMetric[]> { export async function loadSessionMetrics(projectId: ProjectId, limit = 20): Promise<SessionMetric[]> {
return invoke('session_metrics_load', { projectId, limit }); return invoke('session_metrics_load', { projectId, limit });
} }