diff --git a/src/cli/commands/docs.ts b/src/cli/commands/docs.ts index 1604b08..8cdeccb 100644 --- a/src/cli/commands/docs.ts +++ b/src/cli/commands/docs.ts @@ -170,7 +170,7 @@ function registerEditCommands(docs: Command): void { .description("Delete a document with its chunks, links and ratings") .option("-y, --yes", "Do not ask for confirmation") .action(async (documentId: string, flags: { yes?: boolean }) => { - const view = await call(getDocumentOperation, { documentId, maxLength: 0 }); + const view = await call(getDocumentOperation, { documentId }); const title = view.document.title; const question = `Delete "${title}" (${documentId})? This cannot be undone.`; if (!(await confirmOrCancel(question, flags.yes))) return; diff --git a/src/core/documents.ts b/src/core/documents.ts index 7b07440..2581814 100644 --- a/src/core/documents.ts +++ b/src/core/documents.ts @@ -166,7 +166,11 @@ function documentFilterSql(filters: DocumentFilters | undefined): { return { where: where + tagFilter.clause, params: [...params, ...tagFilter.params] }; } -/** List documents with optional filters, most recently updated first. */ +/** + * List documents with optional filters, most recently updated first. updated_at has + * one-second resolution, so documents updated in the same second are ordered by rowid, + * newest insert first. + */ export function listDocuments(db: Database.Database, options?: ListDocumentsOptions): Document[] { const { where, params } = documentFilterSql(options); const columns = DOC_COLUMNS.split(", ") @@ -174,7 +178,7 @@ export function listDocuments(db: Database.Database, options?: ListDocumentsOpti .join(", "); const rows = db .prepare( - `SELECT ${columns} FROM documents d ${where} ORDER BY d.updated_at DESC LIMIT ? OFFSET ?`, + `SELECT ${columns} FROM documents d ${where} ORDER BY d.updated_at DESC, d.rowid DESC LIMIT ? OFFSET ?`, ) .all(...params, options?.limit ?? 50, options?.offset ?? 0) as DocumentRow[]; diff --git a/tests/unit/cli-commands.test.ts b/tests/unit/cli-commands.test.ts index 2c7d6cf..c211e18 100644 --- a/tests/unit/cli-commands.test.ts +++ b/tests/unit/cli-commands.test.ts @@ -48,12 +48,18 @@ vi.mock("../../src/core/operations/index.js", async (importOriginal) => { const { runOperation } = await import("../../src/core/operations/index.js"); const { main, program } = await import("../../src/cli/index.js"); -/** Run `libscope --json ` and return [operation name, input] of each operation call. */ +/** + * Run `libscope --json ` and return [operation name, input] of each operation call. + * Each input must pass the operation's own schema, which the runOperation spy skips. + */ async function invoke(args: string[]): Promise> { vi.mocked(runOperation).mockClear(); await main(["node", "libscope", "--json", ...args]); expect(process.exitCode ?? 0).toBe(0); - return vi.mocked(runOperation).mock.calls.map(([op, , input]) => [op.name, input]); + return vi.mocked(runOperation).mock.calls.map(([op, , input]) => { + expect(op.input.safeParse(input ?? {}).error, `${op.name} input`).toBeUndefined(); + return [op.name, input]; + }); } let home: string; @@ -332,7 +338,7 @@ describe("CLI command -> operation contract", () => { it("docs delete looks the document up, then deletes it", async () => { const calls = await invoke(["docs", "delete", "d1", "-y"]); expect(calls).toEqual([ - ["get-document", { documentId: "d1", maxLength: 0 }], + ["get-document", { documentId: "d1" }], ["delete-document", { documentId: "d1" }], ]); }); diff --git a/tests/unit/documents.test.ts b/tests/unit/documents.test.ts index 08f0166..db4fdd4 100644 --- a/tests/unit/documents.test.ts +++ b/tests/unit/documents.test.ts @@ -128,6 +128,11 @@ describe("documents", () => { const docs = listDocuments(db, { library: "nonexistent" }); expect(docs.length).toBe(0); }); + + it("lists documents updated in the same second newest insert first", () => { + db.prepare("UPDATE documents SET updated_at = '2026-01-01 00:00:00'").run(); + expect(listDocuments(db).map((d) => d.id)).toEqual(["doc-3", "doc-2", "doc-1"]); + }); }); describe("updateDocument", () => {