database.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { existsSync } from 'node:fs'
  2. import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
  3. import { homedir, tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { afterEach, beforeEach, describe, expect, it } from 'vitest'
  6. import {
  7. EkkoDatabaseManager,
  8. EkkoDirectoryManager,
  9. resolveEkkoDatabasePath,
  10. resolveEkkoDataDirectory,
  11. } from '../../packages/ekko-agent/src'
  12. let webUiHome = ''
  13. beforeEach(async () => {
  14. webUiHome = await mkdtemp(join(tmpdir(), 'ekko-database-'))
  15. })
  16. afterEach(async () => {
  17. await rm(webUiHome, { recursive: true, force: true })
  18. })
  19. describe('EkkoDatabaseManager', () => {
  20. it('uses the generic Ekko data directory and database name', () => {
  21. expect(resolveEkkoDataDirectory({ baseDirectory: webUiHome })).toBe(join(webUiHome, '.ekko'))
  22. expect(resolveEkkoDatabasePath({ baseDirectory: webUiHome })).toBe(join(webUiHome, '.ekko', 'ekko.db'))
  23. expect(new EkkoDirectoryManager().baseDirectory).toBe(homedir())
  24. })
  25. it('initializes the Ekko root and only its skills feature directory', () => {
  26. const directories = new EkkoDirectoryManager(webUiHome)
  27. expect(existsSync(directories.rootDirectory)).toBe(false)
  28. expect(directories.initialize()).toEqual({
  29. baseDirectory: webUiHome,
  30. rootDirectory: join(webUiHome, '.ekko'),
  31. databasePath: join(webUiHome, '.ekko', 'ekko.db'),
  32. skillsDirectory: join(webUiHome, '.ekko', 'skills'),
  33. logsDirectory: join(webUiHome, '.ekko', 'logs'),
  34. })
  35. expect(existsSync(directories.skillsDirectory)).toBe(true)
  36. expect(existsSync(directories.logsDirectory)).toBe(false)
  37. expect(existsSync(directories.databasePath)).toBe(false)
  38. expect(directories.profileSkillsDirectory('work')).toBe(join(webUiHome, '.ekko', 'skills', 'work'))
  39. expect(directories.profileLogsDirectory('work')).toBe(join(webUiHome, '.ekko', 'logs', 'work'))
  40. expect(existsSync(join(webUiHome, '.ekko', 'skills', 'work'))).toBe(true)
  41. expect(existsSync(join(webUiHome, '.ekko', 'logs', 'work'))).toBe(true)
  42. expect(existsSync(join(webUiHome, '.ekko', 'skills', 'work', '.ekko-backups'))).toBe(false)
  43. expect(existsSync(join(webUiHome, '.ekko', 'skills', 'work', '.ekko-archive'))).toBe(false)
  44. })
  45. it('imports every Hermes profile skill only when the Ekko skills directory is first created', async () => {
  46. const hermesRoot = join(webUiHome, 'hermes')
  47. const ekkoBase = join(webUiHome, 'web-ui')
  48. await mkdir(join(hermesRoot, 'skills', 'default-skill'), { recursive: true })
  49. await mkdir(join(hermesRoot, 'profiles', 'work', 'skills', 'work-skill', 'references'), { recursive: true })
  50. await writeFile(join(hermesRoot, 'skills', 'default-skill', 'SKILL.md'), '# Default\nOriginal.\n')
  51. await writeFile(join(hermesRoot, 'profiles', 'work', 'skills', 'work-skill', 'SKILL.md'), '# Work\nProfile skill.\n')
  52. await writeFile(
  53. join(hermesRoot, 'profiles', 'work', 'skills', 'work-skill', 'references', 'guide.md'),
  54. 'Imported support file.\n',
  55. )
  56. const directories = new EkkoDirectoryManager(ekkoBase)
  57. directories.initialize({ hermesRootDirectory: hermesRoot })
  58. await expect(readFile(
  59. join(ekkoBase, '.ekko', 'skills', 'default', 'default-skill', 'SKILL.md'),
  60. 'utf8',
  61. )).resolves.toBe('# Default\nOriginal.\n')
  62. await expect(readFile(
  63. join(ekkoBase, '.ekko', 'skills', 'work', 'work-skill', 'references', 'guide.md'),
  64. 'utf8',
  65. )).resolves.toBe('Imported support file.\n')
  66. await writeFile(join(hermesRoot, 'skills', 'default-skill', 'SKILL.md'), '# Default\nChanged later.\n')
  67. await mkdir(join(hermesRoot, 'skills', 'late-skill'), { recursive: true })
  68. await writeFile(join(hermesRoot, 'skills', 'late-skill', 'SKILL.md'), '# Late\nDo not import.\n')
  69. directories.initialize({ hermesRootDirectory: hermesRoot })
  70. await expect(readFile(
  71. join(ekkoBase, '.ekko', 'skills', 'default', 'default-skill', 'SKILL.md'),
  72. 'utf8',
  73. )).resolves.toBe('# Default\nOriginal.\n')
  74. expect(existsSync(join(ekkoBase, '.ekko', 'skills', 'default', 'late-skill'))).toBe(false)
  75. })
  76. it('uses the same .ekko database path with development SQLite settings', () => {
  77. const options = { baseDirectory: webUiHome, env: { NODE_ENV: 'development' } }
  78. const manager = new EkkoDatabaseManager(options)
  79. expect(manager.connection.prepare('PRAGMA journal_mode').get()).toMatchObject({ journal_mode: 'delete' })
  80. expect(existsSync(join(webUiHome, '.ekko', 'ekko.db'))).toBe(true)
  81. manager.close()
  82. })
  83. it('owns the connection and component migrations', () => {
  84. const manager = new EkkoDatabaseManager({ baseDirectory: webUiHome })
  85. manager.migrate([{
  86. component: 'test-component',
  87. version: 1,
  88. migrate(database) {
  89. database.exec('CREATE TABLE test_records (id TEXT PRIMARY KEY)')
  90. },
  91. }])
  92. expect(existsSync(join(webUiHome, '.ekko', 'ekko.db'))).toBe(true)
  93. expect(manager.connection.prepare(
  94. 'SELECT component, version FROM schema_migrations WHERE component = ?',
  95. ).get('test-component')).toMatchObject({ component: 'test-component', version: 1 })
  96. manager.close()
  97. })
  98. it('rolls back failed transactions', () => {
  99. const manager = new EkkoDatabaseManager({ baseDirectory: webUiHome })
  100. manager.connection.exec('CREATE TABLE transaction_test (value TEXT)')
  101. expect(() => manager.transaction(() => {
  102. manager.connection.prepare('INSERT INTO transaction_test (value) VALUES (?)').run('temporary')
  103. throw new Error('rollback')
  104. })).toThrow('rollback')
  105. expect(manager.connection.prepare('SELECT value FROM transaction_test').all()).toEqual([])
  106. manager.close()
  107. })
  108. })
粤ICP备19079148号