claude went crazy
All checks were successful
Build / test (push) Successful in 9m28s
Build / release (push) Successful in 1s
Build / build (push) Successful in 25s

This commit is contained in:
2026-03-28 02:35:00 -04:00
parent d7d4bbc099
commit 6e40413385
22 changed files with 2167 additions and 787 deletions

239
tests/api.test.js Normal file
View File

@@ -0,0 +1,239 @@
import { describe, it, expect, beforeEach } from 'vitest'
import request from 'supertest'
import { app } from '../server/server.js'
import { _resetForTest } from '../server/db.js'
beforeEach(() => _resetForTest())
const base = {
name: 'traefik',
vmid: 100,
state: 'deployed',
stack: 'production',
atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0,
tailscale_ip: '',
hardware_acceleration: 0,
}
// ── GET /api/instances ────────────────────────────────────────────────────────
describe('GET /api/instances', () => {
it('returns empty array when no instances exist', async () => {
const res = await request(app).get('/api/instances')
expect(res.status).toBe(200)
expect(res.body).toEqual([])
})
it('returns all instances sorted by name', async () => {
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'zebra' })
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'alpha' })
const res = await request(app).get('/api/instances')
expect(res.status).toBe(200)
expect(res.body).toHaveLength(2)
expect(res.body[0].name).toBe('alpha')
expect(res.body[1].name).toBe('zebra')
})
it('filters by state', async () => {
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'a', state: 'deployed' })
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'b', state: 'degraded' })
const res = await request(app).get('/api/instances?state=deployed')
expect(res.body).toHaveLength(1)
expect(res.body[0].name).toBe('a')
})
it('filters by stack', async () => {
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'a', stack: 'production' })
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'b', stack: 'development', state: 'testing' })
const res = await request(app).get('/api/instances?stack=development')
expect(res.body).toHaveLength(1)
expect(res.body[0].name).toBe('b')
})
it('searches by name substring', async () => {
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'plex' })
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'gitea' })
const res = await request(app).get('/api/instances?search=ple')
expect(res.body).toHaveLength(1)
expect(res.body[0].name).toBe('plex')
})
it('searches by vmid', async () => {
await request(app).post('/api/instances').send({ ...base, vmid: 137, name: 'a' })
await request(app).post('/api/instances').send({ ...base, vmid: 200, name: 'b' })
const res = await request(app).get('/api/instances?search=137')
expect(res.body).toHaveLength(1)
expect(res.body[0].vmid).toBe(137)
})
it('combines search and state filters', async () => {
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'plex', state: 'deployed' })
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'plex2', state: 'degraded' })
const res = await request(app).get('/api/instances?search=plex&state=deployed')
expect(res.body).toHaveLength(1)
expect(res.body[0].name).toBe('plex')
})
})
// ── GET /api/instances/stacks ─────────────────────────────────────────────────
describe('GET /api/instances/stacks', () => {
it('returns empty array when no instances exist', async () => {
const res = await request(app).get('/api/instances/stacks')
expect(res.status).toBe(200)
expect(res.body).toEqual([])
})
it('returns unique stacks sorted alphabetically', async () => {
await request(app).post('/api/instances').send({ ...base, vmid: 1, name: 'a', stack: 'production' })
await request(app).post('/api/instances').send({ ...base, vmid: 2, name: 'b', stack: 'development', state: 'testing' })
await request(app).post('/api/instances').send({ ...base, vmid: 3, name: 'c', stack: 'production' })
const res = await request(app).get('/api/instances/stacks')
expect(res.body).toEqual(['development', 'production'])
})
})
// ── GET /api/instances/:vmid ──────────────────────────────────────────────────
describe('GET /api/instances/:vmid', () => {
it('returns the instance for a known vmid', async () => {
await request(app).post('/api/instances').send({ ...base, vmid: 117, name: 'plex' })
const res = await request(app).get('/api/instances/117')
expect(res.status).toBe(200)
expect(res.body.name).toBe('plex')
expect(res.body.vmid).toBe(117)
})
it('returns 404 for unknown vmid', async () => {
const res = await request(app).get('/api/instances/999')
expect(res.status).toBe(404)
expect(res.body.error).toBeDefined()
})
it('returns 400 for non-numeric vmid', async () => {
const res = await request(app).get('/api/instances/abc')
expect(res.status).toBe(400)
})
})
// ── POST /api/instances ───────────────────────────────────────────────────────
describe('POST /api/instances', () => {
it('creates an instance and returns 201 with the created record', async () => {
const res = await request(app).post('/api/instances').send(base)
expect(res.status).toBe(201)
expect(res.body.name).toBe('traefik')
expect(res.body.vmid).toBe(100)
expect(res.body.created_at).not.toBeNull()
expect(res.body.updated_at).not.toBeNull()
})
it('stores service flags correctly', async () => {
const res = await request(app).post('/api/instances').send({ ...base, atlas: 1, tailscale: 1, hardware_acceleration: 1 })
expect(res.body.atlas).toBe(1)
expect(res.body.tailscale).toBe(1)
expect(res.body.hardware_acceleration).toBe(1)
expect(res.body.argus).toBe(0)
})
it('returns 409 for duplicate vmid', async () => {
await request(app).post('/api/instances').send(base)
const res = await request(app).post('/api/instances').send({ ...base, name: 'other' })
expect(res.status).toBe(409)
expect(res.body.error).toMatch(/vmid/)
})
it('returns 400 when name is missing', async () => {
const res = await request(app).post('/api/instances').send({ ...base, name: '' })
expect(res.status).toBe(400)
expect(res.body.errors).toBeInstanceOf(Array)
})
it('returns 400 for vmid less than 1', async () => {
const res = await request(app).post('/api/instances').send({ ...base, vmid: 0 })
expect(res.status).toBe(400)
expect(res.body.errors).toBeInstanceOf(Array)
})
it('returns 400 for invalid state', async () => {
const res = await request(app).post('/api/instances').send({ ...base, state: 'invalid' })
expect(res.status).toBe(400)
})
it('returns 400 for invalid stack', async () => {
const res = await request(app).post('/api/instances').send({ ...base, stack: 'invalid' })
expect(res.status).toBe(400)
})
it('trims whitespace from name', async () => {
const res = await request(app).post('/api/instances').send({ ...base, name: ' plex ' })
expect(res.status).toBe(201)
expect(res.body.name).toBe('plex')
})
})
// ── PUT /api/instances/:vmid ──────────────────────────────────────────────────
describe('PUT /api/instances/:vmid', () => {
it('updates fields and returns the updated record', async () => {
await request(app).post('/api/instances').send(base)
const res = await request(app).put('/api/instances/100').send({ ...base, name: 'updated', state: 'degraded' })
expect(res.status).toBe(200)
expect(res.body.name).toBe('updated')
expect(res.body.state).toBe('degraded')
})
it('can change the vmid', async () => {
await request(app).post('/api/instances').send(base)
await request(app).put('/api/instances/100').send({ ...base, vmid: 200 })
expect((await request(app).get('/api/instances/100')).status).toBe(404)
expect((await request(app).get('/api/instances/200')).status).toBe(200)
})
it('returns 404 for unknown vmid', async () => {
const res = await request(app).put('/api/instances/999').send(base)
expect(res.status).toBe(404)
})
it('returns 400 for validation errors', async () => {
await request(app).post('/api/instances').send(base)
const res = await request(app).put('/api/instances/100').send({ ...base, name: '' })
expect(res.status).toBe(400)
expect(res.body.errors).toBeInstanceOf(Array)
})
it('returns 409 when new vmid conflicts with an existing instance', async () => {
await request(app).post('/api/instances').send({ ...base, vmid: 100, name: 'a' })
await request(app).post('/api/instances').send({ ...base, vmid: 200, name: 'b' })
const res = await request(app).put('/api/instances/100').send({ ...base, vmid: 200 })
expect(res.status).toBe(409)
})
})
// ── DELETE /api/instances/:vmid ───────────────────────────────────────────────
describe('DELETE /api/instances/:vmid', () => {
it('deletes a development instance and returns 204', async () => {
await request(app).post('/api/instances').send({ ...base, stack: 'development', state: 'testing' })
const res = await request(app).delete('/api/instances/100')
expect(res.status).toBe(204)
expect((await request(app).get('/api/instances/100')).status).toBe(404)
})
it('returns 422 when attempting to delete a production instance', async () => {
await request(app).post('/api/instances').send({ ...base, stack: 'production' })
const res = await request(app).delete('/api/instances/100')
expect(res.status).toBe(422)
expect(res.body.error).toMatch(/development/)
})
it('returns 404 for unknown vmid', async () => {
const res = await request(app).delete('/api/instances/999')
expect(res.status).toBe(404)
})
it('returns 400 for non-numeric vmid', async () => {
const res = await request(app).delete('/api/instances/abc')
expect(res.status).toBe(400)
})
})

View File

@@ -1,250 +1,167 @@
import { describe, it, expect, beforeEach } from 'vitest'
import initSqlJs from 'sql.js'
import {
_resetForTest,
getInstances, getInstance, getDistinctStacks,
createInstance, updateInstance, deleteInstance,
} from '../server/db.js'
// ── Schema (mirrors db.js) ────────────────────────────────────────────────────
beforeEach(() => _resetForTest());
const SCHEMA = `
CREATE TABLE instances (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
state TEXT DEFAULT 'deployed',
stack TEXT DEFAULT '',
vmid INTEGER UNIQUE NOT NULL,
atlas INTEGER DEFAULT 0,
argus INTEGER DEFAULT 0,
semaphore INTEGER DEFAULT 0,
patchmon INTEGER DEFAULT 0,
tailscale INTEGER DEFAULT 0,
andromeda INTEGER DEFAULT 0,
tailscale_ip TEXT DEFAULT '',
hardware_acceleration INTEGER DEFAULT 0,
createdAt TEXT DEFAULT (datetime('now')),
updatedAt TEXT DEFAULT (datetime('now'))
)
`
// ── Helpers ───────────────────────────────────────────────────────────────────
let db
beforeEach(async () => {
const SQL = await initSqlJs()
db = new SQL.Database()
db.run(SCHEMA)
})
function rows(res) {
if (!res.length) return []
const cols = res[0].columns
return res[0].values.map(row => Object.fromEntries(cols.map((c, i) => [c, row[i]])))
}
function insert(overrides = {}) {
const defaults = {
name: 'test-instance', state: 'deployed', stack: 'production', vmid: 100,
atlas: 0, argus: 0, semaphore: 0, patchmon: 0,
tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0,
}
const d = { ...defaults, ...overrides }
db.run(
`INSERT INTO instances
(name, state, stack, vmid, atlas, argus, semaphore, patchmon, tailscale, andromeda, tailscale_ip, hardware_acceleration)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[d.name, d.state, d.stack, d.vmid, d.atlas, d.argus, d.semaphore,
d.patchmon, d.tailscale, d.andromeda, d.tailscale_ip, d.hardware_acceleration]
)
return d
}
function getInstances(filters = {}) {
let sql = 'SELECT * FROM instances WHERE 1=1'
const params = []
if (filters.search) {
sql += ' AND (name LIKE ? OR CAST(vmid AS TEXT) LIKE ? OR stack LIKE ?)'
const s = `%${filters.search}%`
params.push(s, s, s)
}
if (filters.state) { sql += ' AND state = ?'; params.push(filters.state) }
if (filters.stack) { sql += ' AND stack = ?'; params.push(filters.stack) }
sql += ' ORDER BY name ASC'
return rows(db.exec(sql, params))
}
function getInstance(vmid) {
const res = rows(db.exec('SELECT * FROM instances WHERE vmid = ?', [vmid]))
return res[0] ?? null
}
function getDistinctStacks() {
const res = db.exec(`SELECT DISTINCT stack FROM instances WHERE stack != '' ORDER BY stack`)
if (!res.length) return []
return res[0].values.map(r => r[0])
}
// ── Tests ─────────────────────────────────────────────────────────────────────
// ── getInstances ──────────────────────────────────────────────────────────────
describe('getInstances', () => {
it('returns empty array when no instances exist', () => {
expect(getInstances()).toEqual([])
})
it('returns empty array when table is empty', () => {
expect(getInstances()).toEqual([]);
});
it('returns all instances sorted by name', () => {
insert({ name: 'zebra', vmid: 1 })
insert({ name: 'alpha', vmid: 2 })
const result = getInstances()
expect(result).toHaveLength(2)
expect(result[0].name).toBe('alpha')
expect(result[1].name).toBe('zebra')
})
createInstance({ name: 'zebra', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
createInstance({ name: 'alpha', state: 'deployed', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
const result = getInstances();
expect(result[0].name).toBe('alpha');
expect(result[1].name).toBe('zebra');
});
it('filters by state', () => {
insert({ name: 'a', vmid: 1, state: 'deployed' })
insert({ name: 'b', vmid: 2, state: 'degraded' })
insert({ name: 'c', vmid: 3, state: 'testing' })
expect(getInstances({ state: 'deployed' })).toHaveLength(1)
expect(getInstances({ state: 'degraded' })).toHaveLength(1)
expect(getInstances({ state: 'testing' })).toHaveLength(1)
})
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
createInstance({ name: 'b', state: 'degraded', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
createInstance({ name: 'c', state: 'testing', stack: 'development', vmid: 3, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
expect(getInstances({ state: 'deployed' })).toHaveLength(1);
expect(getInstances({ state: 'degraded' })).toHaveLength(1);
expect(getInstances({ state: 'testing' })).toHaveLength(1);
});
it('filters by stack', () => {
insert({ name: 'a', vmid: 1, stack: 'production' })
insert({ name: 'b', vmid: 2, stack: 'development' })
expect(getInstances({ stack: 'production' })).toHaveLength(1)
expect(getInstances({ stack: 'development' })).toHaveLength(1)
})
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
createInstance({ name: 'b', state: 'testing', stack: 'development', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
expect(getInstances({ stack: 'production' })).toHaveLength(1);
expect(getInstances({ stack: 'development' })).toHaveLength(1);
});
it('searches by name', () => {
insert({ name: 'plex', vmid: 1 })
insert({ name: 'gitea', vmid: 2 })
expect(getInstances({ search: 'ple' })).toHaveLength(1)
expect(getInstances({ search: 'ple' })[0].name).toBe('plex')
})
createInstance({ name: 'plex', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
createInstance({ name: 'gitea', state: 'deployed', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
expect(getInstances({ search: 'ple' })).toHaveLength(1);
expect(getInstances({ search: 'ple' })[0].name).toBe('plex');
});
it('searches by vmid', () => {
insert({ name: 'a', vmid: 137 })
insert({ name: 'b', vmid: 200 })
expect(getInstances({ search: '137' })).toHaveLength(1)
})
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 137, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
createInstance({ name: 'b', state: 'deployed', stack: 'production', vmid: 200, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
expect(getInstances({ search: '137' })).toHaveLength(1);
});
it('searches by stack', () => {
insert({ name: 'a', vmid: 1, stack: 'production' })
insert({ name: 'b', vmid: 2, stack: 'development' })
expect(getInstances({ search: 'prod' })).toHaveLength(1)
})
it('combines filters', () => {
createInstance({ name: 'plex', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
createInstance({ name: 'plex2', state: 'degraded', stack: 'production', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
expect(getInstances({ search: 'plex', state: 'deployed' })).toHaveLength(1);
});
});
it('combines search and state filters', () => {
insert({ name: 'plex', vmid: 1, state: 'deployed' })
insert({ name: 'plex2', vmid: 2, state: 'degraded' })
expect(getInstances({ search: 'plex', state: 'deployed' })).toHaveLength(1)
})
it('returns empty array when no results match', () => {
insert({ name: 'plex', vmid: 1 })
expect(getInstances({ search: 'zzz' })).toEqual([])
})
})
// ── getInstance ───────────────────────────────────────────────────────────────
describe('getInstance', () => {
it('returns the instance with the given vmid', () => {
insert({ name: 'plex', vmid: 117 })
const inst = getInstance(117)
expect(inst).not.toBeNull()
expect(inst.name).toBe('plex')
expect(inst.vmid).toBe(117)
})
createInstance({ name: 'plex', state: 'deployed', stack: 'production', vmid: 117, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
const inst = getInstance(117);
expect(inst).not.toBeNull();
expect(inst.name).toBe('plex');
expect(inst.vmid).toBe(117);
});
it('returns null for an unknown vmid', () => {
expect(getInstance(999)).toBeNull()
})
})
it('returns null for unknown vmid', () => {
expect(getInstance(999)).toBeNull();
});
});
// ── getDistinctStacks ─────────────────────────────────────────────────────────
describe('getDistinctStacks', () => {
it('returns empty array when no instances exist', () => {
expect(getDistinctStacks()).toEqual([])
})
it('returns empty array when table is empty', () => {
expect(getDistinctStacks()).toEqual([]);
});
it('returns unique stacks sorted alphabetically', () => {
insert({ vmid: 1, stack: 'production' })
insert({ vmid: 2, stack: 'development' })
insert({ vmid: 3, stack: 'production' })
expect(getDistinctStacks()).toEqual(['development', 'production'])
})
createInstance({ name: 'a', state: 'deployed', stack: 'production', vmid: 1, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
createInstance({ name: 'b', state: 'testing', stack: 'development', vmid: 2, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
createInstance({ name: 'c', state: 'deployed', stack: 'production', vmid: 3, atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 });
expect(getDistinctStacks()).toEqual(['development', 'production']);
});
});
it('excludes blank stack values', () => {
insert({ vmid: 1, stack: '' })
insert({ vmid: 2, stack: 'production' })
expect(getDistinctStacks()).toEqual(['production'])
})
})
// ── createInstance ────────────────────────────────────────────────────────────
describe('createInstance', () => {
it('inserts a new instance', () => {
insert({ name: 'traefik', vmid: 100, stack: 'production', state: 'deployed' })
const inst = getInstance(100)
expect(inst.name).toBe('traefik')
expect(inst.stack).toBe('production')
expect(inst.state).toBe('deployed')
})
const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 };
it('inserts a new instance and sets timestamps', () => {
createInstance({ ...base, name: 'traefik', vmid: 100 });
const inst = getInstance(100);
expect(inst.name).toBe('traefik');
expect(inst.created_at).not.toBeNull();
expect(inst.updated_at).not.toBeNull();
});
it('stores service flags correctly', () => {
insert({ vmid: 1, atlas: 1, argus: 0, tailscale: 1, hardware_acceleration: 1 })
const inst = getInstance(1)
expect(inst.atlas).toBe(1)
expect(inst.argus).toBe(0)
expect(inst.tailscale).toBe(1)
expect(inst.hardware_acceleration).toBe(1)
})
createInstance({ ...base, name: 'plex', vmid: 1, atlas: 1, tailscale: 1, hardware_acceleration: 1 });
const inst = getInstance(1);
expect(inst.atlas).toBe(1);
expect(inst.argus).toBe(0);
expect(inst.tailscale).toBe(1);
expect(inst.hardware_acceleration).toBe(1);
});
it('rejects duplicate vmid', () => {
insert({ vmid: 100 })
expect(() => insert({ name: 'other', vmid: 100 })).toThrow()
})
createInstance({ ...base, name: 'a', vmid: 100 });
expect(() => createInstance({ ...base, name: 'b', vmid: 100 })).toThrow();
});
it('sets createdAt and updatedAt on insert', () => {
insert({ vmid: 1 })
const inst = getInstance(1)
expect(inst.createdAt).not.toBeNull()
expect(inst.updatedAt).not.toBeNull()
})
})
it('rejects invalid state', () => {
expect(() => createInstance({ ...base, name: 'a', vmid: 1, state: 'invalid' })).toThrow();
});
it('rejects invalid stack', () => {
expect(() => createInstance({ ...base, name: 'a', vmid: 1, stack: 'invalid' })).toThrow();
});
});
// ── updateInstance ────────────────────────────────────────────────────────────
describe('updateInstance', () => {
it('updates fields on an existing instance', () => {
insert({ name: 'old-name', vmid: 100, state: 'testing', stack: 'development' })
const before = getInstance(100)
db.run(
`UPDATE instances SET name=?, state=?, stack=?, updatedAt=datetime('now') WHERE id=?`,
['new-name', 'deployed', 'production', before.id]
)
const after = getInstance(100)
expect(after.name).toBe('new-name')
expect(after.state).toBe('deployed')
expect(after.stack).toBe('production')
})
const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 };
it('updates updatedAt on write', () => {
insert({ vmid: 1 })
const before = getInstance(1)
db.run(`UPDATE instances SET name=?, updatedAt=datetime('now') WHERE id=?`, ['updated', before.id])
const after = getInstance(1)
expect(after.updatedAt).not.toBeNull()
})
})
it('updates fields and refreshes updated_at', () => {
createInstance({ ...base, name: 'old', vmid: 100 });
updateInstance(100, { ...base, name: 'new', vmid: 100, state: 'degraded' });
const inst = getInstance(100);
expect(inst.name).toBe('new');
expect(inst.state).toBe('degraded');
});
it('can change vmid', () => {
createInstance({ ...base, name: 'a', vmid: 100 });
updateInstance(100, { ...base, name: 'a', vmid: 200 });
expect(getInstance(100)).toBeNull();
expect(getInstance(200)).not.toBeNull();
});
});
// ── deleteInstance ────────────────────────────────────────────────────────────
describe('deleteInstance', () => {
const base = { state: 'deployed', stack: 'production', atlas: 0, argus: 0, semaphore: 0, patchmon: 0, tailscale: 0, andromeda: 0, tailscale_ip: '', hardware_acceleration: 0 };
it('removes the instance', () => {
insert({ vmid: 1 })
const inst = getInstance(1)
db.run('DELETE FROM instances WHERE id = ?', [inst.id])
expect(getInstance(1)).toBeNull()
})
createInstance({ ...base, name: 'a', vmid: 1 });
deleteInstance(1);
expect(getInstance(1)).toBeNull();
});
it('only removes the targeted instance', () => {
insert({ name: 'a', vmid: 1 })
insert({ name: 'b', vmid: 2 })
const inst = getInstance(1)
db.run('DELETE FROM instances WHERE id = ?', [inst.id])
expect(getInstance(1)).toBeNull()
expect(getInstance(2)).not.toBeNull()
})
})
createInstance({ ...base, name: 'a', vmid: 1 });
createInstance({ ...base, name: 'b', vmid: 2 });
deleteInstance(1);
expect(getInstance(1)).toBeNull();
expect(getInstance(2)).not.toBeNull();
});
});

View File

@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import { describe, it, expect } from 'vitest'
// ── esc() ─────────────────────────────────────────────────────────────────────