@meshxdata/fops 0.0.1 → 0.0.4
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/README.md +62 -40
- package/package.json +4 -3
- package/src/agent/agent.js +161 -68
- package/src/agent/agents.js +224 -0
- package/src/agent/context.js +287 -96
- package/src/agent/index.js +1 -0
- package/src/agent/llm.js +134 -20
- package/src/auth/coda.js +128 -0
- package/src/auth/index.js +1 -0
- package/src/auth/login.js +13 -13
- package/src/auth/oauth.js +4 -4
- package/src/commands/index.js +94 -21
- package/src/config.js +2 -2
- package/src/doctor.js +208 -22
- package/src/feature-flags.js +197 -0
- package/src/plugins/api.js +23 -0
- package/src/plugins/builtins/stack-api.js +36 -0
- package/src/plugins/index.js +1 -0
- package/src/plugins/knowledge.js +124 -0
- package/src/plugins/loader.js +67 -0
- package/src/plugins/registry.js +3 -0
- package/src/project.js +20 -1
- package/src/setup/aws.js +7 -7
- package/src/setup/setup.js +18 -12
- package/src/setup/wizard.js +86 -15
- package/src/shell.js +2 -2
- package/src/skills/foundation/SKILL.md +200 -66
- package/src/ui/confirm.js +3 -2
- package/src/ui/input.js +31 -34
- package/src/ui/spinner.js +39 -13
- package/src/ui/streaming.js +2 -2
- package/STRUCTURE.md +0 -43
- package/src/agent/agent.test.js +0 -233
- package/src/agent/context.test.js +0 -81
- package/src/agent/llm.test.js +0 -139
- package/src/auth/keychain.test.js +0 -185
- package/src/auth/login.test.js +0 -192
- package/src/auth/oauth.test.js +0 -118
- package/src/auth/resolve.test.js +0 -153
- package/src/config.test.js +0 -70
- package/src/doctor.test.js +0 -134
- package/src/plugins/api.test.js +0 -95
- package/src/plugins/discovery.test.js +0 -92
- package/src/plugins/hooks.test.js +0 -118
- package/src/plugins/manifest.test.js +0 -106
- package/src/plugins/registry.test.js +0 -43
- package/src/plugins/skills.test.js +0 -173
- package/src/project.test.js +0 -196
- package/src/setup/aws.test.js +0 -280
- package/src/shell.test.js +0 -72
- package/src/ui/banner.test.js +0 -97
- package/src/ui/spinner.test.js +0 -29
package/src/agent/agent.test.js
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { formatResponse, extractSuggestedCommands } from "./agent.js";
|
|
3
|
-
|
|
4
|
-
describe("agent", () => {
|
|
5
|
-
describe("formatResponse", () => {
|
|
6
|
-
it("prefixes first line with bullet", () => {
|
|
7
|
-
const result = formatResponse("hello");
|
|
8
|
-
expect(result).toContain("hello");
|
|
9
|
-
expect(result).toMatch(/⏺/);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("renders headings", () => {
|
|
13
|
-
const result = formatResponse("# My Heading\nsome text");
|
|
14
|
-
expect(result).toContain("My Heading");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("renders h2 headings", () => {
|
|
18
|
-
const result = formatResponse("## Sub Heading");
|
|
19
|
-
expect(result).toContain("Sub Heading");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("renders h3 headings", () => {
|
|
23
|
-
const result = formatResponse("### Third Level");
|
|
24
|
-
expect(result).toContain("Third Level");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("renders unordered list items with bullet", () => {
|
|
28
|
-
const result = formatResponse("- item one\n- item two");
|
|
29
|
-
expect(result).toContain("item one");
|
|
30
|
-
expect(result).toContain("item two");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("renders * list items", () => {
|
|
34
|
-
const result = formatResponse("* star item");
|
|
35
|
-
expect(result).toContain("star item");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("renders + list items", () => {
|
|
39
|
-
const result = formatResponse("+ plus item");
|
|
40
|
-
expect(result).toContain("plus item");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("renders ordered list items", () => {
|
|
44
|
-
const result = formatResponse("1. first\n2. second");
|
|
45
|
-
expect(result).toContain("first");
|
|
46
|
-
expect(result).toContain("second");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("renders code blocks", () => {
|
|
50
|
-
const result = formatResponse("```bash\nfops doctor\n```");
|
|
51
|
-
expect(result).toContain("fops doctor");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("renders code blocks with language tag", () => {
|
|
55
|
-
const result = formatResponse("```python\nprint('hi')\n```");
|
|
56
|
-
expect(result).toContain("print('hi')");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("renders code blocks without language", () => {
|
|
60
|
-
const result = formatResponse("```\nplain code\n```");
|
|
61
|
-
expect(result).toContain("plain code");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("renders blockquotes", () => {
|
|
65
|
-
const result = formatResponse("> some quote");
|
|
66
|
-
expect(result).toContain("some quote");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("renders blockquote without space after >", () => {
|
|
70
|
-
const result = formatResponse(">tight quote");
|
|
71
|
-
expect(result).toContain("tight quote");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("handles empty text", () => {
|
|
75
|
-
const result = formatResponse("");
|
|
76
|
-
expect(result).toContain("⏺");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("handles multiple blank lines", () => {
|
|
80
|
-
const result = formatResponse("line1\n\n\nline2");
|
|
81
|
-
expect(result).toContain("line1");
|
|
82
|
-
expect(result).toContain("line2");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("renders inline bold", () => {
|
|
86
|
-
const result = formatResponse("this is **bold** text");
|
|
87
|
-
expect(result).toContain("bold");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("renders inline italic", () => {
|
|
91
|
-
const result = formatResponse("this is *italic* text");
|
|
92
|
-
expect(result).toContain("italic");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("renders inline code", () => {
|
|
96
|
-
const result = formatResponse("run `fops up` now");
|
|
97
|
-
expect(result).toContain("fops up");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("indents subsequent lines with spaces", () => {
|
|
101
|
-
const result = formatResponse("first\nsecond\nthird");
|
|
102
|
-
const lines = result.split("\n");
|
|
103
|
-
expect(lines[0]).toMatch(/⏺/);
|
|
104
|
-
expect(lines[1]).toMatch(/^\s\s/); // indented
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("handles multi-line code block correctly", () => {
|
|
108
|
-
const input = "Before:\n```bash\nline1\nline2\nline3\n```\nAfter.";
|
|
109
|
-
const result = formatResponse(input);
|
|
110
|
-
expect(result).toContain("line1");
|
|
111
|
-
expect(result).toContain("line2");
|
|
112
|
-
expect(result).toContain("line3");
|
|
113
|
-
expect(result).toContain("After.");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("handles indented list items", () => {
|
|
117
|
-
const result = formatResponse(" - indented item");
|
|
118
|
-
expect(result).toContain("indented item");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("handles mixed content", () => {
|
|
122
|
-
const input = [
|
|
123
|
-
"# Title",
|
|
124
|
-
"Some text with **bold** and `code`.",
|
|
125
|
-
"",
|
|
126
|
-
"- item 1",
|
|
127
|
-
"- item 2",
|
|
128
|
-
"",
|
|
129
|
-
"```bash",
|
|
130
|
-
"fops doctor",
|
|
131
|
-
"```",
|
|
132
|
-
"",
|
|
133
|
-
"> A quote",
|
|
134
|
-
].join("\n");
|
|
135
|
-
const result = formatResponse(input);
|
|
136
|
-
expect(result).toContain("Title");
|
|
137
|
-
expect(result).toContain("bold");
|
|
138
|
-
expect(result).toContain("fops doctor");
|
|
139
|
-
expect(result).toContain("A quote");
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe("extractSuggestedCommands", () => {
|
|
144
|
-
it("extracts commands from bash code blocks", () => {
|
|
145
|
-
const text = "Try running:\n```bash\nfops doctor\n```\nThen:\n```bash\nfops up\n```";
|
|
146
|
-
const cmds = extractSuggestedCommands(text);
|
|
147
|
-
expect(cmds).toEqual(["fops doctor", "fops up"]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("extracts from sh blocks", () => {
|
|
151
|
-
const text = "```sh\necho hello\n```";
|
|
152
|
-
const cmds = extractSuggestedCommands(text);
|
|
153
|
-
expect(cmds).toEqual(["echo hello"]);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("returns empty for non-bash blocks", () => {
|
|
157
|
-
const text = "```json\n{}\n```";
|
|
158
|
-
const cmds = extractSuggestedCommands(text);
|
|
159
|
-
expect(cmds).toEqual([]);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("returns empty for unlabeled code blocks", () => {
|
|
163
|
-
const text = "```\nsome code\n```";
|
|
164
|
-
const cmds = extractSuggestedCommands(text);
|
|
165
|
-
expect(cmds).toEqual([]);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("returns empty when no code blocks", () => {
|
|
169
|
-
const cmds = extractSuggestedCommands("just some text");
|
|
170
|
-
expect(cmds).toEqual([]);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("returns empty for empty string", () => {
|
|
174
|
-
const cmds = extractSuggestedCommands("");
|
|
175
|
-
expect(cmds).toEqual([]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("skips comment-only lines", () => {
|
|
179
|
-
const text = "```bash\n# this is a comment\nfops status\n```";
|
|
180
|
-
const cmds = extractSuggestedCommands(text);
|
|
181
|
-
expect(cmds).toEqual(["fops status"]);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("takes only first command from multi-line blocks", () => {
|
|
185
|
-
const text = "```bash\nfops doctor\nfops up\n```";
|
|
186
|
-
const cmds = extractSuggestedCommands(text);
|
|
187
|
-
expect(cmds).toEqual(["fops doctor"]);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("skips blocks where all lines are comments", () => {
|
|
191
|
-
const text = "```bash\n# just a comment\n# another comment\n```";
|
|
192
|
-
const cmds = extractSuggestedCommands(text);
|
|
193
|
-
expect(cmds).toEqual([]);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("handles BASH uppercase language tag", () => {
|
|
197
|
-
const text = "```BASH\nfops status\n```";
|
|
198
|
-
const cmds = extractSuggestedCommands(text);
|
|
199
|
-
expect(cmds).toEqual(["fops status"]);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("handles Bash mixed case", () => {
|
|
203
|
-
const text = "```Bash\nfops logs\n```";
|
|
204
|
-
const cmds = extractSuggestedCommands(text);
|
|
205
|
-
expect(cmds).toEqual(["fops logs"]);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("trims whitespace from commands", () => {
|
|
209
|
-
const text = "```bash\n fops doctor \n```";
|
|
210
|
-
const cmds = extractSuggestedCommands(text);
|
|
211
|
-
expect(cmds).toEqual(["fops doctor"]);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("handles multiple blocks interleaved with text", () => {
|
|
215
|
-
const text = [
|
|
216
|
-
"First do this:",
|
|
217
|
-
"```bash",
|
|
218
|
-
"fops init",
|
|
219
|
-
"```",
|
|
220
|
-
"Then check:",
|
|
221
|
-
"```bash",
|
|
222
|
-
"fops doctor",
|
|
223
|
-
"```",
|
|
224
|
-
"Finally:",
|
|
225
|
-
"```bash",
|
|
226
|
-
"fops up",
|
|
227
|
-
"```",
|
|
228
|
-
].join("\n");
|
|
229
|
-
const cmds = extractSuggestedCommands(text);
|
|
230
|
-
expect(cmds).toEqual(["fops init", "fops doctor", "fops up"]);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { FOUNDATION_SYSTEM_PROMPT, getFoundationContextBlock } from "./context.js";
|
|
3
|
-
|
|
4
|
-
describe("context", () => {
|
|
5
|
-
describe("FOUNDATION_SYSTEM_PROMPT", () => {
|
|
6
|
-
it("defines FOPS personality", () => {
|
|
7
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("FOPS");
|
|
8
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Foundation Operator");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("includes capabilities", () => {
|
|
12
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Setup & Init");
|
|
13
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Operations");
|
|
14
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Debugging");
|
|
15
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Security");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("lists available commands", () => {
|
|
19
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("fops init");
|
|
20
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("fops up");
|
|
21
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("fops down");
|
|
22
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("fops doctor");
|
|
23
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("fops status");
|
|
24
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("fops logs");
|
|
25
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("fops restart");
|
|
26
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("fops login");
|
|
27
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("fops chat");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("includes security rules", () => {
|
|
31
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Never output API keys");
|
|
32
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Validate file paths");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("includes service ports", () => {
|
|
36
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("9001");
|
|
37
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("3002");
|
|
38
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("5432");
|
|
39
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("9092");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("includes personality traits", () => {
|
|
43
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Terse");
|
|
44
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Precise");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("includes setup checklist", () => {
|
|
48
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("Setup Checklist");
|
|
49
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("npm install -g");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("instructs to use fops commands not raw make", () => {
|
|
53
|
-
expect(FOUNDATION_SYSTEM_PROMPT).toContain("ALWAYS use `fops` commands");
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("getFoundationContextBlock", () => {
|
|
58
|
-
it("returns project root message when root is provided", () => {
|
|
59
|
-
const result = getFoundationContextBlock("/path/to/project");
|
|
60
|
-
expect(result).toContain("/path/to/project");
|
|
61
|
-
expect(result).toContain("Project root");
|
|
62
|
-
expect(result).toContain("Commands run in this directory");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("returns init message when root is null", () => {
|
|
66
|
-
const result = getFoundationContextBlock(null);
|
|
67
|
-
expect(result).toContain("fops init");
|
|
68
|
-
expect(result).toContain("No Foundation project root found");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("returns init message when root is undefined", () => {
|
|
72
|
-
const result = getFoundationContextBlock(undefined);
|
|
73
|
-
expect(result).toContain("fops init");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("returns init message when root is empty string", () => {
|
|
77
|
-
const result = getFoundationContextBlock("");
|
|
78
|
-
expect(result).toContain("fops init");
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
});
|
package/src/agent/llm.test.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
vi.mock("execa", () => ({
|
|
4
|
-
execa: vi.fn(),
|
|
5
|
-
execaSync: vi.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
vi.mock("../auth/index.js", () => ({
|
|
9
|
-
resolveAnthropicApiKey: vi.fn(() => null),
|
|
10
|
-
resolveOpenAiApiKey: vi.fn(() => null),
|
|
11
|
-
readClaudeCodeKeychain: vi.fn(() => null),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
vi.mock("../ui/index.js", () => ({
|
|
15
|
-
renderSpinner: vi.fn(() => ({ stop: vi.fn() })),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
const { execa, execaSync } = await import("execa");
|
|
19
|
-
const { readClaudeCodeKeychain } = await import("../auth/index.js");
|
|
20
|
-
const { hasClaudeCode, hasClaudeCodeAuth, runViaClaudeCode, streamViaClaudeCode } = await import("./llm.js");
|
|
21
|
-
|
|
22
|
-
describe("llm", () => {
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
vi.clearAllMocks();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("hasClaudeCode", () => {
|
|
28
|
-
it("returns true when claude is on PATH", () => {
|
|
29
|
-
execaSync.mockReturnValue({ stdout: "/usr/local/bin/claude" });
|
|
30
|
-
expect(hasClaudeCode()).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("returns false when claude is not found", () => {
|
|
34
|
-
execaSync.mockImplementation(() => { throw new Error("not found"); });
|
|
35
|
-
expect(hasClaudeCode()).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("calls which claude", () => {
|
|
39
|
-
execaSync.mockReturnValue({ stdout: "" });
|
|
40
|
-
hasClaudeCode();
|
|
41
|
-
expect(execaSync).toHaveBeenCalledWith("which", ["claude"]);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("hasClaudeCodeAuth", () => {
|
|
46
|
-
it("returns true when keychain has accessToken", () => {
|
|
47
|
-
readClaudeCodeKeychain.mockReturnValue({ accessToken: "sk-ant-oat01-xxx" });
|
|
48
|
-
expect(hasClaudeCodeAuth()).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("returns false when keychain returns null", () => {
|
|
52
|
-
readClaudeCodeKeychain.mockReturnValue(null);
|
|
53
|
-
expect(hasClaudeCodeAuth()).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("returns false when accessToken is missing", () => {
|
|
57
|
-
readClaudeCodeKeychain.mockReturnValue({ refreshToken: "rt-xxx" });
|
|
58
|
-
expect(hasClaudeCodeAuth()).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("returns false when accessToken is empty", () => {
|
|
62
|
-
readClaudeCodeKeychain.mockReturnValue({ accessToken: "" });
|
|
63
|
-
expect(hasClaudeCodeAuth()).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("returns false when keychain returns empty object", () => {
|
|
67
|
-
readClaudeCodeKeychain.mockReturnValue({});
|
|
68
|
-
expect(hasClaudeCodeAuth()).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("runViaClaudeCode", () => {
|
|
73
|
-
it("calls claude CLI with prompt and system prompt", async () => {
|
|
74
|
-
execa.mockResolvedValue({ stdout: "response text" });
|
|
75
|
-
const result = await runViaClaudeCode("my prompt", "system prompt");
|
|
76
|
-
expect(result).toBe("response text");
|
|
77
|
-
expect(execa).toHaveBeenCalledWith(
|
|
78
|
-
"claude",
|
|
79
|
-
["-p", "--no-session-persistence", "--append-system-prompt", "system prompt"],
|
|
80
|
-
expect.objectContaining({ input: "my prompt", encoding: "utf8" })
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("omits system prompt flag when not provided", async () => {
|
|
85
|
-
execa.mockResolvedValue({ stdout: "response" });
|
|
86
|
-
await runViaClaudeCode("prompt", null);
|
|
87
|
-
const args = execa.mock.calls[0][1];
|
|
88
|
-
expect(args).not.toContain("--append-system-prompt");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("returns empty string when stdout is empty", async () => {
|
|
92
|
-
execa.mockResolvedValue({ stdout: "" });
|
|
93
|
-
const result = await runViaClaudeCode("prompt", "sys");
|
|
94
|
-
expect(result).toBe("");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("returns empty string when stdout is null", async () => {
|
|
98
|
-
execa.mockResolvedValue({ stdout: null });
|
|
99
|
-
const result = await runViaClaudeCode("prompt", "sys");
|
|
100
|
-
expect(result).toBe("");
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("streamViaClaudeCode", () => {
|
|
105
|
-
it("accumulates chunks and returns full text", async () => {
|
|
106
|
-
const { EventEmitter } = await import("node:events");
|
|
107
|
-
const mockStdout = new EventEmitter();
|
|
108
|
-
const mockProc = Promise.resolve();
|
|
109
|
-
mockProc.stdout = mockStdout;
|
|
110
|
-
|
|
111
|
-
execa.mockReturnValue(mockProc);
|
|
112
|
-
|
|
113
|
-
const chunks = [];
|
|
114
|
-
const resultPromise = streamViaClaudeCode("prompt", "sys", (chunk) => chunks.push(chunk));
|
|
115
|
-
|
|
116
|
-
mockStdout.emit("data", Buffer.from("hello "));
|
|
117
|
-
mockStdout.emit("data", Buffer.from("world"));
|
|
118
|
-
|
|
119
|
-
const result = await resultPromise;
|
|
120
|
-
expect(result).toBe("hello world");
|
|
121
|
-
expect(chunks).toEqual(["hello ", "world"]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("works without onChunk callback", async () => {
|
|
125
|
-
const { EventEmitter } = await import("node:events");
|
|
126
|
-
const mockStdout = new EventEmitter();
|
|
127
|
-
const mockProc = Promise.resolve();
|
|
128
|
-
mockProc.stdout = mockStdout;
|
|
129
|
-
|
|
130
|
-
execa.mockReturnValue(mockProc);
|
|
131
|
-
|
|
132
|
-
const resultPromise = streamViaClaudeCode("prompt", "sys");
|
|
133
|
-
mockStdout.emit("data", Buffer.from("data"));
|
|
134
|
-
|
|
135
|
-
const result = await resultPromise;
|
|
136
|
-
expect(result).toBe("data");
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
vi.mock("execa", () => ({
|
|
4
|
-
execaSync: vi.fn(),
|
|
5
|
-
}));
|
|
6
|
-
|
|
7
|
-
const { execaSync } = await import("execa");
|
|
8
|
-
const { readClaudeCodeKeychain, saveClaudeCodeKeychain } = await import("./keychain.js");
|
|
9
|
-
|
|
10
|
-
describe("auth/keychain", () => {
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
vi.clearAllMocks();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe("readClaudeCodeKeychain", () => {
|
|
16
|
-
it("returns null on non-darwin platforms", () => {
|
|
17
|
-
const original = process.platform;
|
|
18
|
-
Object.defineProperty(process, "platform", { value: "linux" });
|
|
19
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
20
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("returns null on win32", () => {
|
|
24
|
-
const original = process.platform;
|
|
25
|
-
Object.defineProperty(process, "platform", { value: "win32" });
|
|
26
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
27
|
-
expect(execaSync).not.toHaveBeenCalled();
|
|
28
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("returns tokens on darwin when keychain has data", () => {
|
|
32
|
-
const original = process.platform;
|
|
33
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
34
|
-
execaSync.mockReturnValue({
|
|
35
|
-
stdout: JSON.stringify({
|
|
36
|
-
claudeAiOauth: {
|
|
37
|
-
accessToken: "sk-ant-oat01-test",
|
|
38
|
-
refreshToken: "rt-test",
|
|
39
|
-
},
|
|
40
|
-
}),
|
|
41
|
-
});
|
|
42
|
-
const result = readClaudeCodeKeychain();
|
|
43
|
-
expect(result).toEqual({
|
|
44
|
-
accessToken: "sk-ant-oat01-test",
|
|
45
|
-
refreshToken: "rt-test",
|
|
46
|
-
});
|
|
47
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("returns null refreshToken when not in keychain data", () => {
|
|
51
|
-
const original = process.platform;
|
|
52
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
53
|
-
execaSync.mockReturnValue({
|
|
54
|
-
stdout: JSON.stringify({
|
|
55
|
-
claudeAiOauth: {
|
|
56
|
-
accessToken: "sk-ant-oat01-test",
|
|
57
|
-
},
|
|
58
|
-
}),
|
|
59
|
-
});
|
|
60
|
-
const result = readClaudeCodeKeychain();
|
|
61
|
-
expect(result).toEqual({
|
|
62
|
-
accessToken: "sk-ant-oat01-test",
|
|
63
|
-
refreshToken: null,
|
|
64
|
-
});
|
|
65
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("returns null on darwin when keychain throws", () => {
|
|
69
|
-
const original = process.platform;
|
|
70
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
71
|
-
execaSync.mockImplementation(() => { throw new Error("not found"); });
|
|
72
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
73
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("returns null when keychain data has no claudeAiOauth", () => {
|
|
77
|
-
const original = process.platform;
|
|
78
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
79
|
-
execaSync.mockReturnValue({ stdout: JSON.stringify({ other: "data" }) });
|
|
80
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
81
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("returns null when stdout is not valid JSON", () => {
|
|
85
|
-
const original = process.platform;
|
|
86
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
87
|
-
execaSync.mockReturnValue({ stdout: "not json" });
|
|
88
|
-
expect(readClaudeCodeKeychain()).toBe(null);
|
|
89
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("calls security find-generic-password on darwin", () => {
|
|
93
|
-
const original = process.platform;
|
|
94
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
95
|
-
execaSync.mockReturnValue({ stdout: "{}" });
|
|
96
|
-
readClaudeCodeKeychain();
|
|
97
|
-
expect(execaSync).toHaveBeenCalledWith(
|
|
98
|
-
"security",
|
|
99
|
-
["find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
|
100
|
-
{ encoding: "utf8" }
|
|
101
|
-
);
|
|
102
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("saveClaudeCodeKeychain", () => {
|
|
107
|
-
it("returns false on non-darwin platforms", () => {
|
|
108
|
-
const original = process.platform;
|
|
109
|
-
Object.defineProperty(process, "platform", { value: "linux" });
|
|
110
|
-
expect(saveClaudeCodeKeychain("token")).toBe(false);
|
|
111
|
-
expect(execaSync).not.toHaveBeenCalled();
|
|
112
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("calls security commands on darwin", () => {
|
|
116
|
-
const original = process.platform;
|
|
117
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
118
|
-
execaSync.mockReturnValue({});
|
|
119
|
-
const result = saveClaudeCodeKeychain("access-token", "refresh-token");
|
|
120
|
-
expect(result).toBe(true);
|
|
121
|
-
expect(execaSync).toHaveBeenCalledWith(
|
|
122
|
-
"security",
|
|
123
|
-
expect.arrayContaining(["delete-generic-password"])
|
|
124
|
-
);
|
|
125
|
-
expect(execaSync).toHaveBeenCalledWith(
|
|
126
|
-
"security",
|
|
127
|
-
expect.arrayContaining(["add-generic-password"])
|
|
128
|
-
);
|
|
129
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("succeeds even when delete fails (no existing entry)", () => {
|
|
133
|
-
const original = process.platform;
|
|
134
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
135
|
-
let callCount = 0;
|
|
136
|
-
execaSync.mockImplementation((_cmd, args) => {
|
|
137
|
-
if (args.includes("delete-generic-password")) {
|
|
138
|
-
throw new Error("item not found");
|
|
139
|
-
}
|
|
140
|
-
return {};
|
|
141
|
-
});
|
|
142
|
-
const result = saveClaudeCodeKeychain("access-token");
|
|
143
|
-
expect(result).toBe(true);
|
|
144
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("returns false when add-generic-password fails", () => {
|
|
148
|
-
const original = process.platform;
|
|
149
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
150
|
-
execaSync.mockImplementation((_cmd, args) => {
|
|
151
|
-
if (args.includes("add-generic-password")) {
|
|
152
|
-
throw new Error("keychain locked");
|
|
153
|
-
}
|
|
154
|
-
return {};
|
|
155
|
-
});
|
|
156
|
-
const result = saveClaudeCodeKeychain("token");
|
|
157
|
-
expect(result).toBe(false);
|
|
158
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("saves without refreshToken when not provided", () => {
|
|
162
|
-
const original = process.platform;
|
|
163
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
164
|
-
execaSync.mockReturnValue({});
|
|
165
|
-
saveClaudeCodeKeychain("access-only");
|
|
166
|
-
const addCall = execaSync.mock.calls.find((c) => c[1].includes("add-generic-password"));
|
|
167
|
-
const data = JSON.parse(addCall[1][addCall[1].indexOf("-w") + 1]);
|
|
168
|
-
expect(data.claudeAiOauth.accessToken).toBe("access-only");
|
|
169
|
-
expect(data.claudeAiOauth.refreshToken).toBeUndefined();
|
|
170
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("includes refreshToken when provided", () => {
|
|
174
|
-
const original = process.platform;
|
|
175
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
176
|
-
execaSync.mockReturnValue({});
|
|
177
|
-
saveClaudeCodeKeychain("access", "refresh");
|
|
178
|
-
const addCall = execaSync.mock.calls.find((c) => c[1].includes("add-generic-password"));
|
|
179
|
-
const data = JSON.parse(addCall[1][addCall[1].indexOf("-w") + 1]);
|
|
180
|
-
expect(data.claudeAiOauth.accessToken).toBe("access");
|
|
181
|
-
expect(data.claudeAiOauth.refreshToken).toBe("refresh");
|
|
182
|
-
Object.defineProperty(process, "platform", { value: original });
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
});
|