bitsofchris_openaugi-obsidi.../docs/TESTING.md
Chris Lettieri a69b52281d testing docs
2026-02-25 08:04:57 -05:00

6.5 KiB

Testing Guide

Automated tests for the OpenAugi plugin. Run these before every release and when developing new features.

Running Tests

# Run all tests once
npm test

# Watch mode (re-runs on file changes)
npm run test:watch

Tests run in under 1 second. No Obsidian app, no API keys, no manual setup needed.

How It Works

The plugin runs inside Obsidian's Electron environment, but our tests don't need Obsidian at all. Instead:

  1. Mock Obsidian API (tests/mocks/) — A filesystem-backed mock that simulates Vault, MetadataCache, App, and TFile by reading real markdown files and parsing [[wikilinks]] to build link graphs.

  2. Test vault (tests/vault/) — A small set of markdown files committed to the repo that represent various note structures (links, backlinks, journal dates, dataview blocks, checkboxes, etc.).

  3. Vitest — Fast TypeScript test runner. Config in vitest.config.ts.

Test Files

File What it tests
tests/filename-utils.test.ts sanitizeFilename, BacklinkMapper, file collision handling
tests/openai-service.test.ts Prompt construction, context extraction, API response parsing
tests/distill-service.test.ts Link extraction, backlinks, content aggregation, journal filtering, dataview stripping
tests/file-service.test.ts File/folder creation, session folders, summaries, published posts
tests/context-gathering-service.test.ts BFS link traversal, depth limits, backlink discovery, character limits, folder exclusion

Adding Tests for a New Feature

1. Decide which test file

Match your feature to the service it lives in:

If you changed... Add tests to...
src/utils/filename-utils.ts tests/filename-utils.test.ts
src/services/openai-service.ts tests/openai-service.test.ts
src/services/distill-service.ts tests/distill-service.test.ts
src/services/file-service.ts tests/file-service.test.ts
src/services/context-gathering-service.ts tests/context-gathering-service.test.ts
New service Create tests/your-service.test.ts

2. Add test vault fixtures (if needed)

If your feature needs specific note content to test against, add markdown files to tests/vault/:

tests/vault/
├── Root Note.md              # Forward links to A and B
├── Linked Note A.md          # Links to Deep Note
├── Linked Note B.md          # Links back to A
├── Backlink Source.md        # Links TO Root Note
├── Journal Note.md           # Date headers (### YYYY-MM-DD)
├── Dataview Note.md          # ```dataview blocks
├── Collection Note.md        # Checkbox links [x] / [ ]
├── Context Note.md           # context: section
├── Special Characters!.md    # Filename sanitization edge case
├── Deeply Linked/
│   └── Deep Note.md          # Depth-2 traversal
└── Excluded Folder/
    └── Should Skip.md        # Folder exclusion

After adding a new fixture file, the mock MetadataCache automatically picks it up — it scans all .md files and parses their [[links]] on initialization.

3. Write the test

Pattern for pure unit tests (no Obsidian needed):

import { describe, it, expect } from 'vitest';
import { myFunction } from '../src/utils/my-utils';

describe('myFunction', () => {
  it('does the expected thing', () => {
    expect(myFunction('input')).toBe('output');
  });
});

Pattern for integration tests (using mock Obsidian API):

import { describe, it, expect, beforeEach } from 'vitest';
import { MyService } from '../src/services/my-service';
import { createMockApp, createTestTFile } from './mocks/obsidian-mock';
import * as path from 'path';

const VAULT_DIR = path.resolve(__dirname, 'vault');

describe('MyService', () => {
  let app: ReturnType<typeof createMockApp>;
  let service: MyService;

  beforeEach(() => {
    app = createMockApp(VAULT_DIR);
    service = new MyService(app as any, /* other deps */);
  });

  it('discovers linked notes', async () => {
    const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md');
    const result = await service.someMethod(rootFile);
    expect(result).toContain('expected');
  });
});

Pattern for testing OpenAI calls (mock fetch):

import { vi } from 'vitest';

it('calls API correctly', async () => {
  const mockResponse = {
    ok: true,
    json: async () => ({
      choices: [{ message: { content: '{"summary":"test"}', refusal: null } }]
    })
  };
  const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);

  const result = await service.someApiCall('input');
  expect(result.summary).toBe('test');

  fetchSpy.mockRestore();
});

Pattern for file output tests (uses temp directories):

import * as fs from 'fs';
import * as path from 'path';
import { MockApp } from './mocks/obsidian-mock';

const BASE_OUTPUT_DIR = path.resolve(__dirname, 'vault-output');
let outputDir: string;
let counter = 0;

beforeEach(() => {
  counter++;
  outputDir = path.join(BASE_OUTPUT_DIR, `run-${counter}-${Date.now()}`);
  fs.mkdirSync(outputDir, { recursive: true });
  // Point MockApp at the output dir, not the fixture vault
  app = new MockApp(outputDir);
});

4. Run and verify

npm test

What's NOT Tested (and why)

Area Reason
UI modals Tightly coupled to Obsidian DOM — test manually
Task dispatch (tmux) Requires system-level tmux — test manually
Dataview plugin queries Requires running Dataview plugin — mock returns empty
Real OpenAI API calls Costs money — we mock fetch instead

Mock Obsidian API Reference

The mock (tests/mocks/obsidian-mock.ts) supports:

Obsidian API Mock behavior
vault.read(file) Reads from filesystem
vault.create(path, content) Writes to filesystem
vault.createFolder(path) mkdir -p
vault.getMarkdownFiles() Walks directory tree
vault.getAbstractFileByPath(path) fs.existsSync lookup
vault.adapter.exists(path) fs.existsSync
vault.adapter.stat(path) fs.statSync
metadataCache.getFileCache(file) Parses [[links]] from file content
metadataCache.getFirstLinkpathDest(link, source) Resolves links by basename matching
metadataCache.resolvedLinks Pre-built link graph from all vault files

If a test needs an API that isn't mocked, add it to tests/mocks/obsidian-mock.ts.