test: add comprehensive document and collection command tests#24
test: add comprehensive document and collection command tests#24
Conversation
Adds dedicated test files for: - document.test.ts (21 tests): list, get, open, create, update, delete, move, archive, unarchive - collection.test.ts (18 tests): list, get, create, update, delete Tests cover: - All CLI options (--json, --ndjson, --full, pagination, sorting) - API request parameters validation - Confirmation requirements for destructive operations - Collection/document resolution Addresses #13
There was a problem hiding this comment.
Pull request overview
This PR significantly expands test coverage for the Outline CLI by adding four comprehensive test files for core command modules. The tests follow established vitest patterns with proper mocking, test isolation, and cover various output formats (JSON, NDJSON, formatted).
Changes:
- Adds 21 tests for document commands (list, get, open, create, update, delete, move, archive, unarchive)
- Adds 18 tests for collection commands (list, get, create, update, delete)
- Adds 4 tests for search command functionality
- Adds 5 tests for skill management commands
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/tests/document.test.ts | Comprehensive CRUD tests for document operations including ID resolution, markdown handling, and confirmation flows |
| src/tests/collection.test.ts | Full test coverage for collection management with pagination, output formats, and delete confirmations |
| src/tests/search.test.ts | Tests for document search with filters, status options, and various output formats |
| src/tests/skill.test.ts | Tests for skill installer operations including list, install, uninstall, and error handling |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| describe("document create", () => { | ||
| it("creates document with title and collection ID", async () => { | ||
| // First call: resolveCollectionId verifies collection exists | ||
| // Second call: documents.create | ||
| apiRequest | ||
| .mockResolvedValueOnce({ | ||
| data: { id: COL_ID, name: "Test Collection" }, | ||
| }) | ||
| .mockResolvedValueOnce({ data: mockDocument }); | ||
|
|
||
| const { registerDocumentCommand } = await import( | ||
| "../commands/document.js" | ||
| ); | ||
| const program = new Command(); | ||
| program.exitOverride(); | ||
| registerDocumentCommand(program); | ||
|
|
||
| await program.parseAsync([ | ||
| "node", | ||
| "ol", | ||
| "document", | ||
| "create", | ||
| "--title", | ||
| "New Doc", | ||
| "--collection", | ||
| COL_ID, | ||
| ]); | ||
|
|
||
| expect(apiRequest).toHaveBeenLastCalledWith("documents.create", { | ||
| title: "New Doc", | ||
| collectionId: COL_ID, | ||
| }); | ||
| expect(logs[0]).toContain("Created:"); | ||
| }); | ||
|
|
||
| it("creates document with text content", async () => { | ||
| apiRequest | ||
| .mockResolvedValueOnce({ | ||
| data: { id: COL_ID, name: "Test Collection" }, | ||
| }) | ||
| .mockResolvedValueOnce({ data: mockDocument }); | ||
|
|
||
| const { registerDocumentCommand } = await import( | ||
| "../commands/document.js" | ||
| ); | ||
| const program = new Command(); | ||
| program.exitOverride(); | ||
| registerDocumentCommand(program); | ||
|
|
||
| await program.parseAsync([ | ||
| "node", | ||
| "ol", | ||
| "document", | ||
| "create", | ||
| "--title", | ||
| "New Doc", | ||
| "--collection", | ||
| COL_ID, | ||
| "--text", | ||
| "Hello world", | ||
| ]); | ||
|
|
||
| expect(apiRequest).toHaveBeenLastCalledWith("documents.create", { | ||
| title: "New Doc", | ||
| collectionId: COL_ID, | ||
| text: "Hello world", | ||
| }); | ||
| }); | ||
|
|
||
| it("creates document with --publish flag", async () => { | ||
| apiRequest | ||
| .mockResolvedValueOnce({ | ||
| data: { id: COL_ID, name: "Test Collection" }, | ||
| }) | ||
| .mockResolvedValueOnce({ data: mockDocument }); | ||
|
|
||
| const { registerDocumentCommand } = await import( | ||
| "../commands/document.js" | ||
| ); | ||
| const program = new Command(); | ||
| program.exitOverride(); | ||
| registerDocumentCommand(program); | ||
|
|
||
| await program.parseAsync([ | ||
| "node", | ||
| "ol", | ||
| "document", | ||
| "create", | ||
| "--title", | ||
| "New Doc", | ||
| "--collection", | ||
| COL_ID, | ||
| "--publish", | ||
| ]); | ||
|
|
||
| expect(apiRequest).toHaveBeenLastCalledWith("documents.create", { | ||
| title: "New Doc", | ||
| collectionId: COL_ID, | ||
| publish: true, | ||
| }); | ||
| }); | ||
|
|
||
| it("outputs JSON when --json flag used", async () => { | ||
| apiRequest.mockResolvedValue({ | ||
| data: mockDocument, | ||
| }); | ||
|
|
||
| const { registerDocumentCommand } = await import( | ||
| "../commands/document.js" | ||
| ); | ||
| const program = new Command(); | ||
| program.exitOverride(); | ||
| registerDocumentCommand(program); | ||
|
|
||
| await program.parseAsync([ | ||
| "node", | ||
| "ol", | ||
| "document", | ||
| "create", | ||
| "--title", | ||
| "New Doc", | ||
| "--collection", | ||
| COL_ID, | ||
| "--json", | ||
| ]); | ||
|
|
||
| const parsed = JSON.parse(logs[0]); | ||
| expect(parsed.title).toBe("Test Document"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The document create and update commands support a --file option to read markdown from a file (see lines 58-59 and 164 in src/commands/document.ts), but there are no test cases verifying this functionality. Consider adding tests for creating and updating documents using the --file option to ensure this feature is properly covered.
| describe("document list", () => { | ||
| it("lists documents with default options", async () => { | ||
| apiRequest.mockResolvedValue({ | ||
| data: [mockDocument], | ||
| pagination: { offset: 0, limit: 25 }, | ||
| }); | ||
|
|
||
| const { registerDocumentCommand } = await import( | ||
| "../commands/document.js" | ||
| ); | ||
| const program = new Command(); | ||
| program.exitOverride(); | ||
| registerDocumentCommand(program); | ||
|
|
||
| await program.parseAsync(["node", "ol", "document", "list"]); | ||
|
|
||
| expect(apiRequest).toHaveBeenCalledWith("documents.list", { | ||
| limit: 25, | ||
| offset: 0, | ||
| sort: "updatedAt", | ||
| direction: "DESC", | ||
| }); | ||
| }); | ||
|
|
||
| it("passes pagination options", async () => { | ||
| apiRequest.mockResolvedValue({ | ||
| data: [], | ||
| pagination: { offset: 10, limit: 5 }, | ||
| }); | ||
|
|
||
| const { registerDocumentCommand } = await import( | ||
| "../commands/document.js" | ||
| ); | ||
| const program = new Command(); | ||
| program.exitOverride(); | ||
| registerDocumentCommand(program); | ||
|
|
||
| await program.parseAsync([ | ||
| "node", | ||
| "ol", | ||
| "document", | ||
| "list", | ||
| "--limit", | ||
| "5", | ||
| "--offset", | ||
| "10", | ||
| ]); | ||
|
|
||
| expect(apiRequest).toHaveBeenCalledWith("documents.list", { | ||
| limit: 5, | ||
| offset: 10, | ||
| sort: "updatedAt", | ||
| direction: "DESC", | ||
| }); | ||
| }); | ||
|
|
||
| it("passes sort options", async () => { | ||
| apiRequest.mockResolvedValue({ | ||
| data: [], | ||
| pagination: { offset: 0, limit: 25 }, | ||
| }); | ||
|
|
||
| const { registerDocumentCommand } = await import( | ||
| "../commands/document.js" | ||
| ); | ||
| const program = new Command(); | ||
| program.exitOverride(); | ||
| registerDocumentCommand(program); | ||
|
|
||
| await program.parseAsync([ | ||
| "node", | ||
| "ol", | ||
| "document", | ||
| "list", | ||
| "--sort", | ||
| "title", | ||
| "--direction", | ||
| "ASC", | ||
| ]); | ||
|
|
||
| expect(apiRequest).toHaveBeenCalledWith("documents.list", { | ||
| limit: 25, | ||
| offset: 0, | ||
| sort: "title", | ||
| direction: "ASC", | ||
| }); | ||
| }); | ||
|
|
||
| it("outputs JSON when --json flag used", async () => { | ||
| apiRequest.mockResolvedValue({ | ||
| data: [mockDocument], | ||
| pagination: { offset: 0, limit: 25 }, | ||
| }); | ||
|
|
||
| const { registerDocumentCommand } = await import( | ||
| "../commands/document.js" | ||
| ); | ||
| const program = new Command(); | ||
| program.exitOverride(); | ||
| registerDocumentCommand(program); | ||
|
|
||
| await program.parseAsync(["node", "ol", "document", "list", "--json"]); | ||
|
|
||
| const parsed = JSON.parse(logs[0]); | ||
| expect(parsed[0].title).toBe("Test Document"); | ||
| }); | ||
|
|
||
| it("outputs NDJSON when --ndjson flag used", async () => { | ||
| apiRequest.mockResolvedValue({ | ||
| data: [ | ||
| mockDocument, | ||
| { ...mockDocument, id: "doc-456", title: "Second" }, | ||
| ], | ||
| pagination: { offset: 0, limit: 25 }, | ||
| }); | ||
|
|
||
| const { registerDocumentCommand } = await import( | ||
| "../commands/document.js" | ||
| ); | ||
| const program = new Command(); | ||
| program.exitOverride(); | ||
| registerDocumentCommand(program); | ||
|
|
||
| await program.parseAsync(["node", "ol", "document", "list", "--ndjson"]); | ||
|
|
||
| expect(logs.length).toBe(2); | ||
| expect(JSON.parse(logs[0]).title).toBe("Test Document"); | ||
| expect(JSON.parse(logs[1]).title).toBe("Second"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The PR description mentions "collection filter" as one of the tested features for document list, but there's no test case verifying that the collection filter is properly passed to the API. Consider adding a test that verifies when the --collection option is used with document list, the collectionId is correctly resolved and passed to the documents.list API call (similar to lines 102-104 in src/commands/document.ts).
Summary
Adds dedicated test files for the two priority command modules per #13.
New Test Files
document.test.ts (21 tests)
collection.test.ts (18 tests)
Test Count
Notes
Tests use proper UUID format for entity IDs to correctly exercise the resolution logic. Mock sequences handle the collection/document verification calls that happen during resolution.
Addresses #13