generated from nhcarrigan/template
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>
This commit was merged in pull request #221.
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { getAchievementRarity, getRarityColour, toastStore } from "./toasts";
|
||||
import type { AchievementUnlockedEvent } from "$lib/types/achievements";
|
||||
|
||||
// ---
|
||||
|
||||
describe("getAchievementRarity", () => {
|
||||
describe("legendary tier", () => {
|
||||
it("classifies TokenMaster as legendary", () => {
|
||||
expect(getAchievementRarity("TokenMaster")).toBe("legendary");
|
||||
});
|
||||
});
|
||||
|
||||
describe("epic tier", () => {
|
||||
it("classifies CodeMachine as epic", () => {
|
||||
expect(getAchievementRarity("CodeMachine")).toBe("epic");
|
||||
});
|
||||
|
||||
it("classifies Unstoppable as epic", () => {
|
||||
expect(getAchievementRarity("Unstoppable")).toBe("epic");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rare tier", () => {
|
||||
it("classifies BlossomingCoder as rare", () => {
|
||||
expect(getAchievementRarity("BlossomingCoder")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies CodeWizard as rare", () => {
|
||||
expect(getAchievementRarity("CodeWizard")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies MasterBuilder as rare", () => {
|
||||
expect(getAchievementRarity("MasterBuilder")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies EnduranceChamp as rare", () => {
|
||||
expect(getAchievementRarity("EnduranceChamp")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies DeepDive as rare", () => {
|
||||
expect(getAchievementRarity("DeepDive")).toBe("rare");
|
||||
});
|
||||
|
||||
it("classifies CreativeCoder as rare", () => {
|
||||
expect(getAchievementRarity("CreativeCoder")).toBe("rare");
|
||||
});
|
||||
});
|
||||
|
||||
describe("common tier", () => {
|
||||
it("classifies unknown IDs as common", () => {
|
||||
expect(getAchievementRarity("FirstChat")).toBe("common");
|
||||
expect(getAchievementRarity("SomeNewAchievement")).toBe("common");
|
||||
expect(getAchievementRarity("")).toBe("common");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRarityColour", () => {
|
||||
it("returns yellow-to-orange gradient for legendary", () => {
|
||||
expect(getRarityColour("legendary")).toBe("from-yellow-400 to-orange-500");
|
||||
});
|
||||
|
||||
it("returns purple-to-pink gradient for epic", () => {
|
||||
expect(getRarityColour("epic")).toBe("from-purple-400 to-pink-500");
|
||||
});
|
||||
|
||||
it("returns blue-to-indigo gradient for rare", () => {
|
||||
expect(getRarityColour("rare")).toBe("from-blue-400 to-indigo-500");
|
||||
});
|
||||
|
||||
it("returns green-to-emerald gradient for common", () => {
|
||||
expect(getRarityColour("common")).toBe("from-green-400 to-emerald-500");
|
||||
});
|
||||
|
||||
it("falls back to green-to-emerald gradient for unknown rarities", () => {
|
||||
expect(getRarityColour("mythic")).toBe("from-green-400 to-emerald-500");
|
||||
expect(getRarityColour("")).toBe("from-green-400 to-emerald-500");
|
||||
});
|
||||
|
||||
describe("end-to-end rarity pipeline", () => {
|
||||
it("produces the correct colour for a legendary achievement", () => {
|
||||
const colour = getRarityColour(getAchievementRarity("TokenMaster"));
|
||||
expect(colour).toBe("from-yellow-400 to-orange-500");
|
||||
});
|
||||
|
||||
it("produces the correct colour for an epic achievement", () => {
|
||||
const colour = getRarityColour(getAchievementRarity("CodeMachine"));
|
||||
expect(colour).toBe("from-purple-400 to-pink-500");
|
||||
});
|
||||
|
||||
it("produces the correct colour for a rare achievement", () => {
|
||||
const colour = getRarityColour(getAchievementRarity("CodeWizard"));
|
||||
expect(colour).toBe("from-blue-400 to-indigo-500");
|
||||
});
|
||||
|
||||
it("produces the correct colour for a common achievement", () => {
|
||||
const colour = getRarityColour(getAchievementRarity("FirstChat"));
|
||||
expect(colour).toBe("from-green-400 to-emerald-500");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---
|
||||
|
||||
describe("toastStore", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
// Clear all toasts before each test
|
||||
const current = get(toastStore);
|
||||
for (const toast of current) {
|
||||
toastStore.remove(toast.id);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("addInfo", () => {
|
||||
it("adds an info toast with the correct fields", () => {
|
||||
toastStore.addInfo("Hello world", "🌍");
|
||||
const toasts = get(toastStore);
|
||||
expect(toasts).toHaveLength(1);
|
||||
const toast = toasts[0];
|
||||
expect(toast.kind).toBe("info");
|
||||
if (toast.kind === "info") {
|
||||
expect(toast.message).toBe("Hello world");
|
||||
expect(toast.icon).toBe("🌍");
|
||||
expect(typeof toast.id).toBe("string");
|
||||
expect(toast.id.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a default icon when none is provided", () => {
|
||||
toastStore.addInfo("Default icon test");
|
||||
const toasts = get(toastStore);
|
||||
const toast = toasts[0];
|
||||
if (toast.kind === "info") {
|
||||
expect(toast.icon).toBe("ℹ️");
|
||||
}
|
||||
});
|
||||
|
||||
it("auto-dismisses after 4000ms", () => {
|
||||
toastStore.addInfo("Auto-dismiss test");
|
||||
expect(get(toastStore)).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(3999);
|
||||
expect(get(toastStore)).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(get(toastStore)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addAchievement", () => {
|
||||
const mockAchievement: AchievementUnlockedEvent["achievement"] = {
|
||||
id: "FirstMessage",
|
||||
name: "First Message",
|
||||
description: "Sent your first message",
|
||||
icon: "💬",
|
||||
unlocked_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
it("adds an achievement toast with the correct fields", () => {
|
||||
toastStore.addAchievement(mockAchievement);
|
||||
const toasts = get(toastStore);
|
||||
expect(toasts).toHaveLength(1);
|
||||
const toast = toasts[0];
|
||||
expect(toast.kind).toBe("achievement");
|
||||
if (toast.kind === "achievement") {
|
||||
expect(toast.achievement).toEqual(mockAchievement);
|
||||
expect(typeof toast.id).toBe("string");
|
||||
}
|
||||
});
|
||||
|
||||
it("auto-dismisses after 5000ms", () => {
|
||||
toastStore.addAchievement(mockAchievement);
|
||||
expect(get(toastStore)).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(4999);
|
||||
expect(get(toastStore)).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(get(toastStore)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addUpdate", () => {
|
||||
it("adds a persistent update toast with the correct fields", () => {
|
||||
toastStore.addUpdate("2.0.0", "1.9.0", "https://example.com/release");
|
||||
const toasts = get(toastStore);
|
||||
expect(toasts).toHaveLength(1);
|
||||
const toast = toasts[0];
|
||||
expect(toast.kind).toBe("update");
|
||||
if (toast.kind === "update") {
|
||||
expect(toast.latestVersion).toBe("2.0.0");
|
||||
expect(toast.currentVersion).toBe("1.9.0");
|
||||
expect(toast.releaseUrl).toBe("https://example.com/release");
|
||||
expect(typeof toast.id).toBe("string");
|
||||
}
|
||||
});
|
||||
|
||||
it("does not auto-dismiss after a long time", () => {
|
||||
toastStore.addUpdate("2.0.0", "1.9.0", "https://example.com/release");
|
||||
expect(get(toastStore)).toHaveLength(1);
|
||||
|
||||
vi.advanceTimersByTime(60000);
|
||||
expect(get(toastStore)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("removes a toast by id", () => {
|
||||
toastStore.addInfo("To be removed");
|
||||
const toasts = get(toastStore);
|
||||
expect(toasts).toHaveLength(1);
|
||||
const id = toasts[0].id;
|
||||
|
||||
toastStore.remove(id);
|
||||
expect(get(toastStore)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("does not affect other toasts when removing by id", () => {
|
||||
toastStore.addInfo("First toast");
|
||||
toastStore.addInfo("Second toast");
|
||||
const toasts = get(toastStore);
|
||||
expect(toasts).toHaveLength(2);
|
||||
|
||||
toastStore.remove(toasts[0].id);
|
||||
const remaining = get(toastStore);
|
||||
expect(remaining).toHaveLength(1);
|
||||
if (remaining[0].kind === "info") {
|
||||
expect(remaining[0].message).toBe("Second toast");
|
||||
}
|
||||
});
|
||||
|
||||
it("is a no-op when the id does not exist", () => {
|
||||
toastStore.addInfo("Existing toast");
|
||||
toastStore.remove("non-existent-id");
|
||||
expect(get(toastStore)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user