Files
hikari-desktop/src/lib/utils/filePaths.test.ts
T
hikari 452fe185df
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m21s
CI / Lint & Test (push) Has started running
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
feat: CLI v2.1.68–v2.1.74 compatibility updates (#221)
## Summary

This PR brings Hikari Desktop up to full compatibility with Claude Code CLI versions v2.1.68 through v2.1.74, implementing all changelog items audited in issues #200–#218.

## Changes

### Bug Fixes
- Remove deprecated Claude Opus 4.0 and 4.1 models from the model selector
- Auto-migrate users pinned to deprecated models to Opus 4.6

### New Features
- Add cron tool support (`CronCreate`, `CronDelete`, `CronList`) with character state mapping and `CLAUDE_CODE_DISABLE_CRON` settings toggle
- Handle `EnterWorktree` and `ExitWorktree` tools in character state mapping and tool display
- Add CLI update check with npm registry indicator in the version bar
- Add `agent_type` field and support the Agent tool rename from CLI v2.1.69
- Consume `worktree` field from status line hook events
- Display per-agent model override in the agent monitor tree
- Expose Claude Code CLI built-in slash commands (`/simplify`, `/loop`, `/batch`, `/memory`, `/context`) in the command menu with CLI badges
- Add `includeGitInstructions` toggle in settings
- Add `ENABLE_CLAUDEAI_MCP_SERVERS` opt-out setting
- Linkify MCP binary file paths (PDFs, audio, Office docs) in markdown output
- Add auto-memory panel, `/memory` slash command shortcut, and unified toast notification system
- Toast notifications for `WorktreeCreate` and `WorktreeRemove` hook events
- Sort session resume list by most recent activity, with most recent user message as preview
- Convert WSL Linux paths to Windows UNC paths when opening binary files via `open_binary_file` command
- Expose `autoMemoryDirectory` setting in ConfigSidebar (Agent Settings section)
- Add `/context` as a CLI built-in in the slash command menu
- Expose `modelOverrides` setting as a JSON textarea in ConfigSidebar (for AWS Bedrock, Google Vertex, etc.)

> **Note:** The CLI update check commit does not have a corresponding issue — it was a bonus addition during the audit sprint.

## Closes

Closes #200
Closes #201
Closes #202
Closes #205
Closes #206
Closes #207
Closes #208
Closes #209
Closes #210
Closes #211
Closes #212
Closes #213
Closes #214
Closes #215
Closes #216
Closes #217
Closes #218

Reviewed-on: #221
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-03-13 01:34:44 -07:00

271 lines
8.8 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
BINARY_FILE_EXTENSIONS,
getFileExtension,
getFileTypeIcon,
isBinaryFilePath,
linkifyFilePaths,
} from "./filePaths";
describe("getFileExtension", () => {
it("returns the lowercase extension of a simple path", () => {
expect(getFileExtension("/tmp/report.pdf")).toBe("pdf");
});
it("returns the lowercase extension for uppercase file names", () => {
expect(getFileExtension("/tmp/AUDIO.MP3")).toBe("mp3");
});
it("returns the extension for a path with multiple dots", () => {
expect(getFileExtension("/tmp/my.file.docx")).toBe("docx");
});
it("returns an empty string when there is no extension", () => {
expect(getFileExtension("/tmp/noextension")).toBe("");
});
it("returns an empty string for an empty string input", () => {
expect(getFileExtension("")).toBe("");
});
it("returns the extension for a home-relative path", () => {
expect(getFileExtension("~/downloads/track.wav")).toBe("wav");
});
});
describe("getFileTypeIcon", () => {
it("returns the PDF icon for .pdf files", () => {
expect(getFileTypeIcon("/tmp/doc.pdf")).toBe("📄");
});
it("returns the Word icon for .docx files", () => {
expect(getFileTypeIcon("/tmp/report.docx")).toBe("📝");
});
it("returns the Word icon for .doc files", () => {
expect(getFileTypeIcon("/tmp/old.doc")).toBe("📝");
});
it("returns the spreadsheet icon for .xlsx files", () => {
expect(getFileTypeIcon("/tmp/data.xlsx")).toBe("📊");
});
it("returns the spreadsheet icon for .xls files", () => {
expect(getFileTypeIcon("/tmp/data.xls")).toBe("📊");
});
it("returns the presentation icon for .pptx files", () => {
expect(getFileTypeIcon("/tmp/slides.pptx")).toBe("📽️");
});
it("returns the presentation icon for .ppt files", () => {
expect(getFileTypeIcon("/tmp/slides.ppt")).toBe("📽️");
});
it("returns the audio icon for .mp3 files", () => {
expect(getFileTypeIcon("/tmp/song.mp3")).toBe("🎵");
});
it("returns the audio icon for .wav files", () => {
expect(getFileTypeIcon("/tmp/sound.wav")).toBe("🎵");
});
it("returns the audio icon for .ogg files", () => {
expect(getFileTypeIcon("/tmp/audio.ogg")).toBe("🎵");
});
it("returns the audio icon for .flac files", () => {
expect(getFileTypeIcon("/tmp/lossless.flac")).toBe("🎵");
});
it("returns the audio icon for .aac files", () => {
expect(getFileTypeIcon("/tmp/compressed.aac")).toBe("🎵");
});
it("returns the audio icon for .m4a files", () => {
expect(getFileTypeIcon("/tmp/itunes.m4a")).toBe("🎵");
});
it("returns the video icon for .mp4 files", () => {
expect(getFileTypeIcon("/tmp/video.mp4")).toBe("🎬");
});
it("returns the video icon for .avi files", () => {
expect(getFileTypeIcon("/tmp/old.avi")).toBe("🎬");
});
it("returns the video icon for .mov files", () => {
expect(getFileTypeIcon("/tmp/clip.mov")).toBe("🎬");
});
it("returns the video icon for .mkv files", () => {
expect(getFileTypeIcon("/tmp/film.mkv")).toBe("🎬");
});
it("returns the video icon for .webm files", () => {
expect(getFileTypeIcon("/tmp/stream.webm")).toBe("🎬");
});
it("returns the archive icon for .zip files", () => {
expect(getFileTypeIcon("/tmp/bundle.zip")).toBe("📦");
});
it("returns the archive icon for .tar files", () => {
expect(getFileTypeIcon("/tmp/archive.tar")).toBe("📦");
});
it("returns the archive icon for .gz files", () => {
expect(getFileTypeIcon("/tmp/compressed.gz")).toBe("📦");
});
it("returns the disk icon for .bin files", () => {
expect(getFileTypeIcon("/tmp/firmware.bin")).toBe("💿");
});
it("returns the disk icon for .iso files", () => {
expect(getFileTypeIcon("/tmp/image.iso")).toBe("💿");
});
it("returns the generic folder icon for an unknown extension", () => {
expect(getFileTypeIcon("/tmp/file.unknown")).toBe("📁");
});
it("returns the generic folder icon for a file with no extension", () => {
expect(getFileTypeIcon("/tmp/noext")).toBe("📁");
});
});
describe("isBinaryFilePath", () => {
it("returns true for a PDF path", () => {
expect(isBinaryFilePath("/tmp/report.pdf")).toBe(true);
});
it("returns true for an audio path", () => {
expect(isBinaryFilePath("/tmp/song.mp3")).toBe(true);
});
it("returns true for a video path", () => {
expect(isBinaryFilePath("/tmp/clip.mp4")).toBe(true);
});
it("returns true for a document path", () => {
expect(isBinaryFilePath("/tmp/doc.docx")).toBe(true);
});
it("returns false for a TypeScript file", () => {
expect(isBinaryFilePath("/src/index.ts")).toBe(false);
});
it("returns false for a text file", () => {
expect(isBinaryFilePath("/tmp/output.txt")).toBe(false);
});
it("returns false for a path with no extension", () => {
expect(isBinaryFilePath("/tmp/file")).toBe(false);
});
});
describe("BINARY_FILE_EXTENSIONS", () => {
it("includes pdf", () => {
expect(BINARY_FILE_EXTENSIONS).toContain("pdf");
});
it("includes common audio extensions", () => {
expect(BINARY_FILE_EXTENSIONS).toContain("mp3");
expect(BINARY_FILE_EXTENSIONS).toContain("wav");
});
it("includes common video extensions", () => {
expect(BINARY_FILE_EXTENSIONS).toContain("mp4");
});
it("includes common document extensions", () => {
expect(BINARY_FILE_EXTENSIONS).toContain("docx");
expect(BINARY_FILE_EXTENSIONS).toContain("xlsx");
});
});
describe("linkifyFilePaths", () => {
it("converts a PDF path in plain text to a file link", () => {
const html = "<p>Saved to /tmp/report.pdf successfully.</p>";
const result = linkifyFilePaths(html);
expect(result).toContain('data-filepath="/tmp/report.pdf"');
expect(result).toContain("📄");
expect(result).toContain('class="file-link"');
});
it("converts an audio path to a file link", () => {
const html = "<p>Audio saved to /tmp/output.mp3</p>";
const result = linkifyFilePaths(html);
expect(result).toContain('data-filepath="/tmp/output.mp3"');
expect(result).toContain("🎵");
});
it("does not linkify paths inside code blocks", () => {
const html = "<p>Example:</p><pre><code>/tmp/file.pdf</code></pre>";
const result = linkifyFilePaths(html);
expect(result).not.toContain('data-filepath="/tmp/file.pdf"');
expect(result).toContain("/tmp/file.pdf");
});
it("does not linkify paths inside inline code", () => {
const html = "<p>Use <code>/tmp/file.pdf</code> to open it.</p>";
const result = linkifyFilePaths(html);
expect(result).not.toContain('data-filepath="/tmp/file.pdf"');
expect(result).toContain("/tmp/file.pdf");
});
it("does not modify HTML that has no binary file paths", () => {
const html = "<p>Hello, this is regular text with /tmp/script.sh</p>";
const result = linkifyFilePaths(html);
expect(result).toBe(html);
});
it("does not linkify text file paths", () => {
const html = "<p>Saved to /tmp/output.txt</p>";
const result = linkifyFilePaths(html);
expect(result).not.toContain("data-filepath");
});
it("handles a home-relative path", () => {
const html = "<p>Saved to ~/downloads/audio.flac</p>";
const result = linkifyFilePaths(html);
expect(result).toContain('data-filepath="~/downloads/audio.flac"');
expect(result).toContain("🎵");
});
it("handles multiple file paths in the same HTML", () => {
const html = "<p>Files: /tmp/a.pdf and /tmp/b.mp3</p>";
const result = linkifyFilePaths(html);
expect(result).toContain('data-filepath="/tmp/a.pdf"');
expect(result).toContain('data-filepath="/tmp/b.mp3"');
});
it("does not linkify paths that contain double quotes (invalid path character)", () => {
// Double quotes are excluded from path chars so the path is not matched
const html = `<p>Saved to /tmp/my"file.pdf</p>`;
const result = linkifyFilePaths(html);
expect(result).not.toContain("data-filepath");
});
it("preserves existing HTML tags and attributes", () => {
const html = '<p class="foo">Saved to /tmp/report.pdf</p>';
const result = linkifyFilePaths(html);
expect(result).toContain('class="foo"');
expect(result).toContain('data-filepath="/tmp/report.pdf"');
});
it("does not double-linkify a path already inside an anchor tag", () => {
const html = '<a href="/tmp/file.pdf">/tmp/file.pdf</a>';
const result = linkifyFilePaths(html);
// The href is inside a tag (placeholder), the text content IS linkified
// but the href itself should not be modified
const hrefMatches = result.match(/href="[^"]*\/tmp\/file\.pdf[^"]*"/g) ?? [];
expect(hrefMatches.length).toBe(1);
});
it("returns the input unchanged when html is empty", () => {
expect(linkifyFilePaths("")).toBe("");
});
});