Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cli/commands/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions src/core/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,19 @@ 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(", ")
.map((c) => `d.${c}`)
.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[];

Expand Down
12 changes: 9 additions & 3 deletions tests/unit/cli-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <args>` and return [operation name, input] of each operation call. */
/**
* Run `libscope --json <args>` 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<Array<[string, unknown]>> {
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;
Expand Down Expand Up @@ -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" }],
]);
});
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/documents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading