initialize Electron desktop app and remove unused files
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { createServer, type Server as HttpServer } from 'http'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
const dbState = vi.hoisted(() => ({
|
||||
db: null as DatabaseSync | null,
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/db/index', () => ({
|
||||
getDb: () => dbState.db,
|
||||
isSqliteAvailable: () => Boolean(dbState.db),
|
||||
}))
|
||||
|
||||
vi.mock('socket.io-client', () => ({
|
||||
io: vi.fn(() => ({
|
||||
id: 'agent-socket',
|
||||
connected: true,
|
||||
io: { on: vi.fn() },
|
||||
on: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/services/auth', () => ({
|
||||
getToken: vi.fn(async () => 'test-token'),
|
||||
}))
|
||||
|
||||
describe('group chat workspace diff persistence', () => {
|
||||
let root: string
|
||||
let workspace: string
|
||||
let httpServer: HttpServer
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
root = mkdtempSync(join(tmpdir(), 'hermes-gc-diff-'))
|
||||
workspace = join(root, 'workspace')
|
||||
mkdirSync(workspace)
|
||||
dbState.db = new DatabaseSync(':memory:')
|
||||
const { initAllHermesTables } = await import('../../packages/server/src/db/hermes/schemas')
|
||||
initAllHermesTables()
|
||||
httpServer = createServer()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
httpServer?.close()
|
||||
dbState.db?.close()
|
||||
dbState.db = null
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function makeDraft(runId = '0123456789abcdef0123456789abcdef') {
|
||||
const tracker = await import('../../packages/server/src/services/hermes/run-chat/workspace-diff-tracker')
|
||||
writeFileSync(join(workspace, 'file.txt'), 'old\n')
|
||||
tracker.startWorkspaceRunCheckpoint({ sessionId: 'session-1', runId, workspace })
|
||||
writeFileSync(join(workspace, 'file.txt'), 'new\n')
|
||||
return tracker.completeWorkspaceRunCheckpointDraft({ sessionId: 'session-1', runId, workspace })
|
||||
}
|
||||
|
||||
async function saveDiff(storage: any, roomId = 'room-1', runId = '0123456789abcdef0123456789abcdef') {
|
||||
const draft = await makeDraft(runId)
|
||||
const saved = storage.saveWorkspaceDiffMessageForRun({
|
||||
roomId,
|
||||
senderId: 'agent-1',
|
||||
senderName: 'Worker',
|
||||
sessionId: 'session-1',
|
||||
runId,
|
||||
status: 'completed',
|
||||
workspace,
|
||||
draft: draft!,
|
||||
})
|
||||
return { saved, payload: JSON.parse(saved!.message.content) }
|
||||
}
|
||||
|
||||
function countRows(table: string, where = '', ...args: unknown[]): number {
|
||||
return (dbState.db?.prepare(`SELECT COUNT(*) AS count FROM ${table}${where}`).get(...args) as { count: number }).count
|
||||
}
|
||||
|
||||
function expectWorkspaceChangeDeleted(changeId: string) {
|
||||
expect(countRows('workspace_run_changes', ' WHERE change_id = ?', changeId)).toBe(0)
|
||||
expect(countRows('workspace_run_change_files', ' WHERE change_id = ?', changeId)).toBe(0)
|
||||
}
|
||||
|
||||
it('persists one workspace_run_change and one durable workspace_diff group message', async () => {
|
||||
const { GroupChatServer } = await import('../../packages/server/src/services/hermes/group-chat')
|
||||
const server = new GroupChatServer(httpServer)
|
||||
const storage = server.getStorage()
|
||||
storage.saveRoom('room-1', 'Room 1')
|
||||
const runId = '0123456789abcdef0123456789abcdef'
|
||||
const draft = await makeDraft(runId)
|
||||
|
||||
const saved = storage.saveWorkspaceDiffMessageForRun({
|
||||
roomId: 'room-1',
|
||||
senderId: 'agent-1',
|
||||
senderName: 'Worker',
|
||||
sessionId: 'session-1',
|
||||
runId,
|
||||
status: 'completed',
|
||||
workspace,
|
||||
draft: draft!,
|
||||
})
|
||||
|
||||
expect(saved?.message).toMatchObject({
|
||||
role: 'tool',
|
||||
tool_name: 'workspace_diff',
|
||||
tool_call_id: `workspace_diff:${runId}`,
|
||||
senderId: 'agent-1',
|
||||
senderName: 'Worker',
|
||||
})
|
||||
const payload = JSON.parse(saved!.message.content)
|
||||
expect(payload).toMatchObject({
|
||||
kind: 'workspace_diff',
|
||||
version: 1,
|
||||
room_id: 'room-1',
|
||||
session_id: 'session-1',
|
||||
run_id: runId,
|
||||
status: 'completed',
|
||||
files_changed: 1,
|
||||
})
|
||||
expect(payload.files[0].patch).toContain('-old')
|
||||
expect(payload.files[0].patch).toContain('+new')
|
||||
expect(payload.workspace).toBeUndefined()
|
||||
expect(payload.workspace_basename).toBe('workspace')
|
||||
|
||||
const rows = dbState.db?.prepare('SELECT COUNT(*) AS count FROM gc_messages WHERE tool_name = ?').get('workspace_diff') as { count: number }
|
||||
expect(rows.count).toBe(1)
|
||||
const changeRow = dbState.db?.prepare('SELECT workspace, room_id, message_id FROM workspace_run_changes WHERE change_id = ?').get(payload.change_id) as { workspace: string; room_id: string; message_id: string }
|
||||
expect(changeRow.workspace).toBe('workspace')
|
||||
expect(changeRow.workspace).not.toBe(workspace)
|
||||
expect(changeRow.room_id).toBe('room-1')
|
||||
expect(changeRow.message_id).toBe(saved!.message.id)
|
||||
const changeRows = dbState.db?.prepare('SELECT COUNT(*) AS count FROM workspace_run_changes WHERE change_id = ?').get(payload.change_id) as { count: number }
|
||||
expect(changeRows.count).toBe(1)
|
||||
server.getIO().close()
|
||||
})
|
||||
|
||||
it('keeps workspace_diff message ids unique when room ids are long', async () => {
|
||||
const { GroupChatServer } = await import('../../packages/server/src/services/hermes/group-chat')
|
||||
const server = new GroupChatServer(httpServer)
|
||||
const storage = server.getStorage()
|
||||
const roomId = `room-${'x'.repeat(220)}`
|
||||
storage.saveRoom(roomId, 'Long Room')
|
||||
const runA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
const runB = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
||||
|
||||
const draftA = await makeDraft(runA)
|
||||
const savedA = storage.saveWorkspaceDiffMessageForRun({
|
||||
roomId,
|
||||
senderId: 'agent-1',
|
||||
senderName: 'Worker',
|
||||
sessionId: 'session-1',
|
||||
runId: runA,
|
||||
status: 'completed',
|
||||
workspace,
|
||||
draft: draftA!,
|
||||
})
|
||||
const draftB = await makeDraft(runB)
|
||||
const savedB = storage.saveWorkspaceDiffMessageForRun({
|
||||
roomId,
|
||||
senderId: 'agent-1',
|
||||
senderName: 'Worker',
|
||||
sessionId: 'session-1',
|
||||
runId: runB,
|
||||
status: 'completed',
|
||||
workspace,
|
||||
draft: draftB!,
|
||||
})
|
||||
|
||||
expect(savedA?.message.id).not.toBe(savedB?.message.id)
|
||||
expect(savedA?.message.id).toContain(runA)
|
||||
expect(savedB?.message.id).toContain(runB)
|
||||
const rows = dbState.db?.prepare('SELECT COUNT(*) AS count FROM gc_messages WHERE roomId = ? AND tool_name = ?').get(roomId, 'workspace_diff') as { count: number }
|
||||
expect(rows.count).toBe(2)
|
||||
server.getIO().close()
|
||||
})
|
||||
|
||||
it('cleans persisted workspace diffs when room context is cleared', async () => {
|
||||
const { GroupChatServer } = await import('../../packages/server/src/services/hermes/group-chat')
|
||||
const server = new GroupChatServer(httpServer)
|
||||
const storage = server.getStorage()
|
||||
storage.saveRoom('room-1', 'Room 1')
|
||||
const { payload } = await saveDiff(storage)
|
||||
|
||||
expect(countRows('workspace_run_changes', ' WHERE change_id = ?', payload.change_id)).toBe(1)
|
||||
storage.clearRoomContext('room-1')
|
||||
|
||||
expectWorkspaceChangeDeleted(payload.change_id)
|
||||
expect(countRows('gc_messages', ' WHERE roomId = ?', 'room-1')).toBe(0)
|
||||
server.getIO().close()
|
||||
})
|
||||
|
||||
it('does not delete unrelated workspace changes from spoofed workspace_diff message content', async () => {
|
||||
const { GroupChatServer } = await import('../../packages/server/src/services/hermes/group-chat')
|
||||
const server = new GroupChatServer(httpServer)
|
||||
const storage = server.getStorage()
|
||||
storage.saveRoom('room-1', 'Room 1')
|
||||
storage.saveRoom('room-2', 'Room 2')
|
||||
const { payload } = await saveDiff(storage, 'room-1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
|
||||
|
||||
storage.saveMessageAndRefreshRoom({
|
||||
id: 'fake-workspace-diff',
|
||||
roomId: 'room-2',
|
||||
senderId: 'user-2',
|
||||
senderName: 'Mallory',
|
||||
content: JSON.stringify({ kind: 'workspace_diff', change_id: payload.change_id }),
|
||||
timestamp: 1,
|
||||
role: 'tool',
|
||||
tool_name: 'workspace_diff',
|
||||
tool_call_id: 'workspace_diff:fake',
|
||||
})
|
||||
storage.clearRoomContext('room-2')
|
||||
|
||||
expect(countRows('workspace_run_changes', ' WHERE change_id = ?', payload.change_id)).toBe(1)
|
||||
expect(countRows('workspace_run_change_files', ' WHERE change_id = ?', payload.change_id)).toBeGreaterThan(0)
|
||||
server.getIO().close()
|
||||
})
|
||||
|
||||
it('does not allow client messages to overwrite server-created workspace diff cards', async () => {
|
||||
const { GroupChatServer } = await import('../../packages/server/src/services/hermes/group-chat')
|
||||
const server = new GroupChatServer(httpServer)
|
||||
const storage = server.getStorage()
|
||||
storage.saveRoom('room-1', 'Room 1')
|
||||
const { saved, payload } = await saveDiff(storage, 'room-1', 'cccccccccccccccccccccccccccccccc')
|
||||
const originalTimestamp = saved!.message.timestamp
|
||||
|
||||
const overwrite = storage.saveMessageAndRefreshRoom({
|
||||
id: saved!.message.id,
|
||||
roomId: 'room-1',
|
||||
senderId: 'user-1',
|
||||
senderName: 'Mallory',
|
||||
content: JSON.stringify({ kind: 'workspace_diff', change_id: 'other-change' }),
|
||||
timestamp: 1,
|
||||
role: 'tool',
|
||||
tool_name: 'workspace_diff',
|
||||
tool_call_id: 'workspace_diff:fake',
|
||||
})
|
||||
|
||||
expect(overwrite.message.timestamp).toBe(originalTimestamp)
|
||||
expect(JSON.parse(String(overwrite.message.content)).change_id).toBe(payload.change_id)
|
||||
expect(countRows('workspace_run_changes', ' WHERE change_id = ?', payload.change_id)).toBe(1)
|
||||
server.getIO().close()
|
||||
})
|
||||
|
||||
it('cleans persisted workspace diffs when a room is deleted', async () => {
|
||||
const { GroupChatServer } = await import('../../packages/server/src/services/hermes/group-chat')
|
||||
const server = new GroupChatServer(httpServer)
|
||||
const storage = server.getStorage()
|
||||
storage.saveRoom('room-1', 'Room 1')
|
||||
const { payload } = await saveDiff(storage, 'room-1', 'dddddddddddddddddddddddddddddddd')
|
||||
|
||||
storage.deleteRoom('room-1')
|
||||
|
||||
expectWorkspaceChangeDeleted(payload.change_id)
|
||||
expect(countRows('gc_messages', ' WHERE roomId = ?', 'room-1')).toBe(0)
|
||||
expect(countRows('gc_rooms', ' WHERE id = ?', 'room-1')).toBe(0)
|
||||
server.getIO().close()
|
||||
})
|
||||
|
||||
it('does not write workspace diff rows after a room is deleted', async () => {
|
||||
const { GroupChatServer } = await import('../../packages/server/src/services/hermes/group-chat')
|
||||
const server = new GroupChatServer(httpServer)
|
||||
const storage = server.getStorage()
|
||||
storage.saveRoom('room-1', 'Room 1')
|
||||
const draft = await makeDraft('99999999999999999999999999999999')
|
||||
|
||||
storage.deleteRoom('room-1')
|
||||
const saved = storage.saveWorkspaceDiffMessageForRun({
|
||||
roomId: 'room-1',
|
||||
senderId: 'agent-1',
|
||||
senderName: 'Worker',
|
||||
sessionId: 'session-1',
|
||||
runId: '99999999999999999999999999999999',
|
||||
status: 'completed',
|
||||
workspace,
|
||||
draft: draft!,
|
||||
})
|
||||
|
||||
expect(saved).toBeNull()
|
||||
expect(countRows('gc_messages', ' WHERE roomId = ?', 'room-1')).toBe(0)
|
||||
expect(countRows('workspace_run_changes')).toBe(0)
|
||||
expect(countRows('workspace_run_change_files')).toBe(0)
|
||||
server.getIO().close()
|
||||
})
|
||||
|
||||
it('cleans persisted workspace diffs when their group messages are pruned', async () => {
|
||||
const { GroupChatServer } = await import('../../packages/server/src/services/hermes/group-chat')
|
||||
const server = new GroupChatServer(httpServer)
|
||||
const storage = server.getStorage()
|
||||
storage.saveRoom('room-1', 'Room 1')
|
||||
const { saved, payload } = await saveDiff(storage, 'room-1', 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee')
|
||||
dbState.db?.prepare('UPDATE gc_messages SET timestamp = ? WHERE id = ?').run(1, saved!.message.id)
|
||||
storage.saveMessageAndRefreshRoom({
|
||||
id: 'keep-message',
|
||||
roomId: 'room-1',
|
||||
senderId: 'user-1',
|
||||
senderName: 'User',
|
||||
content: 'keep',
|
||||
timestamp: 100,
|
||||
role: 'user',
|
||||
})
|
||||
|
||||
storage.pruneMessages('room-1', 1)
|
||||
|
||||
expectWorkspaceChangeDeleted(payload.change_id)
|
||||
expect(countRows('gc_messages', ' WHERE id = ?', saved!.message.id)).toBe(0)
|
||||
expect(countRows('gc_messages', ' WHERE id = ?', 'keep-message')).toBe(1)
|
||||
server.getIO().close()
|
||||
})
|
||||
|
||||
it('rolls back diff rows when the group message insert fails', async () => {
|
||||
const { GroupChatServer } = await import('../../packages/server/src/services/hermes/group-chat')
|
||||
const server = new GroupChatServer(httpServer)
|
||||
const storage = server.getStorage()
|
||||
storage.saveRoom('room-1', 'Room 1')
|
||||
const draft = await makeDraft('fedcba9876543210fedcba9876543210')
|
||||
dbState.db?.exec(`CREATE TRIGGER fail_workspace_diff_message
|
||||
BEFORE INSERT ON gc_messages
|
||||
WHEN NEW.tool_name = 'workspace_diff'
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'message failed');
|
||||
END`)
|
||||
|
||||
expect(() => storage.saveWorkspaceDiffMessageForRun({
|
||||
roomId: 'room-1',
|
||||
senderId: 'agent-1',
|
||||
senderName: 'Worker',
|
||||
sessionId: 'session-1',
|
||||
runId: 'fedcba9876543210fedcba9876543210',
|
||||
status: 'failed',
|
||||
workspace,
|
||||
draft: draft!,
|
||||
})).toThrow('message failed')
|
||||
|
||||
expect((dbState.db?.prepare('SELECT COUNT(*) AS count FROM workspace_run_changes').get() as { count: number }).count).toBe(0)
|
||||
expect((dbState.db?.prepare('SELECT COUNT(*) AS count FROM workspace_run_change_files').get() as { count: number }).count).toBe(0)
|
||||
expect((dbState.db?.prepare('SELECT COUNT(*) AS count FROM gc_messages WHERE tool_name = ?').get('workspace_diff') as { count: number }).count).toBe(0)
|
||||
server.getIO().close()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user