From a69b52281d6f7543ed8afb4dd05685e0965b45f8 Mon Sep 17 00:00:00 2001 From: Chris Lettieri Date: Wed, 25 Feb 2026 08:04:57 -0500 Subject: [PATCH] testing docs --- CLAUDE.md | 13 +++- docs/PUBLISHING.md | 3 + docs/TESTING.md | 190 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 docs/TESTING.md diff --git a/CLAUDE.md b/CLAUDE.md index 740b9ef..20690b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,18 @@ There is no standalone `typecheck` script — `npm run build` runs `tsc -noEmit - No external runtime dependencies (only Obsidian API) ### Testing -Currently no automated tests. Manual testing through Obsidian's developer console. + +Automated test suite using Vitest with a mock Obsidian API. See [docs/TESTING.md](docs/TESTING.md) for full details. + +```bash +# Run all tests +npm test + +# Watch mode +npm run test:watch +``` + +Tests cover: filename utils, OpenAI prompt building, link extraction, BFS traversal, content aggregation, file output, journal filtering, backlink discovery. New features should include test coverage. ## API Integration diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index eb6590b..81725d4 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -7,6 +7,8 @@ This guide walks through the complete process of publishing a new OpenAugi plugi Before starting the release process: - [ ] All features/fixes are merged to master +- [ ] **Automated tests pass** (`npm test`) — see [TESTING.md](TESTING.md) +- [ ] New features have test coverage (add to `tests/`) - [ ] Code builds without errors (`npm run build`) - [ ] Type checking passes (`npm run typecheck`) - [ ] Manual testing completed in test vault (`/Users/chris/zk-for-testing`) @@ -110,6 +112,7 @@ Ensure all docs reflect the new release: ```bash # Full release flow (example for version 0.4.0) +npm test npm run build npm run typecheck diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..2a49c12 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,190 @@ +# Testing Guide + +Automated tests for the OpenAugi plugin. Run these before every release and when developing new features. + +## Running Tests + +```bash +# 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): + +```typescript +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): + +```typescript +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; + 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`): + +```typescript +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): + +```typescript +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 + +```bash +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`.