skyboard-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -1
- package/src/__tests__/auth-state.test.ts +0 -111
- package/src/__tests__/card-ref.test.ts +0 -42
- package/src/__tests__/column-match.test.ts +0 -57
- package/src/__tests__/commands.test.ts +0 -326
- package/src/__tests__/helpers.ts +0 -154
- package/src/__tests__/materialize.test.ts +0 -257
- package/src/__tests__/pds.test.ts +0 -213
- package/src/commands/add.ts +0 -47
- package/src/commands/boards.ts +0 -64
- package/src/commands/cards.ts +0 -65
- package/src/commands/cols.ts +0 -51
- package/src/commands/comment.ts +0 -64
- package/src/commands/edit.ts +0 -89
- package/src/commands/login.ts +0 -19
- package/src/commands/logout.ts +0 -14
- package/src/commands/mv.ts +0 -84
- package/src/commands/new.ts +0 -80
- package/src/commands/rm.ts +0 -79
- package/src/commands/show.ts +0 -100
- package/src/commands/status.ts +0 -78
- package/src/commands/use.ts +0 -72
- package/src/commands/whoami.ts +0 -22
- package/src/index.ts +0 -143
- package/src/lib/auth.ts +0 -244
- package/src/lib/card-ref.ts +0 -32
- package/src/lib/column-match.ts +0 -45
- package/src/lib/config.ts +0 -182
- package/src/lib/display.ts +0 -112
- package/src/lib/materialize.ts +0 -138
- package/src/lib/pds.ts +0 -440
- package/src/lib/permissions.ts +0 -28
- package/src/lib/schemas.ts +0 -97
- package/src/lib/tid.ts +0 -22
- package/src/lib/types.ts +0 -144
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -7
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skyboard-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "CLI for Skyboard – a collaborative kanban board on AT Protocol",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Tim Disney"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"bin"
|
|
12
|
+
],
|
|
9
13
|
"bin": {
|
|
10
14
|
"sb": "./bin/sb.js"
|
|
11
15
|
},
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { OWNER_DID, USER_DID, BOARD_RKEY } from "./helpers.js";
|
|
3
|
-
|
|
4
|
-
// Mock config module
|
|
5
|
-
const mockLoadAuthInfo = vi.fn();
|
|
6
|
-
const mockClearDefaultBoard = vi.fn();
|
|
7
|
-
const mockGetDefaultBoard = vi.fn();
|
|
8
|
-
|
|
9
|
-
vi.mock("../lib/config.js", () => ({
|
|
10
|
-
loadAuthInfo: (...args: unknown[]) => mockLoadAuthInfo(...args),
|
|
11
|
-
clearDefaultBoard: (...args: unknown[]) => mockClearDefaultBoard(...args),
|
|
12
|
-
getDefaultBoard: (...args: unknown[]) => mockGetDefaultBoard(...args),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
// Mock auth module
|
|
16
|
-
const mockLogin = vi.fn();
|
|
17
|
-
const mockLogout = vi.fn();
|
|
18
|
-
|
|
19
|
-
vi.mock("../lib/auth.js", () => ({
|
|
20
|
-
login: (...args: unknown[]) => mockLogin(...args),
|
|
21
|
-
logout: (...args: unknown[]) => mockLogout(...args),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
import { logoutCommand } from "../commands/logout.js";
|
|
25
|
-
import { loginCommand } from "../commands/login.js";
|
|
26
|
-
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
vi.clearAllMocks();
|
|
29
|
-
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
30
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe("logoutCommand", () => {
|
|
34
|
-
it("clears the default board on logout", () => {
|
|
35
|
-
mockLoadAuthInfo.mockReturnValue({
|
|
36
|
-
did: OWNER_DID,
|
|
37
|
-
handle: "owner.test",
|
|
38
|
-
service: "https://pds.example.com",
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
logoutCommand();
|
|
42
|
-
|
|
43
|
-
expect(mockLogout).toHaveBeenCalledOnce();
|
|
44
|
-
expect(mockClearDefaultBoard).toHaveBeenCalledOnce();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("does not clear board if not logged in", () => {
|
|
48
|
-
mockLoadAuthInfo.mockReturnValue(null);
|
|
49
|
-
|
|
50
|
-
logoutCommand();
|
|
51
|
-
|
|
52
|
-
expect(mockLogout).not.toHaveBeenCalled();
|
|
53
|
-
expect(mockClearDefaultBoard).not.toHaveBeenCalled();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("loginCommand", () => {
|
|
58
|
-
it("clears the default board when switching to a different account", async () => {
|
|
59
|
-
mockLoadAuthInfo.mockReturnValue({
|
|
60
|
-
did: OWNER_DID,
|
|
61
|
-
handle: "owner.test",
|
|
62
|
-
service: "https://pds.example.com",
|
|
63
|
-
});
|
|
64
|
-
mockLogin.mockResolvedValue({ did: USER_DID, handle: "user.test" });
|
|
65
|
-
|
|
66
|
-
await loginCommand("user.test");
|
|
67
|
-
|
|
68
|
-
expect(mockClearDefaultBoard).toHaveBeenCalledOnce();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("does not clear the default board when logging in as the same account", async () => {
|
|
72
|
-
mockLoadAuthInfo.mockReturnValue({
|
|
73
|
-
did: OWNER_DID,
|
|
74
|
-
handle: "owner.test",
|
|
75
|
-
service: "https://pds.example.com",
|
|
76
|
-
});
|
|
77
|
-
mockLogin.mockResolvedValue({ did: OWNER_DID, handle: "owner.test" });
|
|
78
|
-
|
|
79
|
-
await loginCommand("owner.test");
|
|
80
|
-
|
|
81
|
-
expect(mockClearDefaultBoard).not.toHaveBeenCalled();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("does not clear the default board on first login (no previous auth)", async () => {
|
|
85
|
-
mockLoadAuthInfo.mockReturnValue(null);
|
|
86
|
-
mockLogin.mockResolvedValue({ did: OWNER_DID, handle: "owner.test" });
|
|
87
|
-
|
|
88
|
-
await loginCommand("owner.test");
|
|
89
|
-
|
|
90
|
-
expect(mockClearDefaultBoard).not.toHaveBeenCalled();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("does not clear the default board if login fails", async () => {
|
|
94
|
-
mockLoadAuthInfo.mockReturnValue({
|
|
95
|
-
did: OWNER_DID,
|
|
96
|
-
handle: "owner.test",
|
|
97
|
-
service: "https://pds.example.com",
|
|
98
|
-
});
|
|
99
|
-
mockLogin.mockRejectedValue(new Error("OAuth failed"));
|
|
100
|
-
|
|
101
|
-
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
|
|
102
|
-
throw new Error("process.exit");
|
|
103
|
-
}) as any);
|
|
104
|
-
|
|
105
|
-
await expect(loginCommand("user.test")).rejects.toThrow("process.exit");
|
|
106
|
-
|
|
107
|
-
expect(mockClearDefaultBoard).not.toHaveBeenCalled();
|
|
108
|
-
|
|
109
|
-
exitSpy.mockRestore();
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { resolveCardRef } from "../lib/card-ref.js";
|
|
3
|
-
import { makeMaterializedTask, TASK_RKEY_1, TASK_RKEY_2, TASK_RKEY_3 } from "./helpers.js";
|
|
4
|
-
|
|
5
|
-
describe("resolveCardRef", () => {
|
|
6
|
-
const tasks = [
|
|
7
|
-
makeMaterializedTask({ rkey: TASK_RKEY_1, title: "First Task" }),
|
|
8
|
-
makeMaterializedTask({ rkey: TASK_RKEY_2, title: "Second Task" }),
|
|
9
|
-
makeMaterializedTask({ rkey: TASK_RKEY_3, title: "Third Task" }),
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
it("matches a task by 4+ character rkey prefix", () => {
|
|
13
|
-
// TASK_RKEY_1 = "3jui7a2zbbb22" — need enough chars to disambiguate from TASK_RKEY_2 "3jui7a2zbbb33"
|
|
14
|
-
const result = resolveCardRef(TASK_RKEY_1.slice(0, 12), tasks);
|
|
15
|
-
expect(result.rkey).toBe(TASK_RKEY_1);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("matches a task by full rkey", () => {
|
|
19
|
-
const result = resolveCardRef(TASK_RKEY_2, tasks);
|
|
20
|
-
expect(result.rkey).toBe(TASK_RKEY_2);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("throws on too-short prefix (< 4 chars)", () => {
|
|
24
|
-
expect(() => resolveCardRef("3ju", tasks)).toThrow("too short");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("throws when no task matches", () => {
|
|
28
|
-
expect(() => resolveCardRef("zzzz", tasks)).toThrow('No card found');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("throws on ambiguous match with list of candidates", () => {
|
|
32
|
-
// TASK_RKEY_1 = "3jui7a2zbbb22", TASK_RKEY_2 = "3jui7a2zbbb33"
|
|
33
|
-
// They share prefix "3jui7a2zbbb"
|
|
34
|
-
expect(() => resolveCardRef("3jui7a2zbbb", tasks)).toThrow("Ambiguous");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("resolves unambiguous prefix that partially overlaps", () => {
|
|
38
|
-
// TASK_RKEY_3 = "3jui7a2zbcc44" — prefix "3jui7a2zbcc" is unique
|
|
39
|
-
const result = resolveCardRef("3jui7a2zbcc", tasks);
|
|
40
|
-
expect(result.rkey).toBe(TASK_RKEY_3);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { resolveColumn } from "../lib/column-match.js";
|
|
3
|
-
import { COLUMNS } from "./helpers.js";
|
|
4
|
-
|
|
5
|
-
describe("resolveColumn", () => {
|
|
6
|
-
// COLUMNS: To Do (0), In Progress (1), Done (2)
|
|
7
|
-
|
|
8
|
-
it("matches by 1-based numeric index", () => {
|
|
9
|
-
expect(resolveColumn("1", COLUMNS)).toEqual(COLUMNS[0]); // To Do
|
|
10
|
-
expect(resolveColumn("2", COLUMNS)).toEqual(COLUMNS[1]); // In Progress
|
|
11
|
-
expect(resolveColumn("3", COLUMNS)).toEqual(COLUMNS[2]); // Done
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("matches by exact name (case-insensitive)", () => {
|
|
15
|
-
expect(resolveColumn("To Do", COLUMNS)).toEqual(COLUMNS[0]);
|
|
16
|
-
expect(resolveColumn("to do", COLUMNS)).toEqual(COLUMNS[0]);
|
|
17
|
-
expect(resolveColumn("IN PROGRESS", COLUMNS)).toEqual(COLUMNS[1]);
|
|
18
|
-
expect(resolveColumn("done", COLUMNS)).toEqual(COLUMNS[2]);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("matches by prefix", () => {
|
|
22
|
-
expect(resolveColumn("Do", COLUMNS)).toEqual(COLUMNS[2]); // "Done" unique prefix
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("matches by substring", () => {
|
|
26
|
-
expect(resolveColumn("Prog", COLUMNS)).toEqual(COLUMNS[1]); // "In Progress"
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("throws on ambiguous prefix match", () => {
|
|
30
|
-
// Columns with ambiguous prefixes
|
|
31
|
-
const cols = [
|
|
32
|
-
{ id: "a", name: "Alpha One", order: 0 },
|
|
33
|
-
{ id: "b", name: "Alpha Two", order: 1 },
|
|
34
|
-
];
|
|
35
|
-
expect(() => resolveColumn("Alpha", cols)).toThrow("Ambiguous");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("throws on ambiguous substring match", () => {
|
|
39
|
-
const cols = [
|
|
40
|
-
{ id: "a", name: "Pre-Review", order: 0 },
|
|
41
|
-
{ id: "b", name: "Post-Review", order: 1 },
|
|
42
|
-
];
|
|
43
|
-
// "Review" is a substring of both but not a prefix of either
|
|
44
|
-
expect(() => resolveColumn("Review", cols)).toThrow("Ambiguous");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("throws on no match with available columns", () => {
|
|
48
|
-
expect(() => resolveColumn("Nonexistent", COLUMNS)).toThrow("No column matching");
|
|
49
|
-
expect(() => resolveColumn("Nonexistent", COLUMNS)).toThrow("To Do");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("ignores out-of-range numeric index", () => {
|
|
53
|
-
// "0" is out of 1-based range, and doesn't match any name
|
|
54
|
-
expect(() => resolveColumn("0", COLUMNS)).toThrow("No column matching");
|
|
55
|
-
expect(() => resolveColumn("99", COLUMNS)).toThrow("No column matching");
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import type { BoardData } from "../lib/pds.js";
|
|
3
|
-
import {
|
|
4
|
-
OWNER_DID,
|
|
5
|
-
USER_DID,
|
|
6
|
-
BOARD_RKEY,
|
|
7
|
-
BOARD_URI,
|
|
8
|
-
TASK_RKEY_1,
|
|
9
|
-
TASK_URI_1,
|
|
10
|
-
COLUMNS,
|
|
11
|
-
makeBoard,
|
|
12
|
-
makeMaterializedTask,
|
|
13
|
-
makeComment,
|
|
14
|
-
} from "./helpers.js";
|
|
15
|
-
|
|
16
|
-
// Mock modules before importing commands
|
|
17
|
-
vi.mock("../lib/auth.js", () => ({
|
|
18
|
-
requireAgent: vi.fn(),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
vi.mock("../lib/config.js", () => ({
|
|
22
|
-
getDefaultBoard: vi.fn(),
|
|
23
|
-
loadConfig: vi.fn(() => ({ knownBoards: [] })),
|
|
24
|
-
saveConfig: vi.fn(),
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
vi.mock("../lib/pds.js", () => ({
|
|
28
|
-
fetchBoardData: vi.fn(),
|
|
29
|
-
fetchBoard: vi.fn(),
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
// Mock generateTID to return a predictable value
|
|
33
|
-
vi.mock("../lib/tid.js", async (importOriginal) => {
|
|
34
|
-
const orig = await importOriginal<typeof import("../lib/tid.js")>();
|
|
35
|
-
return {
|
|
36
|
-
...orig,
|
|
37
|
-
generateTID: vi.fn(() => "3juitestid0000000"),
|
|
38
|
-
};
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
import { requireAgent } from "../lib/auth.js";
|
|
42
|
-
import { getDefaultBoard } from "../lib/config.js";
|
|
43
|
-
import { fetchBoardData } from "../lib/pds.js";
|
|
44
|
-
import { newCommand } from "../commands/new.js";
|
|
45
|
-
import { mvCommand } from "../commands/mv.js";
|
|
46
|
-
import { editCommand } from "../commands/edit.js";
|
|
47
|
-
import { rmCommand } from "../commands/rm.js";
|
|
48
|
-
import { showCommand } from "../commands/show.js";
|
|
49
|
-
import { cardsCommand } from "../commands/cards.js";
|
|
50
|
-
|
|
51
|
-
const mockPutRecord = vi.fn(async (_input: any) => ({ uri: "at://fake", cid: "fakecid" }));
|
|
52
|
-
const mockDeleteRecord = vi.fn(async (_input: any) => ({}));
|
|
53
|
-
const mockAgent = {
|
|
54
|
-
com: {
|
|
55
|
-
atproto: {
|
|
56
|
-
repo: {
|
|
57
|
-
putRecord: mockPutRecord,
|
|
58
|
-
deleteRecord: mockDeleteRecord,
|
|
59
|
-
listRecords: vi.fn(async () => ({ data: { records: [] } })),
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
function setupMocks(taskOverrides?: Parameters<typeof makeMaterializedTask>[0]) {
|
|
66
|
-
vi.mocked(requireAgent).mockResolvedValue({
|
|
67
|
-
agent: mockAgent as any,
|
|
68
|
-
did: OWNER_DID,
|
|
69
|
-
handle: "owner.test",
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
vi.mocked(getDefaultBoard).mockReturnValue({
|
|
73
|
-
did: OWNER_DID,
|
|
74
|
-
rkey: BOARD_RKEY,
|
|
75
|
-
name: "Test Board",
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const task = makeMaterializedTask(
|
|
79
|
-
{ rkey: TASK_RKEY_1, did: OWNER_DID, title: "Test Task", ...taskOverrides },
|
|
80
|
-
{ effectiveTitle: taskOverrides?.title ?? "Test Task" },
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
const boardData: BoardData = {
|
|
84
|
-
board: makeBoard(),
|
|
85
|
-
tasks: [task],
|
|
86
|
-
trusts: [],
|
|
87
|
-
comments: [],
|
|
88
|
-
allParticipants: [OWNER_DID],
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
vi.mocked(fetchBoardData).mockResolvedValue(boardData);
|
|
92
|
-
|
|
93
|
-
return { task, boardData };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
beforeEach(() => {
|
|
97
|
-
vi.clearAllMocks();
|
|
98
|
-
// Suppress console output during tests
|
|
99
|
-
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
100
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("newCommand", () => {
|
|
104
|
-
it("creates a task record via putRecord", async () => {
|
|
105
|
-
setupMocks();
|
|
106
|
-
|
|
107
|
-
await newCommand("My New Task", { json: true });
|
|
108
|
-
|
|
109
|
-
expect(mockPutRecord).toHaveBeenCalledOnce();
|
|
110
|
-
const call = mockPutRecord.mock.calls[0][0];
|
|
111
|
-
expect(call.repo).toBe(OWNER_DID);
|
|
112
|
-
expect(call.collection).toBe("dev.skyboard.task");
|
|
113
|
-
expect(call.record.$type).toBe("dev.skyboard.task");
|
|
114
|
-
expect(call.record.title).toBe("My New Task");
|
|
115
|
-
expect(call.record.columnId).toBe("col-todo"); // first column
|
|
116
|
-
expect(call.record.position).toBeDefined();
|
|
117
|
-
expect(call.record.boardUri).toContain(BOARD_RKEY);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("creates task in specified column", async () => {
|
|
121
|
-
setupMocks();
|
|
122
|
-
|
|
123
|
-
await newCommand("Task in Done", { column: "Done", json: true });
|
|
124
|
-
|
|
125
|
-
const call = mockPutRecord.mock.calls[0][0];
|
|
126
|
-
expect(call.record.columnId).toBe("col-done");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("includes description when provided", async () => {
|
|
130
|
-
setupMocks();
|
|
131
|
-
|
|
132
|
-
await newCommand("With Desc", { description: "A description", json: true });
|
|
133
|
-
|
|
134
|
-
const call = mockPutRecord.mock.calls[0][0];
|
|
135
|
-
expect(call.record.description).toBe("A description");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("outputs JSON when --json flag is set", async () => {
|
|
139
|
-
setupMocks();
|
|
140
|
-
|
|
141
|
-
await newCommand("JSON Task", { json: true });
|
|
142
|
-
|
|
143
|
-
expect(console.log).toHaveBeenCalled();
|
|
144
|
-
const output = vi.mocked(console.log).mock.calls[0][0];
|
|
145
|
-
const parsed = JSON.parse(output);
|
|
146
|
-
expect(parsed.title).toBe("JSON Task");
|
|
147
|
-
expect(parsed.column).toBe("To Do");
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe("mvCommand", () => {
|
|
152
|
-
it("creates an op record with columnId and position", async () => {
|
|
153
|
-
setupMocks();
|
|
154
|
-
|
|
155
|
-
await mvCommand(TASK_RKEY_1, "Done", { json: true });
|
|
156
|
-
|
|
157
|
-
expect(mockPutRecord).toHaveBeenCalledOnce();
|
|
158
|
-
const call = mockPutRecord.mock.calls[0][0];
|
|
159
|
-
expect(call.repo).toBe(OWNER_DID);
|
|
160
|
-
expect(call.collection).toBe("dev.skyboard.op");
|
|
161
|
-
expect(call.record.$type).toBe("dev.skyboard.op");
|
|
162
|
-
expect(call.record.fields.columnId).toBe("col-done");
|
|
163
|
-
expect(call.record.fields.position).toBeDefined();
|
|
164
|
-
expect(call.record.targetTaskUri).toContain(TASK_RKEY_1);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("outputs JSON with rkey and column name", async () => {
|
|
168
|
-
setupMocks();
|
|
169
|
-
|
|
170
|
-
await mvCommand(TASK_RKEY_1, "In Progress", { json: true });
|
|
171
|
-
|
|
172
|
-
const output = vi.mocked(console.log).mock.calls[0][0];
|
|
173
|
-
const parsed = JSON.parse(output);
|
|
174
|
-
expect(parsed.rkey).toBe(TASK_RKEY_1);
|
|
175
|
-
expect(parsed.column).toBe("In Progress");
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
describe("editCommand", () => {
|
|
180
|
-
it("creates an op with title field", async () => {
|
|
181
|
-
setupMocks();
|
|
182
|
-
|
|
183
|
-
await editCommand(TASK_RKEY_1, { title: "New Title", json: true });
|
|
184
|
-
|
|
185
|
-
expect(mockPutRecord).toHaveBeenCalledOnce();
|
|
186
|
-
const call = mockPutRecord.mock.calls[0][0];
|
|
187
|
-
expect(call.collection).toBe("dev.skyboard.op");
|
|
188
|
-
expect(call.record.fields.title).toBe("New Title");
|
|
189
|
-
expect(call.record.targetTaskUri).toContain(TASK_RKEY_1);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("creates an op with description field", async () => {
|
|
193
|
-
setupMocks();
|
|
194
|
-
|
|
195
|
-
await editCommand(TASK_RKEY_1, { description: "New Desc", json: true });
|
|
196
|
-
|
|
197
|
-
const call = mockPutRecord.mock.calls[0][0];
|
|
198
|
-
expect(call.record.fields.description).toBe("New Desc");
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("resolves label names to IDs", async () => {
|
|
202
|
-
setupMocks();
|
|
203
|
-
|
|
204
|
-
await editCommand(TASK_RKEY_1, { label: ["bug"], json: true });
|
|
205
|
-
|
|
206
|
-
const call = mockPutRecord.mock.calls[0][0];
|
|
207
|
-
expect(call.record.fields.labelIds).toEqual(["lbl-bug"]);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("exits when no fields provided", async () => {
|
|
211
|
-
setupMocks();
|
|
212
|
-
|
|
213
|
-
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
|
|
214
|
-
throw new Error("process.exit");
|
|
215
|
-
}) as any);
|
|
216
|
-
|
|
217
|
-
await expect(editCommand(TASK_RKEY_1, {})).rejects.toThrow("process.exit");
|
|
218
|
-
expect(mockPutRecord).not.toHaveBeenCalled();
|
|
219
|
-
|
|
220
|
-
exitSpy.mockRestore();
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe("rmCommand", () => {
|
|
225
|
-
it("deletes own task via deleteRecord with --force", async () => {
|
|
226
|
-
setupMocks();
|
|
227
|
-
|
|
228
|
-
await rmCommand(TASK_RKEY_1, { force: true, json: true });
|
|
229
|
-
|
|
230
|
-
expect(mockDeleteRecord).toHaveBeenCalledOnce();
|
|
231
|
-
const call = mockDeleteRecord.mock.calls[0][0];
|
|
232
|
-
expect(call.repo).toBe(OWNER_DID);
|
|
233
|
-
expect(call.collection).toBe("dev.skyboard.task");
|
|
234
|
-
expect(call.rkey).toBe(TASK_RKEY_1);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("rejects deletion of task owned by another user", async () => {
|
|
238
|
-
// Task owned by USER_DID, but we're logged in as OWNER_DID
|
|
239
|
-
setupMocks({ did: USER_DID });
|
|
240
|
-
|
|
241
|
-
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {
|
|
242
|
-
throw new Error("process.exit");
|
|
243
|
-
}) as any);
|
|
244
|
-
|
|
245
|
-
await expect(
|
|
246
|
-
rmCommand(TASK_RKEY_1, { force: true, json: true }),
|
|
247
|
-
).rejects.toThrow("process.exit");
|
|
248
|
-
|
|
249
|
-
expect(mockDeleteRecord).not.toHaveBeenCalled();
|
|
250
|
-
expect(console.error).toHaveBeenCalled();
|
|
251
|
-
|
|
252
|
-
exitSpy.mockRestore();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("outputs JSON with deleted rkey", async () => {
|
|
256
|
-
setupMocks();
|
|
257
|
-
|
|
258
|
-
await rmCommand(TASK_RKEY_1, { force: true, json: true });
|
|
259
|
-
|
|
260
|
-
const output = vi.mocked(console.log).mock.calls[0][0];
|
|
261
|
-
const parsed = JSON.parse(output);
|
|
262
|
-
expect(parsed.deleted).toBe(TASK_RKEY_1);
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
describe("showCommand", () => {
|
|
267
|
-
it("displays card details in JSON mode", async () => {
|
|
268
|
-
const { boardData } = setupMocks();
|
|
269
|
-
|
|
270
|
-
await showCommand(TASK_RKEY_1, { json: true });
|
|
271
|
-
|
|
272
|
-
expect(console.log).toHaveBeenCalled();
|
|
273
|
-
const output = vi.mocked(console.log).mock.calls[0][0];
|
|
274
|
-
const parsed = JSON.parse(output);
|
|
275
|
-
expect(parsed.rkey).toBe(TASK_RKEY_1);
|
|
276
|
-
expect(parsed.title).toBe("Test Task");
|
|
277
|
-
expect(parsed.column).toBe("To Do");
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("includes comments in JSON output", async () => {
|
|
281
|
-
const { boardData } = setupMocks();
|
|
282
|
-
const comment = makeComment({
|
|
283
|
-
targetTaskUri: `at://${OWNER_DID}/dev.skyboard.task/${TASK_RKEY_1}`,
|
|
284
|
-
text: "Hello!",
|
|
285
|
-
});
|
|
286
|
-
boardData.comments = [comment];
|
|
287
|
-
vi.mocked(fetchBoardData).mockResolvedValue(boardData);
|
|
288
|
-
|
|
289
|
-
await showCommand(TASK_RKEY_1, { json: true });
|
|
290
|
-
|
|
291
|
-
const output = vi.mocked(console.log).mock.calls[0][0];
|
|
292
|
-
const parsed = JSON.parse(output);
|
|
293
|
-
expect(parsed.comments).toHaveLength(1);
|
|
294
|
-
expect(parsed.comments[0].text).toBe("Hello!");
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe("cardsCommand", () => {
|
|
299
|
-
it("outputs JSON with columns and cards", async () => {
|
|
300
|
-
setupMocks();
|
|
301
|
-
|
|
302
|
-
await cardsCommand({ json: true });
|
|
303
|
-
|
|
304
|
-
expect(console.log).toHaveBeenCalled();
|
|
305
|
-
const output = vi.mocked(console.log).mock.calls[0][0];
|
|
306
|
-
const parsed = JSON.parse(output);
|
|
307
|
-
expect(parsed).toBeInstanceOf(Array);
|
|
308
|
-
expect(parsed).toHaveLength(3); // 3 columns
|
|
309
|
-
expect(parsed[0].column).toBe("To Do");
|
|
310
|
-
expect(parsed[0].cards).toHaveLength(1);
|
|
311
|
-
expect(parsed[0].cards[0].title).toBe("Test Task");
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("includes all columns even if empty", async () => {
|
|
315
|
-
setupMocks();
|
|
316
|
-
|
|
317
|
-
await cardsCommand({ json: true });
|
|
318
|
-
|
|
319
|
-
const output = vi.mocked(console.log).mock.calls[0][0];
|
|
320
|
-
const parsed = JSON.parse(output);
|
|
321
|
-
expect(parsed[1].column).toBe("In Progress");
|
|
322
|
-
expect(parsed[1].cards).toHaveLength(0);
|
|
323
|
-
expect(parsed[2].column).toBe("Done");
|
|
324
|
-
expect(parsed[2].cards).toHaveLength(0);
|
|
325
|
-
});
|
|
326
|
-
});
|