auq-mcp-server 2.2.2 → 2.4.0
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 +82 -0
- package/dist/bin/auq.js +45 -39
- package/dist/bin/tui-app.js +78 -8
- package/dist/package.json +1 -1
- package/dist/src/__tests__/server.abort.test.js +214 -0
- package/dist/src/cli/commands/__tests__/answer.test.js +199 -0
- package/dist/src/cli/commands/__tests__/config.test.js +218 -0
- package/dist/src/cli/commands/__tests__/sessions.test.js +282 -0
- package/dist/src/cli/commands/answer.js +128 -0
- package/dist/src/cli/commands/config.js +263 -0
- package/dist/src/cli/commands/sessions.js +164 -0
- package/dist/src/cli/utils.js +95 -0
- package/dist/src/config/__tests__/ConfigLoader.test.js +41 -0
- package/dist/src/config/defaults.js +3 -0
- package/dist/src/config/types.js +4 -0
- package/dist/src/core/ask-user-questions.js +3 -2
- package/dist/src/i18n/locales/en.js +8 -1
- package/dist/src/i18n/locales/ko.js +8 -1
- package/dist/src/server.js +64 -11
- package/dist/src/session/SessionManager.js +69 -4
- package/dist/src/session/__tests__/SessionManager.test.js +65 -0
- package/dist/src/tui/ThemeProvider.js +2 -1
- package/dist/src/tui/__tests__/session-watcher.test.js +109 -0
- package/dist/src/tui/components/ConfirmationDialog.js +5 -4
- package/dist/src/tui/components/Footer.js +24 -23
- package/dist/src/tui/components/ReviewScreen.js +2 -1
- package/dist/src/tui/components/SessionDots.js +33 -4
- package/dist/src/tui/components/SessionPicker.js +27 -18
- package/dist/src/tui/components/Spinner.js +19 -0
- package/dist/src/tui/components/StepperView.js +71 -7
- package/dist/src/tui/components/WaitingScreen.js +2 -1
- package/dist/src/tui/components/__tests__/ConfirmationDialog.test.js +134 -0
- package/dist/src/tui/components/__tests__/Footer.test.js +121 -0
- package/dist/src/tui/components/__tests__/ReviewScreen.test.js +89 -0
- package/dist/src/tui/components/__tests__/SessionDots.test.js +160 -1
- package/dist/src/tui/components/__tests__/SessionPicker.test.js +43 -1
- package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +160 -0
- package/dist/src/tui/components/__tests__/StepperView.keyboard.test.js +135 -0
- package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -0
- package/dist/src/tui/components/__tests__/WaitingScreen.test.js +60 -0
- package/dist/src/tui/constants/keybindings.js +40 -0
- package/dist/src/tui/session-watcher.js +50 -0
- package/dist/src/tui/themes/catppuccin-latte.js +7 -0
- package/dist/src/tui/themes/catppuccin-mocha.js +7 -0
- package/dist/src/tui/themes/dark.js +7 -0
- package/dist/src/tui/themes/dracula.js +7 -0
- package/dist/src/tui/themes/github-dark.js +7 -0
- package/dist/src/tui/themes/github-light.js +7 -0
- package/dist/src/tui/themes/gruvbox-dark.js +7 -0
- package/dist/src/tui/themes/gruvbox-light.js +7 -0
- package/dist/src/tui/themes/light.js +7 -0
- package/dist/src/tui/themes/monokai.js +7 -0
- package/dist/src/tui/themes/nord.js +7 -0
- package/dist/src/tui/themes/one-dark.js +7 -0
- package/dist/src/tui/themes/rose-pine.js +7 -0
- package/dist/src/tui/themes/solarized-dark.js +7 -0
- package/dist/src/tui/themes/solarized-light.js +7 -0
- package/dist/src/tui/themes/tokyo-night.js +7 -0
- package/dist/src/tui/utils/__tests__/detectTheme.test.js +78 -0
- package/dist/src/tui/utils/__tests__/staleDetection.test.js +118 -0
- package/dist/src/tui/utils/staleDetection.js +51 -0
- package/package.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the `auq answer` CLI command.
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { SessionManager } from "../../../session/SessionManager.js";
|
|
7
|
+
import { runAnswerCommand } from "../answer.js";
|
|
8
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
9
|
+
const testBaseDir = "/tmp/auq-test-cli-answer";
|
|
10
|
+
const sampleQuestions = [
|
|
11
|
+
{
|
|
12
|
+
title: "Language",
|
|
13
|
+
prompt: "Which language do you prefer?",
|
|
14
|
+
options: [
|
|
15
|
+
{ label: "TypeScript", description: "Typed JS" },
|
|
16
|
+
{ label: "Python", description: "Scripting" },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
title: "Framework",
|
|
21
|
+
prompt: "Pick a framework",
|
|
22
|
+
options: [
|
|
23
|
+
{ label: "React" },
|
|
24
|
+
{ label: "Vue" },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
// Stub getSessionDirectory so the answer command always targets our temp dir.
|
|
29
|
+
vi.mock("../../../session/utils.js", async (importOriginal) => {
|
|
30
|
+
const actual = (await importOriginal());
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
getSessionDirectory: () => testBaseDir,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
// ── Test Suite ─────────────────────────────────────────────────────────────
|
|
37
|
+
describe("answer command", () => {
|
|
38
|
+
let sessionManager;
|
|
39
|
+
let consoleLogSpy;
|
|
40
|
+
let consoleErrorSpy;
|
|
41
|
+
let consoleWarnSpy;
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
let exitSpy;
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
|
|
46
|
+
sessionManager = new SessionManager({
|
|
47
|
+
baseDir: testBaseDir,
|
|
48
|
+
maxSessions: 10,
|
|
49
|
+
sessionTimeout: 0,
|
|
50
|
+
});
|
|
51
|
+
await sessionManager.initialize();
|
|
52
|
+
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
53
|
+
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
54
|
+
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
55
|
+
exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { throw new Error("process.exit"); }));
|
|
56
|
+
});
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
consoleLogSpy.mockRestore();
|
|
59
|
+
consoleErrorSpy.mockRestore();
|
|
60
|
+
consoleWarnSpy.mockRestore();
|
|
61
|
+
exitSpy.mockRestore();
|
|
62
|
+
await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
|
|
63
|
+
});
|
|
64
|
+
// ── Success: answer a pending session ─────────────────────────────────
|
|
65
|
+
it("should answer a pending session with valid answers JSON", async () => {
|
|
66
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
67
|
+
const answersJson = JSON.stringify({
|
|
68
|
+
"0": { selectedOption: "TypeScript" },
|
|
69
|
+
"1": { selectedOption: "React" },
|
|
70
|
+
});
|
|
71
|
+
await runAnswerCommand([sessionId, "--answers", answersJson]);
|
|
72
|
+
// Verify status is completed
|
|
73
|
+
const status = await sessionManager.getSessionStatus(sessionId);
|
|
74
|
+
expect(status?.status).toBe("completed");
|
|
75
|
+
// Verify answers were saved
|
|
76
|
+
const savedAnswers = await sessionManager.getSessionAnswers(sessionId);
|
|
77
|
+
expect(savedAnswers).not.toBeNull();
|
|
78
|
+
expect(savedAnswers.answers).toHaveLength(2);
|
|
79
|
+
expect(savedAnswers.answers[0].selectedOption).toBe("TypeScript");
|
|
80
|
+
expect(savedAnswers.answers[1].selectedOption).toBe("React");
|
|
81
|
+
});
|
|
82
|
+
it("should write answers.json to session directory", async () => {
|
|
83
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
84
|
+
const answersJson = JSON.stringify({
|
|
85
|
+
"0": { selectedOption: "Python" },
|
|
86
|
+
});
|
|
87
|
+
await runAnswerCommand([sessionId, "--answers", answersJson]);
|
|
88
|
+
// Directly verify the file exists
|
|
89
|
+
const answersPath = `${testBaseDir}/${sessionId}/answers.json`;
|
|
90
|
+
const stat = await fs.stat(answersPath);
|
|
91
|
+
expect(stat.isFile()).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
// ── Reject path ──────────────────────────────────────────────────────
|
|
94
|
+
it("should reject session with reason", async () => {
|
|
95
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
96
|
+
await runAnswerCommand([
|
|
97
|
+
sessionId,
|
|
98
|
+
"--reject",
|
|
99
|
+
"--reason",
|
|
100
|
+
"not applicable",
|
|
101
|
+
]);
|
|
102
|
+
const status = await sessionManager.getSessionStatus(sessionId);
|
|
103
|
+
expect(status?.status).toBe("rejected");
|
|
104
|
+
expect(status?.rejectionReason).toBe("not applicable");
|
|
105
|
+
});
|
|
106
|
+
it("should reject session without reason", async () => {
|
|
107
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
108
|
+
await runAnswerCommand([sessionId, "--reject"]);
|
|
109
|
+
const status = await sessionManager.getSessionStatus(sessionId);
|
|
110
|
+
expect(status?.status).toBe("rejected");
|
|
111
|
+
});
|
|
112
|
+
// ── Error cases ──────────────────────────────────────────────────────
|
|
113
|
+
it("should error when sessionId is missing", async () => {
|
|
114
|
+
await expect(runAnswerCommand([])).rejects.toThrow("process.exit");
|
|
115
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
116
|
+
const errorOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
117
|
+
expect(errorOutput).toContain("Missing session ID");
|
|
118
|
+
});
|
|
119
|
+
it("should error when session does not exist", async () => {
|
|
120
|
+
const fakeId = "00000000-0000-4000-a000-000000000000";
|
|
121
|
+
await expect(runAnswerCommand([fakeId, "--answers", '{ "0": {} }'])).rejects.toThrow("process.exit");
|
|
122
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
123
|
+
const errorOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
124
|
+
expect(errorOutput).toContain("Session not found");
|
|
125
|
+
});
|
|
126
|
+
it("should error when answers JSON is malformed", async () => {
|
|
127
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
128
|
+
await expect(runAnswerCommand([sessionId, "--answers", "not-json"])).rejects.toThrow("process.exit");
|
|
129
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
130
|
+
const errorOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
131
|
+
expect(errorOutput).toContain("Invalid answers JSON");
|
|
132
|
+
});
|
|
133
|
+
it("should error when neither --answers nor --reject is provided", async () => {
|
|
134
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
135
|
+
await expect(runAnswerCommand([sessionId])).rejects.toThrow("process.exit");
|
|
136
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
137
|
+
const errorOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
138
|
+
expect(errorOutput).toContain("Either --answers or --reject is required");
|
|
139
|
+
});
|
|
140
|
+
// ── Abandoned session handling ────────────────────────────────────────
|
|
141
|
+
it("should warn for abandoned session without --force", async () => {
|
|
142
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
143
|
+
// Mark as abandoned
|
|
144
|
+
await sessionManager.updateSessionStatus(sessionId, "abandoned");
|
|
145
|
+
await expect(runAnswerCommand([
|
|
146
|
+
sessionId,
|
|
147
|
+
"--answers",
|
|
148
|
+
JSON.stringify({ "0": { selectedOption: "TypeScript" } }),
|
|
149
|
+
])).rejects.toThrow("process.exit");
|
|
150
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
151
|
+
const errorOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
152
|
+
expect(errorOutput).toContain("AI disconnected");
|
|
153
|
+
expect(errorOutput).toContain("--force");
|
|
154
|
+
});
|
|
155
|
+
it("should succeed for abandoned session with --force", async () => {
|
|
156
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
157
|
+
await sessionManager.updateSessionStatus(sessionId, "abandoned");
|
|
158
|
+
const answersJson = JSON.stringify({
|
|
159
|
+
"0": { selectedOption: "TypeScript" },
|
|
160
|
+
"1": { selectedOption: "React" },
|
|
161
|
+
});
|
|
162
|
+
await runAnswerCommand([sessionId, "--answers", answersJson, "--force"]);
|
|
163
|
+
const status = await sessionManager.getSessionStatus(sessionId);
|
|
164
|
+
expect(status?.status).toBe("completed");
|
|
165
|
+
// Check the warning was emitted
|
|
166
|
+
const warnOutput = consoleWarnSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
167
|
+
expect(warnOutput).toContain("Warning");
|
|
168
|
+
});
|
|
169
|
+
// ── JSON output ──────────────────────────────────────────────────────
|
|
170
|
+
it("should produce valid JSON output with --json flag on answer", async () => {
|
|
171
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
172
|
+
const answersJson = JSON.stringify({
|
|
173
|
+
"0": { selectedOption: "TypeScript" },
|
|
174
|
+
});
|
|
175
|
+
await runAnswerCommand([sessionId, "--answers", answersJson, "--json"]);
|
|
176
|
+
// console.log should have been called with valid JSON
|
|
177
|
+
const jsonOutput = consoleLogSpy.mock.calls[0][0];
|
|
178
|
+
const result = JSON.parse(jsonOutput);
|
|
179
|
+
expect(result.success).toBe(true);
|
|
180
|
+
expect(result.sessionId).toBe(sessionId);
|
|
181
|
+
expect(result.status).toBe("completed");
|
|
182
|
+
});
|
|
183
|
+
it("should produce valid JSON output with --json flag on reject", async () => {
|
|
184
|
+
const sessionId = await sessionManager.createSession(sampleQuestions);
|
|
185
|
+
await runAnswerCommand([sessionId, "--reject", "--json"]);
|
|
186
|
+
const jsonOutput = consoleLogSpy.mock.calls[0][0];
|
|
187
|
+
const result = JSON.parse(jsonOutput);
|
|
188
|
+
expect(result.success).toBe(true);
|
|
189
|
+
expect(result.sessionId).toBe(sessionId);
|
|
190
|
+
expect(result.status).toBe("rejected");
|
|
191
|
+
});
|
|
192
|
+
it("should produce valid JSON error output with --json flag", async () => {
|
|
193
|
+
await expect(runAnswerCommand(["--json"])).rejects.toThrow("process.exit");
|
|
194
|
+
const jsonOutput = consoleLogSpy.mock.calls[0][0];
|
|
195
|
+
const result = JSON.parse(jsonOutput);
|
|
196
|
+
expect(result.success).toBe(false);
|
|
197
|
+
expect(result.error).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { runConfigCommand } from "../config.js";
|
|
4
|
+
import { DEFAULT_CONFIG } from "../../../config/defaults.js";
|
|
5
|
+
// Mock fs module
|
|
6
|
+
vi.mock("fs", () => ({
|
|
7
|
+
existsSync: vi.fn(),
|
|
8
|
+
readFileSync: vi.fn(),
|
|
9
|
+
writeFileSync: vi.fn(),
|
|
10
|
+
mkdirSync: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("os", () => ({
|
|
13
|
+
homedir: vi.fn(() => "/home/testuser"),
|
|
14
|
+
}));
|
|
15
|
+
describe("config command", () => {
|
|
16
|
+
let consoleLogSpy;
|
|
17
|
+
let consoleErrorSpy;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
process.exitCode = undefined;
|
|
21
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
22
|
+
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
23
|
+
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
24
|
+
// Default: no config files exist
|
|
25
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
consoleLogSpy.mockRestore();
|
|
29
|
+
consoleErrorSpy.mockRestore();
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
process.exitCode = undefined;
|
|
32
|
+
});
|
|
33
|
+
// ── Config Get ──────────────────────────────────────────────────
|
|
34
|
+
describe("config get", () => {
|
|
35
|
+
it("should show all config values with defaults when no config files exist", async () => {
|
|
36
|
+
await runConfigCommand(["get"]);
|
|
37
|
+
// Should print key=value lines for all known keys
|
|
38
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
39
|
+
const allOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
40
|
+
expect(allOutput).toContain("maxOptions = 5");
|
|
41
|
+
expect(allOutput).toContain("sessionTimeout = 0");
|
|
42
|
+
expect(allOutput).toContain("staleThreshold = 7200000");
|
|
43
|
+
expect(allOutput).toContain("notifyOnStale = true");
|
|
44
|
+
expect(allOutput).toContain("staleAction = warn");
|
|
45
|
+
expect(allOutput).toContain("notifications.enabled = true");
|
|
46
|
+
expect(allOutput).toContain("notifications.sound = true");
|
|
47
|
+
});
|
|
48
|
+
it("should show specific key value", async () => {
|
|
49
|
+
await runConfigCommand(["get", "maxOptions"]);
|
|
50
|
+
expect(consoleLogSpy).toHaveBeenCalledWith("maxOptions = 5");
|
|
51
|
+
});
|
|
52
|
+
it("should show nested key value with dot notation", async () => {
|
|
53
|
+
await runConfigCommand(["get", "notifications.enabled"]);
|
|
54
|
+
expect(consoleLogSpy).toHaveBeenCalledWith("notifications.enabled = true");
|
|
55
|
+
});
|
|
56
|
+
it("should error on unknown key", async () => {
|
|
57
|
+
await runConfigCommand(["get", "unknownKey"]);
|
|
58
|
+
expect(process.exitCode).toBe(1);
|
|
59
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
60
|
+
const errorOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
61
|
+
expect(errorOutput).toContain("Unknown config key");
|
|
62
|
+
expect(errorOutput).toContain("unknownKey");
|
|
63
|
+
});
|
|
64
|
+
it("should output valid JSON with --json flag for all config", async () => {
|
|
65
|
+
await runConfigCommand(["get", "--json"]);
|
|
66
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
|
67
|
+
const output = consoleLogSpy.mock.calls[0][0];
|
|
68
|
+
const parsed = JSON.parse(output);
|
|
69
|
+
expect(parsed.success).toBe(true);
|
|
70
|
+
expect(parsed.config).toBeDefined();
|
|
71
|
+
expect(parsed.config.maxOptions).toBe(DEFAULT_CONFIG.maxOptions);
|
|
72
|
+
});
|
|
73
|
+
it("should output valid JSON with --json flag for specific key", async () => {
|
|
74
|
+
await runConfigCommand(["get", "staleThreshold", "--json"]);
|
|
75
|
+
const output = consoleLogSpy.mock.calls[0][0];
|
|
76
|
+
const parsed = JSON.parse(output);
|
|
77
|
+
expect(parsed.success).toBe(true);
|
|
78
|
+
expect(parsed.key).toBe("staleThreshold");
|
|
79
|
+
expect(parsed.value).toBe(7200000);
|
|
80
|
+
});
|
|
81
|
+
it("should merge local config over defaults", async () => {
|
|
82
|
+
const cwd = process.cwd();
|
|
83
|
+
const localPath = `${cwd}/.auqrc.json`;
|
|
84
|
+
vi.mocked(fs.existsSync).mockImplementation((path) => {
|
|
85
|
+
return String(path) === localPath;
|
|
86
|
+
});
|
|
87
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ maxOptions: 8 }));
|
|
88
|
+
await runConfigCommand(["get", "maxOptions"]);
|
|
89
|
+
expect(consoleLogSpy).toHaveBeenCalledWith("maxOptions = 8");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
// ── Config Set ──────────────────────────────────────────────────
|
|
93
|
+
describe("config set", () => {
|
|
94
|
+
it("should write valid key to local config file", async () => {
|
|
95
|
+
const cwd = process.cwd();
|
|
96
|
+
const expectedPath = `${cwd}/.auqrc.json`;
|
|
97
|
+
// Simulate directory exists but file does not
|
|
98
|
+
vi.mocked(fs.existsSync).mockImplementation((path) => {
|
|
99
|
+
return String(path) === cwd;
|
|
100
|
+
});
|
|
101
|
+
await runConfigCommand(["set", "staleThreshold", "3600000"]);
|
|
102
|
+
expect(process.exitCode).toBeUndefined();
|
|
103
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
104
|
+
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
|
105
|
+
expect(String(writeCall[0])).toBe(expectedPath);
|
|
106
|
+
const written = JSON.parse(writeCall[1]);
|
|
107
|
+
expect(written.staleThreshold).toBe(3600000);
|
|
108
|
+
});
|
|
109
|
+
it("should write to global config with --global flag", async () => {
|
|
110
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
111
|
+
await runConfigCommand(["set", "staleThreshold", "3600000", "--global"]);
|
|
112
|
+
expect(process.exitCode).toBeUndefined();
|
|
113
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
114
|
+
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
|
115
|
+
expect(String(writeCall[0])).toContain(".config/auq/.auqrc.json");
|
|
116
|
+
});
|
|
117
|
+
it("should create directory if it doesn't exist", async () => {
|
|
118
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
119
|
+
await runConfigCommand(["set", "maxOptions", "8", "--global"]);
|
|
120
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith(expect.stringContaining(".config/auq"), { recursive: true });
|
|
121
|
+
});
|
|
122
|
+
it("should merge with existing config", async () => {
|
|
123
|
+
const cwd = process.cwd();
|
|
124
|
+
const localPath = `${cwd}/.auqrc.json`;
|
|
125
|
+
vi.mocked(fs.existsSync).mockImplementation((path) => {
|
|
126
|
+
return String(path) === localPath || String(path) === cwd;
|
|
127
|
+
});
|
|
128
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ maxOptions: 6 }));
|
|
129
|
+
await runConfigCommand(["set", "staleThreshold", "5000000"]);
|
|
130
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
131
|
+
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
|
132
|
+
const written = JSON.parse(writeCall[1]);
|
|
133
|
+
// Existing key should be preserved
|
|
134
|
+
expect(written.maxOptions).toBe(6);
|
|
135
|
+
// New key should be added
|
|
136
|
+
expect(written.staleThreshold).toBe(5000000);
|
|
137
|
+
});
|
|
138
|
+
it("should error on unknown config key with valid keys list", async () => {
|
|
139
|
+
await runConfigCommand(["set", "badKey", "value"]);
|
|
140
|
+
expect(process.exitCode).toBe(1);
|
|
141
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
142
|
+
const errorOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
143
|
+
expect(errorOutput).toContain("Unknown config key");
|
|
144
|
+
expect(errorOutput).toContain("badKey");
|
|
145
|
+
// Should list valid keys
|
|
146
|
+
expect(errorOutput).toContain("maxOptions");
|
|
147
|
+
expect(errorOutput).toContain("staleThreshold");
|
|
148
|
+
});
|
|
149
|
+
it("should error on invalid value type", async () => {
|
|
150
|
+
await runConfigCommand(["set", "maxOptions", "notanumber"]);
|
|
151
|
+
expect(process.exitCode).toBe(1);
|
|
152
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
153
|
+
const errorOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
154
|
+
expect(errorOutput).toContain("Invalid value");
|
|
155
|
+
});
|
|
156
|
+
it("should coerce boolean string values correctly", async () => {
|
|
157
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
158
|
+
await runConfigCommand(["set", "notifyOnStale", "false"]);
|
|
159
|
+
expect(process.exitCode).toBeUndefined();
|
|
160
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
161
|
+
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
|
162
|
+
const written = JSON.parse(writeCall[1]);
|
|
163
|
+
expect(written.notifyOnStale).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
it("should validate enum values", async () => {
|
|
166
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
167
|
+
await runConfigCommand(["set", "staleAction", "archive"]);
|
|
168
|
+
expect(process.exitCode).toBeUndefined();
|
|
169
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
170
|
+
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
|
171
|
+
const written = JSON.parse(writeCall[1]);
|
|
172
|
+
expect(written.staleAction).toBe("archive");
|
|
173
|
+
});
|
|
174
|
+
it("should reject invalid enum values", async () => {
|
|
175
|
+
await runConfigCommand(["set", "staleAction", "invalid_action"]);
|
|
176
|
+
expect(process.exitCode).toBe(1);
|
|
177
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
it("should handle nested key set with dot notation", async () => {
|
|
180
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
181
|
+
await runConfigCommand(["set", "notifications.enabled", "false"]);
|
|
182
|
+
expect(process.exitCode).toBeUndefined();
|
|
183
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
184
|
+
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
|
185
|
+
const written = JSON.parse(writeCall[1]);
|
|
186
|
+
expect(written.notifications.enabled).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
it("should output JSON with --json flag", async () => {
|
|
189
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
190
|
+
await runConfigCommand(["set", "staleThreshold", "3600000", "--json"]);
|
|
191
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
|
192
|
+
const output = consoleLogSpy.mock.calls[0][0];
|
|
193
|
+
const parsed = JSON.parse(output);
|
|
194
|
+
expect(parsed.success).toBe(true);
|
|
195
|
+
expect(parsed.key).toBe("staleThreshold");
|
|
196
|
+
expect(parsed.value).toBe(3600000);
|
|
197
|
+
expect(parsed.file).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
it("should error when key and value are missing", async () => {
|
|
200
|
+
await runConfigCommand(["set"]);
|
|
201
|
+
expect(process.exitCode).toBe(1);
|
|
202
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
// ── Config help ─────────────────────────────────────────────────
|
|
206
|
+
describe("config help", () => {
|
|
207
|
+
it("should show usage help when no subcommand provided", async () => {
|
|
208
|
+
await runConfigCommand([]);
|
|
209
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
210
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
211
|
+
expect(output).toContain("Usage");
|
|
212
|
+
});
|
|
213
|
+
it("should show usage and set exitCode for unknown subcommand", async () => {
|
|
214
|
+
await runConfigCommand(["unknown"]);
|
|
215
|
+
expect(process.exitCode).toBe(1);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|