mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-10 02:30:15 +00:00
206 lines
7.3 KiB
TypeScript
206 lines
7.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { DatabaseSync } from 'node:sqlite'
|
|
import { execFileSync } from 'child_process'
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { tmpdir } from 'os'
|
|
|
|
const state = vi.hoisted(() => ({
|
|
db: null as DatabaseSync | null,
|
|
appHome: '',
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/db/index', () => ({
|
|
getDb: () => state.db,
|
|
isSqliteAvailable: () => Boolean(state.db),
|
|
jsonDelete: vi.fn(),
|
|
jsonGet: vi.fn(),
|
|
jsonGetAll: vi.fn(() => ({})),
|
|
jsonSet: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/config', () => ({
|
|
config: {
|
|
appHome: state.appHome,
|
|
},
|
|
}))
|
|
|
|
function git(cwd: string, args: string[]): void {
|
|
execFileSync('git', args, { cwd, stdio: 'ignore' })
|
|
}
|
|
|
|
describe('workspace diff tracker', () => {
|
|
let root: string
|
|
let repo: string
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
root = mkdtempSync(join(tmpdir(), 'hermes-workspace-diff-'))
|
|
state.appHome = join(root, 'home')
|
|
state.db = new DatabaseSync(join(root, 'diffs.db'))
|
|
const { initAllHermesTables } = await import('../../packages/server/src/db/hermes/schemas')
|
|
initAllHermesTables()
|
|
|
|
repo = join(root, 'repo')
|
|
mkdirSync(repo)
|
|
git(repo, ['init'])
|
|
git(repo, ['config', 'user.email', 'test@example.com'])
|
|
git(repo, ['config', 'user.name', 'Test User'])
|
|
writeFileSync(join(repo, 'dirty.txt'), 'committed\n')
|
|
writeFileSync(join(repo, 'changed.txt'), 'old\n')
|
|
git(repo, ['add', '.'])
|
|
git(repo, ['commit', '-m', 'initial'])
|
|
})
|
|
|
|
afterEach(() => {
|
|
state.db?.close()
|
|
state.db = null
|
|
rmSync(root, { recursive: true, force: true })
|
|
})
|
|
|
|
it('records only files changed during the run when the repo was already dirty', async () => {
|
|
const {
|
|
completeWorkspaceRunCheckpoint,
|
|
startWorkspaceRunCheckpoint,
|
|
} = await import('../../packages/server/src/services/hermes/run-chat/workspace-diff-tracker')
|
|
|
|
writeFileSync(join(repo, 'dirty.txt'), 'preexisting dirty change\n')
|
|
startWorkspaceRunCheckpoint({
|
|
sessionId: 'session-1',
|
|
runId: 'run-1',
|
|
workspace: repo,
|
|
})
|
|
|
|
writeFileSync(join(repo, 'changed.txt'), 'new\n')
|
|
const change = completeWorkspaceRunCheckpoint({
|
|
sessionId: 'session-1',
|
|
runId: 'run-1',
|
|
workspace: repo,
|
|
})
|
|
|
|
expect(change).not.toBeNull()
|
|
expect(change?.change_id).toMatch(/^run:run-1:/)
|
|
expect(change?.files.map(file => file.path)).toEqual(['changed.txt'])
|
|
expect(change?.files[0]).toMatchObject({
|
|
change_type: 'modified',
|
|
additions: 1,
|
|
deletions: 1,
|
|
binary: false,
|
|
})
|
|
expect(change?.files[0].patch).toBeUndefined()
|
|
|
|
const { getWorkspaceRunChangeFile, listWorkspaceRunChangesForSession } = await import('../../packages/server/src/db/hermes/workspace-run-changes-store')
|
|
const detail = getWorkspaceRunChangeFile('session-1', change!.change_id, change!.files[0].id)
|
|
expect(detail?.patch).toContain('-old')
|
|
expect(detail?.patch).toContain('+new')
|
|
|
|
startWorkspaceRunCheckpoint({
|
|
sessionId: 'session-1',
|
|
runId: 'run-1',
|
|
workspace: repo,
|
|
})
|
|
writeFileSync(join(repo, 'changed.txt'), 'newer\n')
|
|
const secondChange = completeWorkspaceRunCheckpoint({
|
|
sessionId: 'session-1',
|
|
runId: 'run-1',
|
|
workspace: repo,
|
|
})
|
|
|
|
expect(secondChange).not.toBeNull()
|
|
expect(secondChange?.change_id).toMatch(/^run:run-1:/)
|
|
expect(secondChange?.change_id).not.toBe(change?.change_id)
|
|
|
|
const savedChanges = listWorkspaceRunChangesForSession('session-1')
|
|
expect(savedChanges).toHaveLength(2)
|
|
expect(savedChanges.map(saved => saved.change_id)).toEqual(expect.arrayContaining([
|
|
change!.change_id,
|
|
secondChange!.change_id,
|
|
]))
|
|
})
|
|
|
|
it('records added, modified, and deleted files in non-git workspaces', async () => {
|
|
const {
|
|
completeWorkspaceRunCheckpoint,
|
|
startWorkspaceRunCheckpoint,
|
|
} = await import('../../packages/server/src/services/hermes/run-chat/workspace-diff-tracker')
|
|
|
|
const workspace = join(root, 'plain-workspace')
|
|
mkdirSync(workspace)
|
|
writeFileSync(join(workspace, 'deleted.txt'), 'remove me\n')
|
|
writeFileSync(join(workspace, 'old.txt'), 'old\n')
|
|
writeFileSync(join(workspace, 'unchanged.txt'), 'same\n')
|
|
|
|
startWorkspaceRunCheckpoint({
|
|
sessionId: 'session-plain',
|
|
runId: 'run-plain',
|
|
workspace,
|
|
})
|
|
|
|
rmSync(join(workspace, 'deleted.txt'))
|
|
writeFileSync(join(workspace, 'added.txt'), 'added\n')
|
|
writeFileSync(join(workspace, 'old.txt'), 'new\n')
|
|
const change = completeWorkspaceRunCheckpoint({
|
|
sessionId: 'session-plain',
|
|
runId: 'run-plain',
|
|
workspace,
|
|
})
|
|
|
|
expect(change).not.toBeNull()
|
|
expect(change?.workspace_kind).toBe('filesystem')
|
|
expect(change?.files.map(file => file.path)).toEqual(['added.txt', 'deleted.txt', 'old.txt'])
|
|
expect(change?.files.map(file => [file.path, file.change_type])).toEqual([
|
|
['added.txt', 'added'],
|
|
['deleted.txt', 'deleted'],
|
|
['old.txt', 'modified'],
|
|
])
|
|
|
|
const { getWorkspaceRunChangeFile } = await import('../../packages/server/src/db/hermes/workspace-run-changes-store')
|
|
const modified = change!.files.find(file => file.path === 'old.txt')!
|
|
const detail = getWorkspaceRunChangeFile('session-plain', change!.change_id, modified.id)
|
|
expect(detail?.patch).toContain('-old')
|
|
expect(detail?.patch).toContain('+new')
|
|
})
|
|
|
|
it('skips common language dependency and build directories in non-git workspaces', async () => {
|
|
const {
|
|
completeWorkspaceRunCheckpoint,
|
|
startWorkspaceRunCheckpoint,
|
|
} = await import('../../packages/server/src/services/hermes/run-chat/workspace-diff-tracker')
|
|
|
|
const workspace = join(root, 'plain-with-language-artifacts')
|
|
mkdirSync(join(workspace, 'src'), { recursive: true })
|
|
mkdirSync(join(workspace, 'node_modules'), { recursive: true })
|
|
mkdirSync(join(workspace, '__pycache__'), { recursive: true })
|
|
mkdirSync(join(workspace, 'target'), { recursive: true })
|
|
mkdirSync(join(workspace, 'vendor'), { recursive: true })
|
|
mkdirSync(join(workspace, '.terraform'), { recursive: true })
|
|
writeFileSync(join(workspace, 'src', 'app.py'), 'old\n')
|
|
writeFileSync(join(workspace, 'node_modules', 'ignored.js'), 'before\n')
|
|
writeFileSync(join(workspace, '__pycache__', 'ignored.pyc'), 'before\n')
|
|
writeFileSync(join(workspace, 'target', 'ignored.class'), 'before\n')
|
|
writeFileSync(join(workspace, 'vendor', 'ignored.php'), 'before\n')
|
|
writeFileSync(join(workspace, '.terraform', 'ignored.tfstate'), 'before\n')
|
|
|
|
startWorkspaceRunCheckpoint({
|
|
sessionId: 'session-ignore',
|
|
runId: 'run-ignore',
|
|
workspace,
|
|
})
|
|
|
|
writeFileSync(join(workspace, 'src', 'app.py'), 'new\n')
|
|
writeFileSync(join(workspace, 'node_modules', 'ignored.js'), 'after\n')
|
|
writeFileSync(join(workspace, '__pycache__', 'ignored.pyc'), 'after\n')
|
|
writeFileSync(join(workspace, 'target', 'ignored.class'), 'after\n')
|
|
writeFileSync(join(workspace, 'vendor', 'ignored.php'), 'after\n')
|
|
writeFileSync(join(workspace, '.terraform', 'ignored.tfstate'), 'after\n')
|
|
const change = completeWorkspaceRunCheckpoint({
|
|
sessionId: 'session-ignore',
|
|
runId: 'run-ignore',
|
|
workspace,
|
|
})
|
|
|
|
expect(change).not.toBeNull()
|
|
expect(change?.files.map(file => file.path)).toEqual(['src/app.py'])
|
|
})
|
|
})
|