@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,129 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { ChildProcess } from "node:child_process";
4
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
5
- import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- import { runCommand, type CommandResult } from "../process/CommandRunner.js";
8
- import { resolveExecutable, buildSpawnSpec } from "./executableResolver.js";
9
-
10
- function commandResult(overrides: Partial<CommandResult>): CommandResult {
11
- return {
12
- status: "completed",
13
- exitCode: 0,
14
- signal: null,
15
- stdout: "",
16
- stderr: "",
17
- startedAt: 0,
18
- endedAt: 0,
19
- durationMs: 0,
20
- userMessage: "Command completed.",
21
- ...overrides,
22
- };
23
- }
24
-
25
- function mockRunCommand(result: CommandResult, onCall?: (spec: Parameters<typeof runCommand>[0]) => void): typeof runCommand {
26
- return ((spec) => {
27
- onCall?.(spec);
28
- return {
29
- child: null as unknown as ChildProcess,
30
- result: Promise.resolve(result),
31
- cancel: () => undefined,
32
- };
33
- }) as typeof runCommand;
34
- }
35
-
36
- test("resolver: uses configuredPath", async () => {
37
- const resolved = await resolveExecutable({
38
- commandNames: ["test"],
39
- label: "test",
40
- configuredPath: process.execPath,
41
- });
42
- assert.equal(resolved, process.execPath);
43
- });
44
-
45
- test("resolver: uses environment override", async () => {
46
- const original = process.env.TEST_EXECUTABLE;
47
- process.env.TEST_EXECUTABLE = "custom-test";
48
- try {
49
- const resolved = await resolveExecutable({
50
- commandNames: ["test"],
51
- label: "test",
52
- envOverrides: ["TEST_EXECUTABLE"],
53
- });
54
- assert.equal(resolved, "custom-test");
55
- } finally {
56
- if (original === undefined) delete process.env.TEST_EXECUTABLE;
57
- else process.env.TEST_EXECUTABLE = original;
58
- }
59
- });
60
-
61
- test("resolver: rejects environment override with shell metacharacters", async () => {
62
- const original = process.env.TEST_EXECUTABLE;
63
- process.env.TEST_EXECUTABLE = "custom-test & calc";
64
- try {
65
- await assert.rejects(
66
- () => resolveExecutable({
67
- commandNames: ["test"],
68
- label: "test",
69
- envOverrides: ["TEST_EXECUTABLE"],
70
- }),
71
- /shell metacharacters|single executable name/i,
72
- );
73
- } finally {
74
- if (original === undefined) delete process.env.TEST_EXECUTABLE;
75
- else process.env.TEST_EXECUTABLE = original;
76
- }
77
- });
78
-
79
- test("resolver: rejects configured executable values that include arguments", async () => {
80
- await assert.rejects(
81
- () => resolveExecutable({
82
- commandNames: ["test"],
83
- label: "test",
84
- configuredPath: "test --version",
85
- }),
86
- /single executable name/i,
87
- );
88
- });
89
-
90
- test("resolver: accepts quoted executable paths with spaces", async () => {
91
- const tempRoot = join(tmpdir(), `codexa resolver ${Date.now()}`);
92
- const executablePath = join(tempRoot, "tool with spaces.exe");
93
- mkdirSync(tempRoot, { recursive: true });
94
- writeFileSync(executablePath, "");
95
- try {
96
- const resolved = await resolveExecutable({
97
- commandNames: ["test"],
98
- label: "test",
99
- configuredPath: `"${executablePath}"`,
100
- });
101
- assert.equal(resolved, executablePath);
102
- } finally {
103
- rmSync(tempRoot, { recursive: true, force: true });
104
- }
105
- });
106
-
107
- test("resolver: falls back to bare name if not found", async () => {
108
- const mockImpl = mockRunCommand(commandResult({ status: "failed", exitCode: 1 }));
109
- const resolved = await resolveExecutable({
110
- runCommandImpl: mockImpl,
111
- commandNames: ["mytest"],
112
- label: "test",
113
- });
114
- assert.equal(resolved, "mytest");
115
- });
116
-
117
- test("buildSpawnSpec: wraps .cmd files in cmd.exe on Windows", async () => {
118
- if (process.platform !== "win32") return;
119
- const spec = buildSpawnSpec("test.cmd", ["arg1"]);
120
- assert.equal(spec.executable, "cmd.exe");
121
- assert.deepEqual(spec.args, ["/d", "/s", "/c", "call", "test.cmd", "arg1"]);
122
- });
123
-
124
- test("buildSpawnSpec: does not wrap .exe files", async () => {
125
- if (process.platform !== "win32") return;
126
- const spec = buildSpawnSpec("test.exe", ["arg1"]);
127
- assert.equal(spec.executable, "test.exe");
128
- assert.deepEqual(spec.args, ["arg1"]);
129
- });
@@ -1,116 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import type { ChildProcess } from "node:child_process";
4
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
5
- import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- import { buildGeminiSpawnSpec, resetGeminiExecutableCacheForTests, resolveGeminiExecutable } from "./geminiExecutable.js";
8
- import { runCommand, type CommandResult } from "../process/CommandRunner.js";
9
-
10
- function commandResult(overrides: Partial<CommandResult>): CommandResult {
11
- return {
12
- status: "completed",
13
- exitCode: 0,
14
- signal: null,
15
- stdout: "",
16
- stderr: "",
17
- startedAt: 0,
18
- endedAt: 0,
19
- durationMs: 0,
20
- userMessage: "Command completed.",
21
- ...overrides,
22
- };
23
- }
24
-
25
- function mockRunCommand(onCall: (spec: Parameters<typeof runCommand>[0]) => CommandResult): typeof runCommand {
26
- return ((spec) => ({
27
- child: null as unknown as ChildProcess,
28
- result: Promise.resolve(onCall(spec)),
29
- cancel: () => undefined,
30
- })) as typeof runCommand;
31
- }
32
-
33
- async function withEnv<T>(env: Partial<NodeJS.ProcessEnv>, callback: () => Promise<T>): Promise<T> {
34
- const originalExecutable = process.env.GEMINI_EXECUTABLE;
35
- const originalCliPath = process.env.GEMINI_CLI_PATH;
36
- const originalAppData = process.env.APPDATA;
37
- try {
38
- if ("GEMINI_EXECUTABLE" in env) process.env.GEMINI_EXECUTABLE = env.GEMINI_EXECUTABLE;
39
- else delete process.env.GEMINI_EXECUTABLE;
40
- if ("GEMINI_CLI_PATH" in env) process.env.GEMINI_CLI_PATH = env.GEMINI_CLI_PATH;
41
- else delete process.env.GEMINI_CLI_PATH;
42
- if ("APPDATA" in env) process.env.APPDATA = env.APPDATA;
43
- resetGeminiExecutableCacheForTests();
44
- return await callback();
45
- } finally {
46
- if (originalExecutable === undefined) delete process.env.GEMINI_EXECUTABLE;
47
- else process.env.GEMINI_EXECUTABLE = originalExecutable;
48
- if (originalCliPath === undefined) delete process.env.GEMINI_CLI_PATH;
49
- else process.env.GEMINI_CLI_PATH = originalCliPath;
50
- if (originalAppData === undefined) delete process.env.APPDATA;
51
- else process.env.APPDATA = originalAppData;
52
- resetGeminiExecutableCacheForTests();
53
- }
54
- }
55
-
56
- test("Gemini resolver: env GEMINI_EXECUTABLE wins over PATH", async () => {
57
- await withEnv({ GEMINI_EXECUTABLE: "env-gemini.cmd" }, async () => {
58
- let whereCalled = false;
59
- const resolved = await resolveGeminiExecutable({
60
- runCommandImpl: mockRunCommand((spec) => {
61
- if (spec.executable === "where.exe") whereCalled = true;
62
- return commandResult({ stdout: "C:\\Tools\\gemini.cmd\n" });
63
- }),
64
- });
65
-
66
- assert.equal(resolved, "env-gemini.cmd");
67
- assert.equal(whereCalled, false);
68
- });
69
- });
70
-
71
- test("Gemini resolver: APPDATA npm shim fallback works", async () => {
72
- if (process.platform !== "win32") return;
73
- const tempRoot = join(tmpdir(), `codexa-gemini-${Date.now()}`);
74
- const npmDir = join(tempRoot, "npm");
75
- const shim = join(npmDir, "gemini.cmd");
76
- mkdirSync(npmDir, { recursive: true });
77
- writeFileSync(shim, "@echo off\r\n", "utf-8");
78
-
79
- try {
80
- await withEnv({ APPDATA: tempRoot }, async () => {
81
- const resolved = await resolveGeminiExecutable({
82
- runCommandImpl: mockRunCommand(() => commandResult({ status: "failed", exitCode: 1 })),
83
- });
84
- assert.equal(resolved, shim);
85
- });
86
- } finally {
87
- rmSync(tempRoot, { recursive: true, force: true });
88
- }
89
- });
90
-
91
- test("Gemini resolver: PowerShell function text is not accepted as executable path", async () => {
92
- if (process.platform !== "win32") return;
93
- await withEnv({}, async () => {
94
- const tempRoot = join(tmpdir(), `codexa-gemini-real-${Date.now()}`);
95
- const npmDir = join(tempRoot, "npm");
96
- const shim = join(npmDir, "gemini.cmd");
97
- mkdirSync(npmDir, { recursive: true });
98
- writeFileSync(shim, "@echo off\r\n", "utf-8");
99
- process.env.APPDATA = tempRoot;
100
- try {
101
- const resolved = await resolveGeminiExecutable({
102
- runCommandImpl: mockRunCommand(() => commandResult({ stdout: "function gemini { param($p) }\n" })),
103
- });
104
- assert.equal(resolved, shim);
105
- } finally {
106
- rmSync(tempRoot, { recursive: true, force: true });
107
- }
108
- });
109
- });
110
-
111
- test("Gemini spawn spec bypasses PowerShell and targets the resolved executable", () => {
112
- const spec = buildGeminiSpawnSpec("C:\\Users\\Example\\AppData\\Roaming\\npm\\gemini.cmd", ["-p", "Respond with READY only."]);
113
- assert.equal(spec.executable, "C:\\Users\\Example\\AppData\\Roaming\\npm\\gemini.cmd");
114
- assert.deepEqual(spec.args, ["-p", "Respond with READY only."]);
115
- assert.equal(spec.shell, undefined);
116
- });
@@ -1,47 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { readdirSync, readFileSync, statSync } from "node:fs";
4
- import { join } from "node:path";
5
-
6
- function collectTsFiles(dir: string): string[] {
7
- const result: string[] = [];
8
- for (const entry of readdirSync(dir)) {
9
- const full = join(dir, entry);
10
- if (statSync(full).isDirectory()) {
11
- result.push(...collectTsFiles(full));
12
- } else if (full.endsWith(".ts")) {
13
- result.push(full);
14
- }
15
- }
16
- return result;
17
- }
18
-
19
- const SRC_ROOT = join(import.meta.dirname, "../../..");
20
-
21
- // Guard against personal user-specific paths appearing in source.
22
- // Patterns are split across array entries to prevent THIS file from matching itself.
23
- const BANNED_FRAGMENTS: Array<[string, string]> = [
24
- ["C", ":\\\\Users\\\\jorda"],
25
- ["C", ":/Users/jorda"],
26
- ];
27
-
28
- test("no source files contain personal hardcoded user paths", { timeout: 30_000 }, () => {
29
- const patterns = BANNED_FRAGMENTS.map(([a, b]) => new RegExp(a + b));
30
- const files = collectTsFiles(SRC_ROOT);
31
- const violations: string[] = [];
32
-
33
- for (const file of files) {
34
- const content = readFileSync(file, "utf-8");
35
- for (const pattern of patterns) {
36
- if (pattern.test(content)) {
37
- violations.push(`${file}: matches ${pattern}`);
38
- }
39
- }
40
- }
41
-
42
- assert.deepEqual(
43
- violations,
44
- [],
45
- `Personal hardcoded user paths found in source:\n${violations.join("\n")}`,
46
- );
47
- });
@@ -1,92 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- parseRepoIdentity,
5
- classifyDiagnostics,
6
- type DiagnosticResult,
7
- type RepoIdentity
8
- } from "./githubDiagnostics.js";
9
-
10
- test("parseRepoIdentity: handles various GitHub URL formats", () => {
11
- const cases = [
12
- {
13
- url: "https://github.com/owner/repo.git",
14
- expected: { owner: "owner", repo: "repo", provider: "github" }
15
- },
16
- {
17
- url: "git@github.com:owner/repo.git",
18
- expected: { owner: "owner", repo: "repo", provider: "github" }
19
- },
20
- {
21
- url: "https://github.com/owner/repo",
22
- expected: { owner: "owner", repo: "repo", provider: "github" }
23
- },
24
- {
25
- url: "ssh://git@github.com/owner/repo.git",
26
- expected: { owner: "owner", repo: "repo", provider: "github" }
27
- }
28
- ];
29
-
30
- for (const { url, expected } of cases) {
31
- const result = parseRepoIdentity(url);
32
- assert.ok(result);
33
- assert.equal(result.owner, expected.owner);
34
- assert.equal(result.repo, expected.repo);
35
- assert.equal(result.provider, expected.provider);
36
- }
37
- });
38
-
39
- test("parseRepoIdentity: handles missing or non-GitHub origins", () => {
40
- assert.equal(parseRepoIdentity(null), null);
41
- assert.equal(parseRepoIdentity(""), null);
42
-
43
- const other = parseRepoIdentity("https://gitlab.com/owner/repo.git");
44
- assert.ok(other);
45
- assert.equal(other.provider, "other");
46
- });
47
-
48
- test("classifyDiagnostics: handles all status combinations", () => {
49
- const repo: RepoIdentity = { owner: "o", repo: "r", provider: "github", remoteUrl: "..." };
50
-
51
- const pass: DiagnosticResult = { path: "p", status: "PASS", evidence: "e", blocker: null, recommendedUse: false };
52
- const fail: DiagnosticResult = { path: "p", status: "FAIL", evidence: "e", blocker: "b", recommendedUse: false };
53
- const partial: DiagnosticResult = { path: "p", status: "PARTIAL", evidence: "e", blocker: "b", recommendedUse: false };
54
-
55
- // 1. All PASS -> Local Git + GH CLI
56
- assert.equal(
57
- classifyDiagnostics(repo, pass, pass, pass, fail),
58
- "Local Git + GH CLI"
59
- );
60
-
61
- // 2. GH CLI missing but connector OK -> Connector-only
62
- assert.equal(
63
- classifyDiagnostics(repo, fail, pass, pass, pass),
64
- "Local Git + connector PR creation"
65
- );
66
-
67
- // 3. .git write fails but connector OK -> Connector-only
68
- assert.equal(
69
- classifyDiagnostics(repo, pass, pass, fail, pass),
70
- "Connector-only"
71
- );
72
-
73
- // 4. Connector read OK but write unknown (treated as PASS if not auth error)
74
- assert.equal(
75
- classifyDiagnostics(repo, fail, fail, fail, pass),
76
- "Connector-only"
77
- );
78
-
79
- // 5. Connector permission rejected
80
- const connectorRejected: DiagnosticResult = { path: "p", status: "PARTIAL", evidence: "e", blocker: "auth error", recommendedUse: false };
81
- assert.equal(
82
- classifyDiagnostics(repo, fail, fail, fail, connectorRejected),
83
- "Cannot publish yet"
84
- );
85
-
86
- // 6. Non-GitHub repo
87
- const otherRepo: RepoIdentity = { owner: "", repo: "", provider: "other", remoteUrl: "..." };
88
- assert.equal(
89
- classifyDiagnostics(otherRepo, pass, pass, pass, pass),
90
- "Cannot publish yet"
91
- );
92
- });
@@ -1,58 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { formatHollowResponse } from "./hollowResponseFormat.js";
4
- import type { HollowResponseResult } from "./codexPrompt.js";
5
-
6
- function makeResult(kind: HollowResponseResult["kind"], reason = ""): HollowResponseResult {
7
- return { isHollow: true, kind, reason };
8
- }
9
-
10
- test("greeting produces exactly 2 lines", () => {
11
- const out = formatHollowResponse(makeResult("greeting"));
12
- const lines = out.split("\n");
13
- assert.equal(lines.length, 2);
14
- assert.match(lines[0]!, /generic greeting/);
15
- assert.match(lines[1]!, /Retry/);
16
- });
17
-
18
- test("filler produces exactly 2 lines", () => {
19
- const out = formatHollowResponse(makeResult("filler"));
20
- const lines = out.split("\n");
21
- assert.equal(lines.length, 2);
22
- assert.match(lines[0]!, /acknowledged without acting/);
23
- });
24
-
25
- test("clarification produces exactly 2 lines", () => {
26
- const out = formatHollowResponse(makeResult("clarification"));
27
- const lines = out.split("\n");
28
- assert.equal(lines.length, 2);
29
- assert.match(lines[0]!, /clarification/);
30
- assert.match(lines[1]!, /suggest mode/);
31
- });
32
-
33
- test("short-no-action produces exactly 2 lines", () => {
34
- const out = formatHollowResponse(makeResult("short-no-action"));
35
- const lines = out.split("\n");
36
- assert.equal(lines.length, 2);
37
- assert.match(lines[0]!, /too brief/);
38
- });
39
-
40
- test("no emoji or warning symbols in any output", () => {
41
- for (const kind of ["greeting", "filler", "clarification", "short-no-action"] as const) {
42
- const out = formatHollowResponse(makeResult(kind));
43
- assert.doesNotMatch(out, /⚠/);
44
- assert.doesNotMatch(out, /---/);
45
- }
46
- });
47
-
48
- test("verbose=false omits raw response", () => {
49
- const out = formatHollowResponse(makeResult("greeting"), "Hello.");
50
- assert.doesNotMatch(out, /Hello\./);
51
- });
52
-
53
- test("verbose=true appends raw response", () => {
54
- const out = formatHollowResponse(makeResult("greeting"), "Hello.", true);
55
- assert.match(out, /Backend response: Hello\./);
56
- const lines = out.split("\n");
57
- assert.equal(lines.length, 4); // 2 message lines + blank + backend response
58
- });
@@ -1,157 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- import test from "node:test";
6
- import {
7
- buildWorkspaceStatusMessage,
8
- createWorkspaceRelaunchPlan,
9
- guardWorkspaceRelaunch,
10
- resolveLaunchContext,
11
- } from "./launchContext.js";
12
- import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
13
-
14
- function createTempWorkspace(): string {
15
- return mkdtempSync(join(tmpdir(), "codexa-launch-context-"));
16
- }
17
-
18
- test("uses installed launcher metadata when available", () => {
19
- const context = resolveLaunchContext({
20
- workspaceRoot: "C:/target",
21
- packageRoot: "C:/repo",
22
- execPath: "C:/Program Files/nodejs/node.exe",
23
- env: {
24
- CODEXA_LAUNCH_KIND: "installed-bin",
25
- CODEXA_PACKAGE_ROOT: "C:/repo",
26
- CODEXA_LAUNCHER_SCRIPT: "C:/repo/bin/codexa.js",
27
- CODEXA_RELAUNCH_EXECUTABLE: "C:/Program Files/nodejs/node.exe",
28
- CODEXA_RELAUNCH_ARGS: JSON.stringify(["C:/repo/bin/codexa.js", "--profile", "review"]),
29
- },
30
- });
31
-
32
- assert.equal(context.launchKind, "installed-bin");
33
- assert.equal(context.packageRoot, "C:\\repo");
34
- assert.equal(context.launcherScriptPath, "C:/repo/bin/codexa.js");
35
- assert.equal(context.relaunchExecutable, "C:/Program Files/nodejs/node.exe");
36
- assert.deepEqual(context.relaunchArgs, ["C:/repo/bin/codexa.js", "--profile", "review"]);
37
- });
38
-
39
- test("falls back to bun repo launch metadata when no installed launcher env exists", () => {
40
- const context = resolveLaunchContext({
41
- workspaceRoot: "C:/repo",
42
- packageRoot: "C:/repo",
43
- execPath: "C:/tools/bun.exe",
44
- hasBunRuntime: true,
45
- forwardArgs: ["--profile", "review", "--config", "model=\"gpt-5.4\""],
46
- env: {},
47
- });
48
-
49
- assert.equal(context.launchKind, "dev-run");
50
- assert.equal(context.relaunchExecutable, "C:/tools/bun.exe");
51
- assert.deepEqual(context.relaunchArgs, [
52
- "run",
53
- "--silent",
54
- join("C:\\repo", "src", "index.tsx"),
55
- "--",
56
- "--profile",
57
- "review",
58
- "--config",
59
- "model=\"gpt-5.4\"",
60
- ]);
61
- });
62
-
63
- test("creates an installed-bin relaunch plan with normalized target cwd and env", () => {
64
- const root = createTempWorkspace();
65
- try {
66
- const nextWorkspace = join(root, "next");
67
- mkdirSync(nextWorkspace);
68
-
69
- const context = resolveLaunchContext({
70
- workspaceRoot: root,
71
- packageRoot: "C:/repo",
72
- execPath: "C:/Program Files/nodejs/node.exe",
73
- env: {
74
- CODEXA_LAUNCH_KIND: "installed-bin",
75
- CODEXA_PACKAGE_ROOT: "C:/repo",
76
- CODEXA_LAUNCHER_SCRIPT: "C:/repo/bin/codexa.js",
77
- CODEXA_RELAUNCH_EXECUTABLE: "C:/Program Files/nodejs/node.exe",
78
- CODEXA_RELAUNCH_ARGS: JSON.stringify(["C:/repo/bin/codexa.js", "--profile", "review"]),
79
- },
80
- });
81
-
82
- const result = createWorkspaceRelaunchPlan(".\\next", context, {});
83
- assert.equal(result.ok, true);
84
- if (!result.ok) return;
85
-
86
- assert.equal(result.plan.executable, "C:/Program Files/nodejs/node.exe");
87
- assert.deepEqual(result.plan.args, ["C:/repo/bin/codexa.js", "--profile", "review"]);
88
- assert.equal(result.plan.cwd, normalizeWorkspaceRoot(nextWorkspace));
89
- assert.equal(result.plan.env.CODEX_WORKSPACE_ROOT, normalizeWorkspaceRoot(nextWorkspace));
90
- assert.equal(result.plan.env.CODEXA_LAUNCH_KIND, "installed-bin");
91
- } finally {
92
- rmSync(root, { recursive: true, force: true });
93
- }
94
- });
95
-
96
- test("rejects relaunch targets that do not exist", () => {
97
- const root = createTempWorkspace();
98
- try {
99
- const context = resolveLaunchContext({
100
- workspaceRoot: root,
101
- packageRoot: root,
102
- execPath: "C:/tools/bun.exe",
103
- hasBunRuntime: true,
104
- env: {},
105
- });
106
-
107
- const result = createWorkspaceRelaunchPlan(".\\missing", context, {});
108
- assert.equal(result.ok, false);
109
- if (result.ok) return;
110
- assert.match(result.message, /does not exist/i);
111
- } finally {
112
- rmSync(root, { recursive: true, force: true });
113
- }
114
- });
115
-
116
- test("rejects relaunch targets that are files instead of directories", () => {
117
- const root = createTempWorkspace();
118
- try {
119
- const fileTarget = join(root, "notes.txt");
120
- writeFileSync(fileTarget, "hello", "utf8");
121
- const context = resolveLaunchContext({
122
- workspaceRoot: root,
123
- packageRoot: root,
124
- execPath: "C:/tools/bun.exe",
125
- hasBunRuntime: true,
126
- env: {},
127
- });
128
-
129
- const result = createWorkspaceRelaunchPlan(".\\notes.txt", context, {});
130
- assert.equal(result.ok, false);
131
- if (result.ok) return;
132
- assert.match(result.message, /not a directory/i);
133
- } finally {
134
- rmSync(root, { recursive: true, force: true });
135
- }
136
- });
137
-
138
- test("blocks workspace relaunch while busy", () => {
139
- assert.deepEqual(guardWorkspaceRelaunch(false), { allowed: true });
140
- assert.equal(guardWorkspaceRelaunch(true).allowed, false);
141
- assert.match(guardWorkspaceRelaunch(true).message ?? "", /finish the current run/i);
142
- });
143
-
144
- test("describes dev launch mode with install guidance", () => {
145
- const message = buildWorkspaceStatusMessage(resolveLaunchContext({
146
- workspaceRoot: "C:/repo",
147
- packageRoot: "C:/repo",
148
- execPath: "C:/tools/bun.exe",
149
- hasBunRuntime: true,
150
- env: {},
151
- }));
152
-
153
- assert.match(message, /Launch mode: dev\/repo launch/i);
154
- assert.match(message, /npm link/i);
155
- assert.match(message, /where codexa/i);
156
- assert.match(message, /\/workspace relaunch <path>/i);
157
- });
@@ -1,45 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { parseCodexCliCapabilities } from "./codexCapabilities.js";
4
-
5
- test("detects exec help flags and strips ANSI noise", () => {
6
- const capabilities = parseCodexCliCapabilities(
7
- "\u001B[32mOptions:\u001B[0m\n --ask-for-approval <policy>\n --sandbox <mode>\n -c, --config <key=value>\n",
8
- "Global options:\n --ask-for-approval <policy>\n",
9
- );
10
-
11
- assert.deepEqual(capabilities, {
12
- askForApproval: true,
13
- sandbox: true,
14
- config: true,
15
- fullAuto: false,
16
- });
17
- });
18
-
19
- test("does not promote top-level approval flags into exec capabilities", () => {
20
- const capabilities = parseCodexCliCapabilities(
21
- "Usage: codex exec [OPTIONS]\n --sandbox <mode>\n --full-auto\n",
22
- "Global options:\n --ask-for-approval <policy>\n --sandbox <mode>\n --full-auto\n",
23
- );
24
-
25
- assert.deepEqual(capabilities, {
26
- askForApproval: false,
27
- sandbox: true,
28
- config: false,
29
- fullAuto: true,
30
- });
31
- });
32
-
33
- test("requires exact option tokens instead of prose guesses", () => {
34
- const capabilities = parseCodexCliCapabilities(
35
- "This build documents ask-for-approval compatibility in prose only.\nIt also mentions sandbox_mode in config docs.\n",
36
- "Use full auto mode for trusted environments.\n",
37
- );
38
-
39
- assert.deepEqual(capabilities, {
40
- askForApproval: false,
41
- sandbox: false,
42
- config: false,
43
- fullAuto: false,
44
- });
45
- });