@golba98/codexa 1.0.2 → 1.0.3

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.
Files changed (150) hide show
  1. package/README.md +391 -94
  2. package/package.json +12 -7
  3. package/src/app.tsx +149 -117
  4. package/src/commands/handler.ts +4 -4
  5. package/src/config/buildInfo.ts +2 -2
  6. package/src/config/persistence.ts +10 -0
  7. package/src/config/settings.ts +7 -15
  8. package/src/config/updateCheckCache.ts +19 -1
  9. package/src/core/channel.ts +23 -0
  10. package/src/core/executables/codexExecutable.ts +1 -0
  11. package/src/core/executables/executableResolver.ts +61 -43
  12. package/src/core/launchContext.ts +33 -33
  13. package/src/core/process/processValidation.ts +9 -5
  14. package/src/core/providerLauncher/launcher.ts +59 -42
  15. package/src/core/updateCheck.ts +111 -95
  16. package/src/session/types.ts +2 -1
  17. package/src/ui/ActionRequiredBlock.tsx +5 -5
  18. package/src/ui/ActivityBars.tsx +3 -3
  19. package/src/ui/ActivityIndicator.tsx +6 -6
  20. package/src/ui/AgentBlock.tsx +6 -6
  21. package/src/ui/AnimatedStatusText.tsx +1 -1
  22. package/src/ui/AppShell.tsx +9 -24
  23. package/src/ui/AttachmentImportPanel.tsx +8 -8
  24. package/src/ui/AuthPanel.tsx +20 -20
  25. package/src/ui/BottomComposer.tsx +117 -78
  26. package/src/ui/DashCard.tsx +3 -3
  27. package/src/ui/Markdown.tsx +17 -17
  28. package/src/ui/ModelPickerScreen.tsx +10 -10
  29. package/src/ui/ModelReasoningPicker.tsx +14 -14
  30. package/src/ui/Panel.tsx +3 -3
  31. package/src/ui/PlanActionPicker.tsx +6 -6
  32. package/src/ui/PlanReviewPanel.tsx +8 -8
  33. package/src/ui/ProviderPicker.tsx +18 -18
  34. package/src/ui/RunFooter.tsx +3 -3
  35. package/src/ui/SelectionPanel.tsx +4 -4
  36. package/src/ui/SettingsPanel.tsx +8 -8
  37. package/src/ui/Spinner.tsx +1 -1
  38. package/src/ui/TextEntryPanel.tsx +10 -10
  39. package/src/ui/ThinkingBlock.tsx +8 -8
  40. package/src/ui/Timeline.tsx +21 -17
  41. package/src/ui/TopHeader.tsx +421 -293
  42. package/src/ui/TurnGroup.tsx +31 -31
  43. package/src/ui/UpdateAvailableCard.tsx +41 -0
  44. package/src/ui/UpdatePromptPanel.tsx +203 -0
  45. package/src/ui/focus.ts +3 -0
  46. package/src/ui/layout.ts +1 -1
  47. package/src/ui/logoVariants.ts +107 -0
  48. package/src/ui/modeDisplay.ts +12 -12
  49. package/src/ui/runtimeDisplay.ts +100 -0
  50. package/src/ui/theme.tsx +274 -395
  51. package/src/ui/timelineMeasure.ts +29 -19
  52. package/scripts/audit-codexa-capabilities.mjs +0 -466
  53. package/scripts/gen-build-info.mjs +0 -33
  54. package/scripts/smoke-terminal-bench.mjs +0 -35
  55. package/src/appRenderStability.test.ts +0 -131
  56. package/src/commands/handler.test.ts +0 -655
  57. package/src/config/launchArgs.test.ts +0 -189
  58. package/src/config/layeredConfig.test.ts +0 -143
  59. package/src/config/persistence.test.ts +0 -114
  60. package/src/config/runtimeConfig.test.ts +0 -218
  61. package/src/config/settings.test.ts +0 -155
  62. package/src/config/trustStore.test.ts +0 -29
  63. package/src/core/attachments.test.ts +0 -155
  64. package/src/core/auth/codexAuth.test.ts +0 -68
  65. package/src/core/cleanupFastFail.test.ts +0 -76
  66. package/src/core/codexExecArgs.test.ts +0 -195
  67. package/src/core/codexLaunch.test.ts +0 -205
  68. package/src/core/codexPrompt.test.ts +0 -252
  69. package/src/core/executables/codexExecutable.test.ts +0 -212
  70. package/src/core/executables/executableResolver.test.ts +0 -129
  71. package/src/core/executables/geminiExecutable.test.ts +0 -116
  72. package/src/core/executables/pathSanityScan.test.ts +0 -47
  73. package/src/core/githubDiagnostics.test.ts +0 -92
  74. package/src/core/hollowResponseFormat.test.ts +0 -58
  75. package/src/core/launchContext.test.ts +0 -157
  76. package/src/core/models/codexCapabilities.test.ts +0 -45
  77. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  78. package/src/core/models/modelSpecs.test.ts +0 -283
  79. package/src/core/perf/renderDebug.test.ts +0 -230
  80. package/src/core/planStorage.test.ts +0 -143
  81. package/src/core/process/CommandRunner.test.ts +0 -105
  82. package/src/core/projectInstructions.test.ts +0 -50
  83. package/src/core/providerLauncher/launcher.test.ts +0 -238
  84. package/src/core/providerLauncher/registry.test.ts +0 -324
  85. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  86. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  87. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  88. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  89. package/src/core/providerRuntime/gemini.test.ts +0 -437
  90. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  91. package/src/core/providerRuntime/local.test.ts +0 -787
  92. package/src/core/providerRuntime/registry.test.ts +0 -233
  93. package/src/core/providers/codexJsonStream.test.ts +0 -148
  94. package/src/core/providers/codexSubprocess.test.ts +0 -68
  95. package/src/core/providers/codexTranscript.test.ts +0 -284
  96. package/src/core/terminal/startupClear.test.ts +0 -55
  97. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  98. package/src/core/terminal/terminalControl.test.ts +0 -75
  99. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  100. package/src/core/terminal/terminalSelection.test.ts +0 -42
  101. package/src/core/terminal/terminalTitle.test.ts +0 -328
  102. package/src/core/updateCheck.test.ts +0 -194
  103. package/src/core/workspaceActivity.test.ts +0 -163
  104. package/src/core/workspaceGuard.test.ts +0 -151
  105. package/src/core/workspaceRoot.test.ts +0 -23
  106. package/src/exec.test.ts +0 -13
  107. package/src/headless/execArgs.test.ts +0 -147
  108. package/src/headless/execRunner.test.ts +0 -436
  109. package/src/index.test.tsx +0 -620
  110. package/src/session/appSession.test.ts +0 -897
  111. package/src/session/chatLifecycle.test.ts +0 -64
  112. package/src/session/liveRenderScheduler.test.ts +0 -201
  113. package/src/session/planFlow.test.ts +0 -103
  114. package/src/session/planTranscript.test.ts +0 -65
  115. package/src/session/promptRunSchedule.test.ts +0 -36
  116. package/src/ui/ActivityIndicator.test.tsx +0 -58
  117. package/src/ui/AgentBlock.test.ts +0 -6
  118. package/src/ui/AnimatedStatusText.test.ts +0 -16
  119. package/src/ui/AppShell.test.tsx +0 -1776
  120. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  121. package/src/ui/BottomComposer.test.ts +0 -674
  122. package/src/ui/CodexLogo.tsx +0 -55
  123. package/src/ui/Markdown.test.ts +0 -157
  124. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  125. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  126. package/src/ui/ModelPickerState.test.tsx +0 -151
  127. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  128. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  129. package/src/ui/PromptCardBorder.test.tsx +0 -161
  130. package/src/ui/ProviderPicker.test.tsx +0 -289
  131. package/src/ui/ProviderShortcut.test.tsx +0 -143
  132. package/src/ui/SettingsPanel.test.tsx +0 -233
  133. package/src/ui/Timeline.test.ts +0 -2067
  134. package/src/ui/TimelineNavigation.test.tsx +0 -201
  135. package/src/ui/TopHeader.test.tsx +0 -254
  136. package/src/ui/TurnGroup.test.tsx +0 -365
  137. package/src/ui/busyStatusAnimation.test.ts +0 -30
  138. package/src/ui/commandNormalize.test.ts +0 -142
  139. package/src/ui/diffRenderer.test.ts +0 -102
  140. package/src/ui/focusFlow.test.tsx +0 -1098
  141. package/src/ui/inputBuffer.test.ts +0 -151
  142. package/src/ui/layout.test.ts +0 -146
  143. package/src/ui/modeDisplay.test.ts +0 -42
  144. package/src/ui/runActivityView.test.ts +0 -89
  145. package/src/ui/runLifecycleView.test.tsx +0 -237
  146. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  147. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  148. package/src/ui/textLayout.test.ts +0 -18
  149. package/src/ui/themeFlow.test.ts +0 -53
  150. package/src/ui/timelineMeasureCache.test.ts +0 -986
@@ -1,230 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { existsSync, readFileSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import test from "node:test";
6
- import {
7
- configureRenderDebug,
8
- getRenderDebugLogPath,
9
- traceBlankFrame,
10
- traceEvent,
11
- traceLayoutValidity,
12
- traceLifecycleEvent,
13
- traceLifecycleTransition,
14
- traceFlickerEvent,
15
- traceStatusTick,
16
- traceRender,
17
- traceTerminalWrite,
18
- } from "./renderDebug.js";
19
-
20
- function clean(path: string): void {
21
- rmSync(path, { force: true });
22
- }
23
-
24
- test("render debug stays quiet by default", () => {
25
- const logPath = join(tmpdir(), `codexa-render-debug-quiet-${process.pid}.jsonl`);
26
- clean(logPath);
27
-
28
- configureRenderDebug({ CODEXA_RENDER_DEBUG_FILE: logPath });
29
- traceEvent("test", "quiet");
30
- traceRender("QuietComponent", "test");
31
-
32
- assert.equal(existsSync(logPath), false);
33
- });
34
-
35
- test("render debug writes JSONL only when explicitly enabled", () => {
36
- const logPath = join(tmpdir(), `codexa-render-debug-enabled-${process.pid}.jsonl`);
37
- clean(logPath);
38
-
39
- try {
40
- configureRenderDebug({
41
- CODEXA_RENDER_DEBUG: "1",
42
- CODEXA_RENDER_DEBUG_FILE: logPath,
43
- });
44
- traceRender("EnabledComponent", "unit");
45
-
46
- assert.equal(getRenderDebugLogPath(), logPath);
47
- const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
48
- assert.equal(records[0]?.kind, "session");
49
- assert.equal(records[1]?.kind, "render");
50
- assert.equal(records[1]?.component, "EnabledComponent");
51
- assert.equal(records[1]?.reason, "unit");
52
- } finally {
53
- configureRenderDebug({});
54
- clean(logPath);
55
- }
56
- });
57
-
58
- test("render debug defaults to the repo-local diagnostic log path", () => {
59
- configureRenderDebug({});
60
- assert.equal(getRenderDebugLogPath(), join(process.cwd(), ".codexa-debug", "render-debug.log"));
61
- });
62
-
63
- test("render debug creates missing log directories", () => {
64
- const logPath = join(tmpdir(), `codexa-render-debug-nested-${process.pid}`, "render-debug.log");
65
- rmSync(join(tmpdir(), `codexa-render-debug-nested-${process.pid}`), { force: true, recursive: true });
66
-
67
- try {
68
- configureRenderDebug({
69
- CODEXA_RENDER_DEBUG: "1",
70
- CODEXA_RENDER_DEBUG_FILE: logPath,
71
- });
72
- assert.equal(existsSync(logPath), true);
73
- } finally {
74
- configureRenderDebug({});
75
- rmSync(join(tmpdir(), `codexa-render-debug-nested-${process.pid}`), { force: true, recursive: true });
76
- }
77
- });
78
-
79
- test("render debug records lifecycle, layout, and blank-frame diagnostics", () => {
80
- const logPath = join(tmpdir(), `codexa-render-diagnostics-${process.pid}.jsonl`);
81
- clean(logPath);
82
-
83
- try {
84
- configureRenderDebug({
85
- CODEXA_RENDER_DEBUG: "1",
86
- CODEXA_RENDER_DEBUG_FILE: logPath,
87
- });
88
- traceLifecycleEvent("Timeline", "mount", { viewportRows: 12 });
89
- traceLayoutValidity("Timeline", { viewportRows: 0, cols: 120 });
90
- traceBlankFrame("Timeline", { reason: "visible-rows-zero-with-events" });
91
-
92
- const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
93
- assert.equal(records[1]?.kind, "lifecycle");
94
- assert.equal(records[1]?.component, "Timeline");
95
- assert.equal(records[1]?.event, "mount");
96
- assert.equal(records[2]?.kind, "layout");
97
- assert.equal(records[2]?.event, "invalidLayout");
98
- assert.deepEqual(records[2]?.invalidValues, [{ key: "viewportRows", value: 0 }]);
99
- assert.equal(records[3]?.kind, "blankFrame");
100
- assert.equal(records[3]?.reason, "visible-rows-zero-with-events");
101
- } finally {
102
- configureRenderDebug({});
103
- clean(logPath);
104
- }
105
- });
106
-
107
- test("terminal writes are classified for clear and reset diagnosis", () => {
108
- const logPath = join(tmpdir(), `codexa-terminal-classification-${process.pid}.jsonl`);
109
- clean(logPath);
110
-
111
- try {
112
- configureRenderDebug({
113
- CODEXA_RENDER_DEBUG: "1",
114
- CODEXA_RENDER_DEBUG_FILE: logPath,
115
- });
116
- traceTerminalWrite(
117
- "stdout",
118
- "unit",
119
- "\x1b[2J\x1b[3J\x1b[H\x1bc\x1b[?1049h\x1b]0;CODEXA\x07\x1b[?2004h\x1b[?1000h",
120
- );
121
-
122
- const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
123
- assert.equal(records[1]?.kind, "stdout");
124
- assert.equal(records[1]?.containsViewportClear, true);
125
- assert.equal(records[1]?.containsScrollbackClear, true);
126
- assert.equal(records[1]?.containsCursorHome, true);
127
- assert.equal(records[1]?.containsTerminalReset, true);
128
- assert.equal(records[1]?.containsAlternateScreen, true);
129
- assert.equal(records[1]?.containsTitleSequence, true);
130
- assert.equal(records[1]?.containsBracketedPaste, true);
131
- assert.equal(records[1]?.containsMouseMode, true);
132
- } finally {
133
- configureRenderDebug({});
134
- clean(logPath);
135
- }
136
- });
137
-
138
- test("CODEXA_DEBUG_RENDER aliases render debug logging", () => {
139
- const logPath = join(tmpdir(), `codexa-debug-render-alias-${process.pid}.jsonl`);
140
- clean(logPath);
141
-
142
- try {
143
- configureRenderDebug({
144
- CODEXA_DEBUG_RENDER: "1",
145
- CODEXA_RENDER_DEBUG_FILE: logPath,
146
- });
147
- traceRender("AliasComponent", "unit");
148
-
149
- const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
150
- assert.equal(records[0]?.kind, "session");
151
- assert.equal(records[1]?.kind, "render");
152
- assert.equal(records[1]?.component, "AliasComponent");
153
- } finally {
154
- configureRenderDebug({});
155
- clean(logPath);
156
- }
157
- });
158
-
159
- test("render trace flag enables compact render diagnostics", () => {
160
- const logPath = join(tmpdir(), `codexa-render-trace-${process.pid}.jsonl`);
161
- clean(logPath);
162
-
163
- try {
164
- configureRenderDebug({
165
- CODEXA_DEBUG_RENDER_TRACE: "1",
166
- CODEXA_RENDER_DEBUG_FILE: logPath,
167
- });
168
- traceRender("TraceComponent", "unit");
169
- traceFlickerEvent("viewportSlice", { reason: "unit" });
170
-
171
- const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
172
- assert.equal(records[0]?.kind, "session");
173
- assert.equal(records[1]?.kind, "render");
174
- assert.equal(records[1]?.component, "TraceComponent");
175
- assert.equal(records[2]?.kind, "flicker");
176
- assert.equal(records[2]?.event, "viewportSlice");
177
- } finally {
178
- configureRenderDebug({});
179
- clean(logPath);
180
- }
181
- });
182
-
183
- test("lifecycle trace is gated by CODEXA_DEBUG_LIFECYCLE", () => {
184
- const logPath = join(tmpdir(), `codexa-lifecycle-debug-${process.pid}.jsonl`);
185
- clean(logPath);
186
-
187
- try {
188
- configureRenderDebug({
189
- CODEXA_DEBUG_LIFECYCLE: "1",
190
- CODEXA_RENDER_DEBUG_FILE: logPath,
191
- });
192
- traceLifecycleTransition({
193
- prevKind: "THINKING",
194
- nextKind: "IDLE",
195
- reason: "unit",
196
- });
197
-
198
- const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
199
- assert.equal(records.length, 1);
200
- assert.equal(records[0]?.kind, "lifecycle");
201
- assert.equal(records[0]?.reason, "unit");
202
- } finally {
203
- configureRenderDebug({});
204
- clean(logPath);
205
- }
206
- });
207
-
208
- test("flicker trace is gated by CODEXA_DEBUG_FLICKER", () => {
209
- const logPath = join(tmpdir(), `codexa-flicker-debug-${process.pid}.jsonl`);
210
- clean(logPath);
211
-
212
- try {
213
- configureRenderDebug({
214
- CODEXA_DEBUG_FLICKER: "1",
215
- CODEXA_RENDER_DEBUG_FILE: logPath,
216
- });
217
- traceFlickerEvent("timelineRender", { reason: "unit" });
218
- traceStatusTick({ owner: "Status", label: "Codex is thinking" });
219
-
220
- const records = readFileSync(logPath, "utf8").trim().split("\n").map((line) => JSON.parse(line));
221
- assert.equal(records.length, 2);
222
- assert.equal(records[0]?.kind, "flicker");
223
- assert.equal(records[0]?.event, "timelineRender");
224
- assert.equal(records[1]?.kind, "flicker");
225
- assert.equal(records[1]?.event, "statusTick");
226
- } finally {
227
- configureRenderDebug({});
228
- clean(logPath);
229
- }
230
- });
@@ -1,143 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test, { afterEach, beforeEach, describe } from "node:test";
3
- import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
4
- import { join } from "path";
5
- import { tmpdir } from "os";
6
- import { resolvePlanDir, savePlan, readPlan } from "./planStorage.js";
7
-
8
- describe("resolvePlanDir", () => {
9
- const savedEnv: Record<string, string | undefined> = {};
10
- const envKeys = ["CODEXA_PLAN_DIR", "LOCALAPPDATA", "APPDATA", "XDG_DATA_HOME"];
11
-
12
- beforeEach(() => {
13
- for (const key of envKeys) {
14
- savedEnv[key] = process.env[key];
15
- }
16
- });
17
-
18
- afterEach(() => {
19
- for (const key of envKeys) {
20
- if (savedEnv[key] === undefined) {
21
- delete process.env[key];
22
- } else {
23
- process.env[key] = savedEnv[key];
24
- }
25
- }
26
- });
27
-
28
- test("CODEXA_PLAN_DIR override takes priority", () => {
29
- process.env["CODEXA_PLAN_DIR"] = "/custom/plan/dir";
30
- assert.equal(resolvePlanDir(), "/custom/plan/dir");
31
- });
32
-
33
- test("Windows LOCALAPPDATA path resolution", () => {
34
- delete process.env["CODEXA_PLAN_DIR"];
35
- process.env["LOCALAPPDATA"] = "C:\\Users\\test\\AppData\\Local";
36
- const result = resolvePlanDir("win32");
37
- assert.ok(result.includes("Codexa"));
38
- assert.ok(result.includes("plans"));
39
- assert.ok(result.includes("AppData"));
40
- });
41
-
42
- test("Windows APPDATA fallback", () => {
43
- delete process.env["CODEXA_PLAN_DIR"];
44
- delete process.env["LOCALAPPDATA"];
45
- process.env["APPDATA"] = "C:\\Users\\test\\AppData\\Roaming";
46
- const result = resolvePlanDir("win32");
47
- assert.ok(result.includes("Codexa"));
48
- assert.ok(result.includes("plans"));
49
- assert.ok(result.includes("Roaming"));
50
- });
51
-
52
- test("macOS path", () => {
53
- delete process.env["CODEXA_PLAN_DIR"];
54
- const result = resolvePlanDir("darwin");
55
- assert.ok(result.includes("Library"));
56
- assert.ok(result.includes("Codexa"));
57
- assert.ok(result.includes("plans"));
58
- });
59
-
60
- test("Linux default path", () => {
61
- delete process.env["CODEXA_PLAN_DIR"];
62
- delete process.env["XDG_DATA_HOME"];
63
- const result = resolvePlanDir("linux");
64
- assert.ok(result.includes(".local"));
65
- assert.ok(result.includes("share"));
66
- assert.ok(result.includes("codexa"));
67
- assert.ok(result.includes("plans"));
68
- });
69
-
70
- test("Linux XDG_DATA_HOME override", () => {
71
- delete process.env["CODEXA_PLAN_DIR"];
72
- process.env["XDG_DATA_HOME"] = "/custom/xdg/data";
73
- const result = resolvePlanDir("linux");
74
- assert.ok(result.includes("custom"));
75
- assert.ok(result.includes("xdg"));
76
- assert.ok(result.includes("codexa"));
77
- assert.ok(result.includes("plans"));
78
- });
79
- });
80
-
81
- describe("savePlan", () => {
82
- let tempDir: string;
83
-
84
- beforeEach(() => {
85
- tempDir = join(tmpdir(), `planStorage-test-${Date.now()}`);
86
- mkdirSync(tempDir, { recursive: true });
87
- process.env["CODEXA_PLAN_DIR"] = tempDir;
88
- });
89
-
90
- afterEach(() => {
91
- delete process.env["CODEXA_PLAN_DIR"];
92
- rmSync(tempDir, { recursive: true, force: true });
93
- });
94
-
95
- test("safe filename format with timestamp and 8-hex-char hash", () => {
96
- const result = savePlan("# My Plan", "/some/workspace");
97
- assert.notEqual(result, null);
98
- const filename = result!.split(/[/\\]/).pop()!;
99
- assert.match(filename, /^plan-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}.*-[0-9a-f]{8}\.md$/);
100
- });
101
-
102
- test("written content can be read back", () => {
103
- const content = "# Test Plan\n\n- Step 1\n- Step 2\n";
104
- const result = savePlan(content, "/workspace");
105
- assert.notEqual(result, null);
106
- assert.ok(existsSync(result!));
107
- assert.equal(readPlan(result!), content);
108
- });
109
-
110
- test("write failure returns null", () => {
111
- process.env["CODEXA_PLAN_DIR"] = "/nonexistent\x00/bad/path";
112
- const result = savePlan("content", "/workspace");
113
- assert.equal(result, null);
114
- });
115
-
116
- test("does not write to process.cwd()", () => {
117
- const cwdPlanDir = join(process.cwd(), ".codexa");
118
- const beforeMarkdown = existsSync(cwdPlanDir)
119
- ? readdirSync(cwdPlanDir).filter((name) => name.endsWith(".md")).sort()
120
- : [];
121
- savePlan("# Plan", process.cwd());
122
- const afterMarkdown = existsSync(cwdPlanDir)
123
- ? readdirSync(cwdPlanDir).filter((name) => name.endsWith(".md")).sort()
124
- : [];
125
- assert.deepEqual(afterMarkdown, beforeMarkdown);
126
- });
127
- });
128
-
129
- describe("readPlan", () => {
130
- test("returns null for missing file", () => {
131
- assert.equal(readPlan("/nonexistent/file.md"), null);
132
- });
133
-
134
- test("reads existing file content", () => {
135
- const tempFile = join(tmpdir(), `plan-read-test-${Date.now()}.md`);
136
- writeFileSync(tempFile, "hello plan", "utf-8");
137
- try {
138
- assert.equal(readPlan(tempFile), "hello plan");
139
- } finally {
140
- rmSync(tempFile, { force: true });
141
- }
142
- });
143
- });
@@ -1,105 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { readFileSync } from "node:fs";
3
- import { fileURLToPath } from "node:url";
4
- import test from "node:test";
5
- import { runCommand, runShellCommand, summarizeCommandResult, type CommandResult } from "./CommandRunner.js";
6
-
7
- function makeResult(overrides: Partial<CommandResult> = {}): CommandResult {
8
- return {
9
- status: "completed",
10
- exitCode: 0,
11
- signal: null,
12
- stdout: "",
13
- stderr: "",
14
- startedAt: 1,
15
- endedAt: 2,
16
- durationMs: 1,
17
- userMessage: "Command completed.",
18
- ...overrides,
19
- };
20
- }
21
-
22
- test("summarizes ripgrep file listings without flooding the UI", () => {
23
- const result = makeResult({
24
- stdout: "src/app.tsx\nsrc/ui/BottomComposer.tsx\n",
25
- });
26
-
27
- assert.equal(summarizeCommandResult("rg --files", result), "Found 2 files.");
28
- });
29
-
30
- test("keeps a concise fallback summary for generic successful commands", () => {
31
- const result = makeResult({
32
- stdout: "alpha\nbeta\ngamma\n",
33
- });
34
-
35
- assert.equal(summarizeCommandResult("node script.js", result), "Produced 3 lines of output.");
36
- });
37
-
38
- test("preserves the failure message for unsuccessful commands", () => {
39
- const result = makeResult({
40
- status: "failed",
41
- exitCode: 1,
42
- userMessage: "git exited with code 1.",
43
- stderr: "fatal: not a git repository",
44
- });
45
-
46
- assert.equal(summarizeCommandResult("git status", result), "git exited with code 1.");
47
- });
48
-
49
- test("runCommand executes a direct executable with argument array", async () => {
50
- const runner = runCommand({
51
- executable: process.execPath,
52
- args: ["-e", "console.log(process.argv[1])", "direct-ok"],
53
- cwd: process.cwd(),
54
- });
55
-
56
- const result = await runner.result;
57
- assert.equal(result.status, "completed");
58
- assert.equal(result.exitCode, 0);
59
- assert.equal(result.stdout.trim(), "direct-ok");
60
- });
61
-
62
- test("runCommand rejects obvious executable injection", () => {
63
- assert.throws(
64
- () => runCommand({
65
- executable: "node & echo injected",
66
- args: ["--version"],
67
- cwd: process.cwd(),
68
- }),
69
- /single executable name|shell metacharacters/i,
70
- );
71
- });
72
-
73
- test("runShellCommand is the explicit shell execution path", async () => {
74
- const runner = runShellCommand("echo shell-ok", { cwd: process.cwd() });
75
- const result = await runner.result;
76
-
77
- assert.equal(result.status, "completed");
78
- assert.equal(result.exitCode, 0);
79
- assert.match(result.stdout, /shell-ok/);
80
- });
81
-
82
- test("command runner reports lifecycle boundaries for terminal title reassertion", () => {
83
- const source = readFileSync(fileURLToPath(new URL("./CommandRunner.ts", import.meta.url)), "utf8");
84
- const beforeSpawnIndex = source.indexOf('handlers.onProcessLifecycle?.("before-spawn")');
85
- const spawnIndex = source.indexOf("child = spawn(");
86
- const spawnedIndex = source.indexOf('handlers.onProcessLifecycle?.("spawned")', spawnIndex);
87
- const errorIndex = source.indexOf('child.once("error"', spawnIndex);
88
- const lifecycleErrorIndex = source.indexOf('handlers.onProcessLifecycle?.("error")', errorIndex);
89
- const closeIndex = source.indexOf('child.once("close"', spawnIndex);
90
- const exitIndex = source.indexOf('handlers.onProcessLifecycle?.("exit")', closeIndex);
91
- const cancelIndex = source.indexOf("cancel: () =>");
92
- const lifecycleCancelIndex = source.indexOf('handlers.onProcessLifecycle?.("cancel")', cancelIndex);
93
-
94
- assert.ok(beforeSpawnIndex >= 0 && beforeSpawnIndex < spawnIndex);
95
- assert.ok(spawnedIndex > spawnIndex);
96
- assert.ok(lifecycleErrorIndex > errorIndex);
97
- assert.ok(exitIndex > closeIndex);
98
- assert.ok(lifecycleCancelIndex > cancelIndex);
99
- });
100
-
101
- test("generic command runner does not expose shell mode", () => {
102
- const source = readFileSync(fileURLToPath(new URL("./CommandRunner.ts", import.meta.url)), "utf8");
103
- assert.equal(source.includes("shell?: boolean"), false);
104
- assert.equal(source.includes("spec.shell"), false);
105
- });
@@ -1,50 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import test from "node:test";
6
- import { loadProjectInstructions } from "./projectInstructions.js";
7
-
8
- function withTempWorkspace(run: (workspaceRoot: string) => void) {
9
- const workspaceRoot = mkdtempSync(join(tmpdir(), "codexa-instructions-"));
10
- try {
11
- run(workspaceRoot);
12
- } finally {
13
- rmSync(workspaceRoot, { recursive: true, force: true });
14
- }
15
- }
16
-
17
- test("loads AGENTS.md from the workspace root first", () => {
18
- withTempWorkspace((workspaceRoot) => {
19
- mkdirSync(join(workspaceRoot, ".codex"), { recursive: true });
20
- writeFileSync(join(workspaceRoot, "AGENTS.md"), "root instructions\n", "utf8");
21
- writeFileSync(join(workspaceRoot, ".codex", "AGENTS.md"), "nested instructions\n", "utf8");
22
-
23
- const result = loadProjectInstructions(workspaceRoot);
24
-
25
- assert.equal(result.status, "loaded");
26
- if (result.status !== "loaded") return;
27
- assert.equal(result.instructions.content, "root instructions");
28
- assert.match(result.instructions.path, /AGENTS\.md$/);
29
- });
30
- });
31
-
32
- test("falls back to .codex/AGENTS.md", () => {
33
- withTempWorkspace((workspaceRoot) => {
34
- mkdirSync(join(workspaceRoot, ".codex"), { recursive: true });
35
- writeFileSync(join(workspaceRoot, ".codex", "AGENTS.md"), "project instructions\n", "utf8");
36
-
37
- const result = loadProjectInstructions(workspaceRoot);
38
-
39
- assert.equal(result.status, "loaded");
40
- if (result.status !== "loaded") return;
41
- assert.equal(result.instructions.content, "project instructions");
42
- assert.match(result.instructions.path, /\.codex[\\/]AGENTS\.md$/);
43
- });
44
- });
45
-
46
- test("treats missing instruction files as non-fatal", () => {
47
- withTempWorkspace((workspaceRoot) => {
48
- assert.deepEqual(loadProjectInstructions(workspaceRoot), { status: "missing" });
49
- });
50
- });