6c605849d8
- /api/workspaces and `agenthub workspace(s)` / `--workspace` as aliases of /api/lanes and `agenthub lanes` / `--lane`; old names print one stderr note. REPL `help lanes|workspace|workspaces` resolves through the aliases. - A workspace holds up to ten sessions (slots 0-9); every console tab is a group of 1-4 split panes (PaneGroups, agenthub.workspace.paneLayout); parseGroups keeps the stored active tab when an earlier one is dropped. - Browse button beside path fields: POST /api/lanes/pick-path opens the OS picker on the server's desktop — loopback only and no forwarding header (EREMOTE), a display failure is ENODIALOG not a Cancel, the dialog is killed when the request closes, a folder field falls back to the in-app browser on any failure. - Changes rail counts chip, image viewer (/git/raw), status bar session counts and per-day cost (`date=` on /api/pricing/cost). - /agenthub-open and /agenthub-doctor run from the INSTALLED build (supersededBy), doctor offers fixes behind two confirmations; plugin-open treats a locked bootstrap as non-fatal. - New-terminal shell waits for the first attach (issue #55). - repo-onboarding / debug-live-issue skills removed. - OpenAPI: pick-path, lane/date on /pricing/cost, tool on /run/history, /api/workspaces mount.
101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
/**
|
|
* @file laneOrderStorage.test.ts
|
|
* @description Tests for the Workspace lane strip's stored card order: the
|
|
* storage round-trip, and the two rules that keep a stale stored order from
|
|
* losing a lane — unknown ids land at the end, vanished ids are skipped — plus
|
|
* the drop math: a drop lands between items by the pointer's position.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
import {
|
|
readLaneOrder,
|
|
writeLaneOrder,
|
|
sortByStoredOrder,
|
|
moveLane,
|
|
insertionIndex,
|
|
moveBefore,
|
|
} from "../laneOrderStorage";
|
|
|
|
const lanes = (...ids: number[]) => ids.map((id) => ({ id }));
|
|
|
|
describe("laneOrderStorage", () => {
|
|
beforeEach(() => localStorage.clear());
|
|
|
|
it("returns an empty order when nothing is stored", () => {
|
|
expect(readLaneOrder()).toEqual([]);
|
|
});
|
|
|
|
it("round-trips a written order", () => {
|
|
writeLaneOrder([3, 1, 2]);
|
|
expect(readLaneOrder()).toEqual([3, 1, 2]);
|
|
});
|
|
|
|
it("falls back to an empty order when stored JSON is malformed", () => {
|
|
localStorage.setItem("agenthub.workspace.laneOrder", "{not json");
|
|
expect(readLaneOrder()).toEqual([]);
|
|
});
|
|
|
|
it("drops non-numeric entries from a stored order", () => {
|
|
localStorage.setItem("agenthub.workspace.laneOrder", JSON.stringify([1, "2", null, 3]));
|
|
expect(readLaneOrder()).toEqual([1, 3]);
|
|
});
|
|
});
|
|
|
|
describe("sortByStoredOrder", () => {
|
|
it("keeps server order when nothing is stored", () => {
|
|
expect(sortByStoredOrder(lanes(1, 2, 3), [])).toEqual(lanes(1, 2, 3));
|
|
});
|
|
|
|
it("applies the stored order", () => {
|
|
expect(sortByStoredOrder(lanes(1, 2, 3), [3, 1, 2])).toEqual(lanes(3, 1, 2));
|
|
});
|
|
|
|
it("appends lanes the stored order has never seen, in server order", () => {
|
|
expect(sortByStoredOrder(lanes(1, 2, 3, 4), [3, 1])).toEqual(lanes(3, 1, 2, 4));
|
|
});
|
|
|
|
it("skips stored ids whose lane no longer exists", () => {
|
|
expect(sortByStoredOrder(lanes(1, 3), [3, 2, 1])).toEqual(lanes(3, 1));
|
|
});
|
|
|
|
it("does not mutate the input", () => {
|
|
const input = lanes(1, 2, 3);
|
|
sortByStoredOrder(input, [3, 2, 1]);
|
|
expect(input.map((l) => l.id)).toEqual([1, 2, 3]);
|
|
});
|
|
});
|
|
|
|
describe("moveLane", () => {
|
|
it("moves a card forward", () => {
|
|
expect(moveLane([1, 2, 3], 0, 2)).toEqual([2, 3, 1]);
|
|
});
|
|
|
|
it("moves a card backward", () => {
|
|
expect(moveLane([1, 2, 3], 2, 0)).toEqual([3, 1, 2]);
|
|
});
|
|
|
|
it("returns the order unchanged for a no-op or out-of-range move", () => {
|
|
expect(moveLane([1, 2, 3], 1, 1)).toEqual([1, 2, 3]);
|
|
expect(moveLane([1, 2, 3], 0, 3)).toEqual([1, 2, 3]);
|
|
expect(moveLane([1, 2, 3], -1, 0)).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it("places a drop before the first item whose middle is past the pointer", () => {
|
|
const mids = [10, 30, 50];
|
|
expect(insertionIndex(mids, 0)).toBe(0);
|
|
expect(insertionIndex(mids, 20)).toBe(1); // a gap between two items
|
|
expect(insertionIndex(mids, 49)).toBe(2);
|
|
expect(insertionIndex(mids, 90)).toBe(3); // past the last: the end
|
|
});
|
|
|
|
it("moves an item to just before an insertion point", () => {
|
|
expect(moveBefore([1, 2, 3, 4], 0, 2)).toEqual([2, 1, 3, 4]);
|
|
expect(moveBefore([1, 2, 3, 4], 0, 4)).toEqual([2, 3, 4, 1]);
|
|
expect(moveBefore([1, 2, 3, 4], 3, 0)).toEqual([4, 1, 2, 3]);
|
|
// Before itself or right after itself: no move.
|
|
expect(moveBefore([1, 2, 3], 1, 1)).toEqual([1, 2, 3]);
|
|
expect(moveBefore([1, 2, 3], 1, 2)).toEqual([1, 2, 3]);
|
|
});
|
|
});
|