feat(ext/sw): device management — devices.ts + router handlers

Adds readDevices, addDevice, revokeDevice helpers that read/write
.relicario/devices.json. Router handlers: list_devices, add_device,
revoke_device.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-26 15:53:08 -04:00
parent 5a001a805c
commit 0003c3e658
3 changed files with 142 additions and 4 deletions

View File

@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { readDevices, addDevice, revokeDevice } from '../devices';
import type { GitHost } from '../git-host';
function makeGitHost(devicesJson = '{"devices":[]}'): GitHost {
let stored = devicesJson;
return {
readFile: vi.fn().mockImplementation(async () => new TextEncoder().encode(stored)),
writeFile: vi.fn().mockImplementation(async (_p, bytes) => { stored = new TextDecoder().decode(bytes); }),
deleteFile: vi.fn(),
listDir: vi.fn(),
putBlob: vi.fn(),
getBlob: vi.fn(),
deleteBlob: vi.fn(),
};
}
describe('devices', () => {
it('readDevices returns empty array when file missing', async () => {
const host = makeGitHost();
(host.readFile as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('404'));
const result = await readDevices(host);
expect(result).toEqual([]);
});
it('readDevices parses existing devices', async () => {
const host = makeGitHost('{"devices":[{"name":"CLI","public_key":"abc123","added_at":1000}]}');
const result = await readDevices(host);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('CLI');
});
it('addDevice appends to list', async () => {
const host = makeGitHost();
await addDevice(host, { name: 'Chrome', public_key: 'def456', added_at: 2000 });
expect(host.writeFile).toHaveBeenCalled();
const written = (host.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1];
const parsed = JSON.parse(new TextDecoder().decode(written));
expect(parsed.devices).toHaveLength(1);
expect(parsed.devices[0].name).toBe('Chrome');
});
it('addDevice rejects duplicate name', async () => {
const host = makeGitHost('{"devices":[{"name":"Chrome","public_key":"abc","added_at":1000}]}');
await expect(addDevice(host, { name: 'Chrome', public_key: 'xyz', added_at: 2000 }))
.rejects.toThrow(/already exists/);
});
it('revokeDevice removes by name', async () => {
const host = makeGitHost('{"devices":[{"name":"CLI","public_key":"a","added_at":1},{"name":"Chrome","public_key":"b","added_at":2}]}');
await revokeDevice(host, 'CLI');
const written = (host.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1];
const parsed = JSON.parse(new TextDecoder().decode(written));
expect(parsed.devices).toHaveLength(1);
expect(parsed.devices[0].name).toBe('Chrome');
});
it('revokeDevice throws if not found', async () => {
const host = makeGitHost();
await expect(revokeDevice(host, 'nonexistent')).rejects.toThrow(/not found/);
});
});