mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 17:29:29 +05:30
feat: v4.6.0 — AI assistant, collaboration, knowledge base, and 15 more features
- AI Assistant plugin: multi-provider chat (OpenAI/Anthropic/Ollama/LM Studio), summarize/improve/translate commands, proofread via ai:analyze; calls proxied through main so API keys stay out of the renderer - Collaboration plugin: anchor-based comments in .comments/ sidecars with drift detection and F8 navigation - Local knowledge base: [[wiki-links]] with click-to-create + Backlinks panel - Crash recovery: debounced session snapshots with restore prompt on launch - Version history: pre-save snapshots, History panel with restore/diff/delete - Real PDF encryption: swap pdf-lib for @cantoo/pdf-lib (probe-driven UI) - XLSX export (native workbooks via JSZip), ODT headers/footers + page size - Offline KaTeX (bundled CSS+fonts), local-first PlantUML rendering - Editor: vim mode toggle, snippet Tab-expansion, zen word-goal setter, writing heatmap, writing-studio panels wired with rail icons - Quick Note global scratchpad (Ctrl+Alt+Q), markdownconverter:// deep links, REPL first-run confirmation - Fix: Ctrl+Shift+P collision, pandoc converter availability check, CLI dangling --css/--reference-doc flags, dead converter button 8 new test suites; 613 tests green; lint clean
This commit is contained in:
@@ -14,7 +14,7 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
|
||||
const { PDFDocument, StandardFonts, rgb } = require('@cantoo/pdf-lib');
|
||||
const PDFOperations = require('../../src/main/PDFOperations');
|
||||
|
||||
describe('PDFOperations - Task 15 new operations', () => {
|
||||
@@ -386,7 +386,7 @@ describe('PDFOperations - Task 16 form field fill/flatten', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PDFOperations - Task 27 honest encryption failure', () => {
|
||||
describe('PDFOperations - real encryption (@cantoo/pdf-lib)', () => {
|
||||
let tmpDir, inputPath;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -402,16 +402,16 @@ describe('PDFOperations - Task 27 honest encryption failure', () => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('detects the bundled pdf-lib as encryption-incapable via the module-load probe', async () => {
|
||||
// Pins the Task 27 premise: pdf-lib 1.17.1's save() ignores password
|
||||
// options (SaveOptions has no such fields), so the probe — which saves a
|
||||
// tiny document with a userPassword and checks the bytes for /Encrypt —
|
||||
// must report false. If this fails after a library swap, the probe
|
||||
// re-enabled the ops and the honest-failure tests below no longer apply.
|
||||
await expect(PDFOperations.pdfEncryptionSupported).resolves.toBe(false);
|
||||
it('detects the encryption-capable library via the module-load probe', async () => {
|
||||
// The @cantoo/pdf-lib fork adds doc.encrypt(); the probe — which encrypts
|
||||
// a tiny document and checks the bytes for /Encrypt — must report true so
|
||||
// the password ops are enabled in the UI. If this fails after a library
|
||||
// change, the ops fall back to honest failure and these tests no longer
|
||||
// apply.
|
||||
await expect(PDFOperations.pdfEncryptionSupported).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('pdfEncrypt fails honestly without writing an output file', async () => {
|
||||
it('pdfEncrypt writes an encrypted PDF that requires the password', async () => {
|
||||
const outputPath = path.join(tmpDir, 'encrypted.pdf');
|
||||
const result = await PDFOperations.pdfEncrypt({
|
||||
inputPath,
|
||||
@@ -421,60 +421,90 @@ describe('PDFOperations - Task 27 honest encryption failure', () => {
|
||||
permissions: { printing: true },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
expect(fs.existsSync(outputPath)).toBe(false);
|
||||
expect(result.success).toBe(true);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
const bytes = fs.readFileSync(outputPath);
|
||||
expect(bytes.includes('/Encrypt')).toBe(true);
|
||||
|
||||
// Correct password opens it; wrong password is rejected.
|
||||
const opened = await PDFDocument.load(bytes, { password: 'secret' });
|
||||
expect(opened.getPageCount()).toBe(1);
|
||||
await expect(PDFDocument.load(bytes, { password: 'wrong' })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('pdfDecrypt fails honestly without writing an output file', async () => {
|
||||
it('pdfDecrypt removes password protection and writes a clean PDF', async () => {
|
||||
const encryptedPath = path.join(tmpDir, 'encrypted.pdf');
|
||||
const encryptResult = await PDFOperations.pdfEncrypt({
|
||||
inputPath,
|
||||
outputPath: encryptedPath,
|
||||
userPassword: 'secret',
|
||||
permissions: { printing: true },
|
||||
});
|
||||
expect(encryptResult.success).toBe(true);
|
||||
|
||||
const outputPath = path.join(tmpDir, 'decrypted.pdf');
|
||||
const result = await PDFOperations.pdfDecrypt({
|
||||
inputPath,
|
||||
inputPath: encryptedPath,
|
||||
outputPath,
|
||||
password: 'secret',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const bytes = fs.readFileSync(outputPath);
|
||||
expect(bytes.includes('/Encrypt')).toBe(false);
|
||||
const opened = await PDFDocument.load(bytes);
|
||||
expect(opened.getPageCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('pdfDecrypt rejects a wrong password without writing output', async () => {
|
||||
const encryptedPath = path.join(tmpDir, 'encrypted.pdf');
|
||||
await PDFOperations.pdfEncrypt({
|
||||
inputPath,
|
||||
outputPath: encryptedPath,
|
||||
userPassword: 'secret',
|
||||
permissions: {},
|
||||
});
|
||||
|
||||
const outputPath = path.join(tmpDir, 'decrypted.pdf');
|
||||
const result = await PDFOperations.pdfDecrypt({
|
||||
inputPath: encryptedPath,
|
||||
outputPath,
|
||||
password: 'wrong',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
expect(result.error).toMatch(/password/i);
|
||||
expect(fs.existsSync(outputPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('pdfSetPermissions fails honestly without writing an output file', async () => {
|
||||
it('pdfSetPermissions writes an encrypted PDF with an owner password', async () => {
|
||||
const outputPath = path.join(tmpDir, 'permissions.pdf');
|
||||
const result = await PDFOperations.pdfSetPermissions({
|
||||
inputPath,
|
||||
outputPath,
|
||||
ownerPassword: 'owner-secret',
|
||||
permissions: { printing: true },
|
||||
permissions: { printing: true, copying: false },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
expect(fs.existsSync(outputPath)).toBe(false);
|
||||
expect(result.success).toBe(true);
|
||||
const bytes = fs.readFileSync(outputPath);
|
||||
expect(bytes.includes('/Encrypt')).toBe(true);
|
||||
// Owner password grants full access, so loading with it must succeed.
|
||||
const opened = await PDFDocument.load(bytes, { password: 'owner-secret' });
|
||||
expect(opened.getPageCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('fails honestly even before reading the input, so a missing input reports unavailability', async () => {
|
||||
const result = await PDFOperations.pdfEncrypt({
|
||||
inputPath: path.join(tmpDir, 'missing.pdf'),
|
||||
outputPath: path.join(tmpDir, 'never-written.pdf'),
|
||||
userPassword: 'secret',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
expect(fs.existsSync(path.join(tmpDir, 'never-written.pdf'))).toBe(false);
|
||||
});
|
||||
|
||||
it('executeOperation routes the password ops to the honest failure', async () => {
|
||||
it('executeOperation routes encrypt to the real implementation', async () => {
|
||||
const outputPath = path.join(tmpDir, 'exec-encrypted.pdf');
|
||||
const result = await PDFOperations.executeOperation('encrypt', {
|
||||
inputPath,
|
||||
outputPath: path.join(tmpDir, 'exec-encrypted.pdf'),
|
||||
outputPath,
|
||||
userPassword: 'secret',
|
||||
permissions: { printing: true },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe(PDFOperations.PDF_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
||||
expect(result.success).toBe(true);
|
||||
expect(fs.readFileSync(outputPath).includes('/Encrypt')).toBe(true);
|
||||
});
|
||||
|
||||
it('the module-load probe does not affect other operations', async () => {
|
||||
|
||||
Reference in New Issue
Block a user