Files
gnome-jarvis/test/obsidian.test.js
T
snxraven aa06c71fe1
Rolling release / release (push) Failing after 1m46s
obsidian
2026-09-14 11:24:10 -04:00

85 lines
6.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { ObsidianVault } from '../daemon/obsidian.js';
import { createObsidianTools } from '../skills/obsidian-tools.js';
import { voiceSettings } from '../daemon/voice-settings.js';
function fixture(t, memory = true) {
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvis-obsidian-'));
t.after(() => fs.rmSync(parent, { recursive: true, force: true }));
const root = path.join(parent, 'vault');
const vault = new ObsidianVault({ obsidianEnabled: true, obsidianMemoryEnabled: memory, obsidianVaultPath: root });
return { parent, root, vault };
}
test('default is off and performs no vault creation', t => {
const { root } = fixture(t); const settings = voiceSettings({ obsidianVaultPath: root }); const vault = new ObsidianVault(settings);
assert.equal(settings.obsidianEnabled, false); assert.equal(settings.obsidianMemoryEnabled, false);
assert.equal(vault.status().enabled, false); assert.deepEqual(createObsidianTools(vault), []);
assert.throws(() => vault.initialize(), /disabled/); assert.throws(() => vault.read('Welcome.md'), /disabled/);
assert.equal(fs.existsSync(root), false);
});
test('initialization is explicit, refuses nonempty directories, and verifies actual memory access', t => {
const { vault, root } = fixture(t);
assert.equal(vault.status().ready, false); assert.equal(fs.existsSync(root), false);
fs.mkdirSync(root); fs.writeFileSync(path.join(root, 'Personal.md'), 'private');
assert.throws(() => vault.initialize(), /empty directory/); fs.unlinkSync(path.join(root, 'Personal.md'));
assert.equal(vault.initialize().ready, true); assert.equal(vault.initialize().ready, true);
const result = vault.verify(); assert.equal(result.readWriteVerified, true); assert.equal(result.memoryVerified, true);
assert.deepEqual(fs.readdirSync(path.join(root, 'memory')), []);
assert.ok(result.uri.startsWith('obsidian://open?path='));
});
test('agent manages notes, attachments, folders, search, conflicts and trash restoration', t => {
const { vault } = fixture(t); vault.initialize();
const tool = createObsidianTools(vault)[0];
tool.execute({ action: 'mkdir', path: 'projects' });
const first = tool.execute({ action: 'write', path: 'projects/Plan.md', content: '---\ntags: [agent]\n---\n# Plan\n[[Welcome]]' });
assert.throws(() => vault.write(first.path, 'clobber'), /Revision conflict/);
const updated = vault.write(first.path, '## Revised [[Welcome]]', first.revision);
assert.throws(() => vault.remove(first.path, first.revision), /Revision conflict/);
assert.equal(vault.search('[[welcome]]').matches.length, 1);
vault.move(first.path, 'projects/Next.md', updated.revision);
assert.equal(vault.read('projects/Next.md').content, '## Revised [[Welcome]]');
const deleted = vault.remove('projects/Next.md', updated.revision);
assert.equal(vault.trash()[0].id, deleted.trashId);
vault.restore(deleted.trashId); assert.equal(vault.trash().length, 0);
const bytes = Buffer.from([0, 255, 12, 31]); vault.write('assets/image.png', bytes.toString('base64'), undefined, 'base64');
assert.equal(vault.read('assets/image.png', 'base64').content, bytes.toString('base64'));
vault.write('memory/user.md', 'Prefers tea'); assert.equal(vault.search('tea', { memory: true }).matches.length, 1);
assert.throws(() => vault.remove('projects'), /ENOTEMPTY/);
vault.mkdir('empty'); assert.deepEqual(vault.remove('empty'), { removedEmptyFolder: 'empty' });
});
test('traversal, hidden paths, symlinks, hardlinks and special files are not exposed', t => {
const { vault, parent, root } = fixture(t); vault.initialize();
for (const bad of ['../outside.md', '/tmp/outside.md', 'a/../../outside.md', '.obsidian/plugins/code.js', 'a/.hidden', 'a\\b', 'a//b']) assert.throws(() => vault.write(bad, 'bad'), /relative vault path/);
const outside = path.join(parent, 'outside.md'); fs.writeFileSync(outside, 'secret');
fs.symlinkSync(outside, path.join(root, 'link.md')); fs.linkSync(outside, path.join(root, 'hard.md'));
for (const name of ['link.md', 'hard.md']) { assert.throws(() => vault.read(name), /links/); assert.throws(() => vault.write(name, 'bad'), /links/); }
assert.equal(vault.list().entries.some(e => /link|hard/.test(e.path)), false);
fs.symlinkSync(parent, path.join(root, 'escape')); assert.throws(() => vault.mkdir('escape/new'), /links/);
assert.equal(fs.readFileSync(outside, 'utf8'), 'secret');
});
test('memory opt-out covers direct paths, listing, search, and trash; disabling revokes existing tool', t => {
const { vault, root } = fixture(t); vault.initialize();
const note = vault.write('memory/secret.md', 'secret'); const deleted = vault.remove(note.path, note.revision);
vault.write('memory/kept.md', 'secret'); const tool = createObsidianTools(vault)[0];
vault.configure({ obsidianEnabled: true, obsidianMemoryEnabled: false, obsidianVaultPath: root });
assert.throws(() => vault.read('memory/kept.md'), /memory access is disabled/);
assert.throws(() => vault.restore(deleted.trashId, 'leak.md'), /memory access is disabled/);
assert.equal(vault.trash().length, 0); assert.equal(vault.search('secret').matches.length, 0);
assert.equal(vault.verify().memoryVerified, false);
vault.configure({ obsidianEnabled: false, obsidianVaultPath: root });
assert.throws(() => tool.execute({ action: 'list' }), /disabled/);
});
test('oversized reads and writes, colliding moves and restore are rejected', t => {
const { vault, root } = fixture(t); vault.initialize();
assert.throws(() => vault.write('big.md', 'a'.repeat(2 * 1024 * 1024 + 1)), /limit/);
fs.writeFileSync(path.join(root, 'big.md'), Buffer.alloc(2 * 1024 * 1024 + 1)); assert.throws(() => vault.read('big.md'), /limit/);
const note = vault.write('a.md', 'a'); vault.write('b.md', 'b');
assert.throws(() => vault.move('a.md', 'b.md', note.revision), /already exists/);
const trash = vault.remove('a.md', note.revision); assert.throws(() => vault.restore(trash.trashId, 'b.md'), /already exists/);
});