Files
hermes-ui/tests/server/workspace-diff-tracker.test.ts
T
toor 1e9ea89d86
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
2026-10-16 10:34:35 +08:00

376 lines
13 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 empty zero-byte file changes 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-empty-file')
mkdirSync(workspace)
startWorkspaceRunCheckpoint({
sessionId: 'session-empty',
runId: 'run-empty',
workspace,
})
writeFileSync(join(workspace, 'empty.txt'), '')
const emptyOnlyChange = completeWorkspaceRunCheckpoint({
sessionId: 'session-empty',
runId: 'run-empty',
workspace,
})
expect(emptyOnlyChange).toBeNull()
startWorkspaceRunCheckpoint({
sessionId: 'session-empty',
runId: 'run-non-empty',
workspace,
})
writeFileSync(join(workspace, 'non-empty.txt'), 'content\n')
const nonEmptyChange = completeWorkspaceRunCheckpoint({
sessionId: 'session-empty',
runId: 'run-non-empty',
workspace,
})
expect(nonEmptyChange).not.toBeNull()
expect(nonEmptyChange?.files.map(file => file.path)).toEqual(['non-empty.txt'])
})
it('does not save zero-line diffs that are not covered by filename filters', async () => {
const {
completeWorkspaceRunCheckpoint,
startWorkspaceRunCheckpoint,
} = await import('../../packages/server/src/services/hermes/run-chat/workspace-diff-tracker')
const workspace = join(root, 'plain-zero-line-diff')
mkdirSync(workspace)
writeFileSync(join(workspace, '.global-cache'), Buffer.from([0, 1, 2, 3]))
startWorkspaceRunCheckpoint({
sessionId: 'session-zero-line',
runId: 'run-zero-line',
workspace,
})
writeFileSync(join(workspace, '.global-cache'), Buffer.from([0, 4, 5, 6]))
writeFileSync(join(workspace, 'notes.md'), 'visible change\n')
const change = completeWorkspaceRunCheckpoint({
sessionId: 'session-zero-line',
runId: 'run-zero-line',
workspace,
})
expect(change).not.toBeNull()
expect(change?.files.map(file => file.path)).toEqual(['notes.md'])
expect(state.db?.prepare('SELECT COUNT(*) AS count FROM workspace_run_change_files WHERE additions = 0 AND deletions = 0').get()).toEqual({ count: 0 })
})
it('skips SQLite WAL and SHM sidecar 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-sqlite-sidecars')
mkdirSync(workspace)
startWorkspaceRunCheckpoint({
sessionId: 'session-sqlite-sidecars',
runId: 'run-sqlite-sidecars',
workspace,
})
writeFileSync(join(workspace, 'state.db-wal'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(workspace, 'state.db-shm'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(workspace, 'cache.sqlite-wal'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(workspace, 'cache.sqlite-shm'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(workspace, 'notes.md'), 'visible change\n')
const change = completeWorkspaceRunCheckpoint({
sessionId: 'session-sqlite-sidecars',
runId: 'run-sqlite-sidecars',
workspace,
})
expect(change).not.toBeNull()
expect(change?.files.map(file => file.path)).toEqual(['notes.md'])
})
it('skips SQLite WAL and SHM sidecar files in git workspaces', async () => {
const {
completeWorkspaceRunCheckpoint,
startWorkspaceRunCheckpoint,
} = await import('../../packages/server/src/services/hermes/run-chat/workspace-diff-tracker')
startWorkspaceRunCheckpoint({
sessionId: 'session-git-sqlite-sidecars',
runId: 'run-git-sqlite-sidecars',
workspace: repo,
})
writeFileSync(join(repo, 'state.db-wal'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(repo, 'state.db-shm'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(repo, 'cache.sqlite-wal'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(repo, 'cache.sqlite-shm'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(repo, 'notes.md'), 'visible change\n')
const change = completeWorkspaceRunCheckpoint({
sessionId: 'session-git-sqlite-sidecars',
runId: 'run-git-sqlite-sidecars',
workspace: repo,
})
expect(change).not.toBeNull()
expect(change?.files.map(file => file.path)).toEqual(['notes.md'])
})
it('records newly created ordinary files even when many unchanged files already exist 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-many-existing-files')
mkdirSync(workspace)
for (let index = 0; index < 120; index += 1) {
writeFileSync(join(workspace, `existing-${index.toString().padStart(3, '0')}.txt`), 'unchanged\n')
}
startWorkspaceRunCheckpoint({
sessionId: 'session-many-existing-files',
runId: 'run-many-existing-files',
workspace,
})
writeFileSync(join(workspace, 'README-visible.md'), '# visible new file\n')
writeFileSync(join(workspace, 'notes-visible.txt'), 'plain text visible\n')
writeFileSync(join(workspace, 'config-visible.json'), '{"visible": true}\n')
writeFileSync(join(workspace, 'state.db-wal'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(workspace, 'state.db-shm'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(workspace, 'cache.sqlite-wal'), Buffer.alloc(32 * 1024, 0))
writeFileSync(join(workspace, 'cache.sqlite-shm'), Buffer.alloc(32 * 1024, 0))
const change = completeWorkspaceRunCheckpoint({
sessionId: 'session-many-existing-files',
runId: 'run-many-existing-files',
workspace,
})
expect(change).not.toBeNull()
expect(change?.files).toHaveLength(3)
expect(change?.files.map(file => file.path)).toEqual(expect.arrayContaining([
'README-visible.md',
'config-visible.json',
'notes-visible.txt',
]))
})
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'])
})
})