Skip to content

Commit b58e243

Browse files
committed
fix(auto): cover api and selection edge cases
1 parent 0d95d08 commit b58e243

File tree

7 files changed

+110
-1
lines changed

7 files changed

+110
-1
lines changed

packages/api/src/api/contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export type CreateProjectRequest = {
4949
readonly gitTokenLabel?: string | undefined
5050
readonly codexTokenLabel?: string | undefined
5151
readonly claudeTokenLabel?: string | undefined
52+
readonly agentAutoMode?: string | undefined
5253
readonly up?: boolean | undefined
5354
readonly openSsh?: boolean | undefined
5455
readonly force?: boolean | undefined

packages/api/src/api/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const CreateProjectRequestSchema = Schema.Struct({
2525
gitTokenLabel: OptionalString,
2626
codexTokenLabel: OptionalString,
2727
claudeTokenLabel: OptionalString,
28+
agentAutoMode: OptionalString,
2829
up: OptionalBoolean,
2930
openSsh: OptionalBoolean,
3031
force: OptionalBoolean,

packages/api/src/services/projects.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export const createProjectFromRequest = (
199199
...(request.gitTokenLabel === undefined ? {} : { gitTokenLabel: request.gitTokenLabel }),
200200
...(request.codexTokenLabel === undefined ? {} : { codexTokenLabel: request.codexTokenLabel }),
201201
...(request.claudeTokenLabel === undefined ? {} : { claudeTokenLabel: request.claudeTokenLabel }),
202+
...(request.agentAutoMode === undefined ? {} : { agentAutoMode: request.agentAutoMode }),
202203
...(request.up === undefined ? {} : { up: request.up }),
203204
...(request.openSsh === undefined ? {} : { openSsh: request.openSsh }),
204205
...(request.force === undefined ? {} : { force: request.force }),

packages/app/src/docker-git/cli/usage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Options:
6565
--up | --no-up Run docker compose up after init (default: --up)
6666
--ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
6767
--mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
68-
--auto[=claude|codex] Auto-execute an agent; without value picks one by available auth
68+
--auto[=claude|codex] Auto-execute an agent; without value picks by auth, random if both are available
6969
--force Overwrite existing files and wipe compose volumes (docker compose down -v)
7070
--force-env Reset project env defaults only (keep workspace volume/data)
7171
-h, --help Show this help

packages/app/tests/docker-git/parser.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ describe("parseArgs", () => {
128128
it.effect("rejects legacy --claude flag", () =>
129129
expectParseErrorTag(["clone", "https://github.com/org/repo.git", "--claude", "--auto"], "InvalidOption"))
130130

131+
it.effect("rejects legacy --codex flag", () =>
132+
expectParseErrorTag(["clone", "https://github.com/org/repo.git", "--codex", "--auto"], "InvalidOption"))
133+
131134
it.effect("rejects invalid --auto value", () =>
132135
expectParseErrorTag(["clone", "https://github.com/org/repo.git", "--auto=foo"], "InvalidOption"))
133136

packages/lib/src/usecases/actions/create-project.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ const runCreateProject = (
212212

213213
const resolvedConfig = yield* _(resolveCreateConfig(command, ctx, resolvedOutDir))
214214
const resolvedAgentMode = yield* _(resolveAutoAgentMode(resolvedConfig))
215+
if ((resolvedConfig.agentAuto ?? false) && resolvedConfig.agentMode === undefined && resolvedAgentMode !== undefined) {
216+
yield* _(Effect.log(`Auto agent selected: ${resolvedAgentMode}`))
217+
}
215218
const finalConfig = resolvedAgentMode === undefined ? resolvedConfig : { ...resolvedConfig, agentMode: resolvedAgentMode }
216219
const { globalConfig, projectConfig } = buildProjectConfigs(path, ctx.baseDir, resolvedOutDir, finalConfig)
217220

packages/lib/tests/usecases/agent-auto-select.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Path from "@effect/platform/Path"
33
import { NodeContext } from "@effect/platform-node"
44
import { describe, expect, it } from "@effect/vitest"
55
import { Effect } from "effect"
6+
import { vi } from "vitest"
67

78
import type { TemplateConfig } from "../../src/core/domain.js"
89
import { resolveAutoAgentMode } from "../../src/usecases/agent-auto-select.js"
@@ -62,6 +63,22 @@ describe("resolveAutoAgentMode", () => {
6263
})
6364
).pipe(Effect.provide(NodeContext.layer)))
6465

66+
it.effect("keeps explicit Claude mode when Claude auth exists", () =>
67+
withTempDir((root) =>
68+
Effect.gen(function*(_) {
69+
const fs = yield* _(FileSystem.FileSystem)
70+
const path = yield* _(Path.Path)
71+
const config = { ...makeConfig(root, path), agentMode: "claude" as const }
72+
const claudeRoot = path.join(root, ".orch/auth/claude/default")
73+
74+
yield* _(fs.makeDirectory(claudeRoot, { recursive: true }))
75+
yield* _(fs.writeFileString(path.join(claudeRoot, ".oauth-token"), "token\n"))
76+
77+
const mode = yield* _(resolveAutoAgentMode(config))
78+
expect(mode).toBe("claude")
79+
})
80+
).pipe(Effect.provide(NodeContext.layer)))
81+
6582
it.effect("chooses Codex when only Codex auth exists", () =>
6683
withTempDir((root) =>
6784
Effect.gen(function*(_) {
@@ -78,6 +95,89 @@ describe("resolveAutoAgentMode", () => {
7895
})
7996
).pipe(Effect.provide(NodeContext.layer)))
8097

98+
it.effect("keeps explicit Codex mode when Codex auth exists", () =>
99+
withTempDir((root) =>
100+
Effect.gen(function*(_) {
101+
const fs = yield* _(FileSystem.FileSystem)
102+
const path = yield* _(Path.Path)
103+
const config = { ...makeConfig(root, path), agentMode: "codex" as const }
104+
const codexRoot = path.join(root, ".orch/auth/codex")
105+
106+
yield* _(fs.makeDirectory(codexRoot, { recursive: true }))
107+
yield* _(fs.writeFileString(path.join(codexRoot, "auth.json"), "{\"ok\":true}\n"))
108+
109+
const mode = yield* _(resolveAutoAgentMode(config))
110+
expect(mode).toBe("codex")
111+
})
112+
).pipe(Effect.provide(NodeContext.layer)))
113+
114+
it.effect("chooses randomly when both Claude and Codex auth exist", () =>
115+
withTempDir((root) =>
116+
Effect.gen(function*(_) {
117+
const fs = yield* _(FileSystem.FileSystem)
118+
const path = yield* _(Path.Path)
119+
const config = makeConfig(root, path)
120+
const claudeRoot = path.join(root, ".orch/auth/claude/default")
121+
const codexRoot = path.join(root, ".orch/auth/codex")
122+
123+
yield* _(fs.makeDirectory(claudeRoot, { recursive: true }))
124+
yield* _(fs.makeDirectory(codexRoot, { recursive: true }))
125+
yield* _(fs.writeFileString(path.join(claudeRoot, ".oauth-token"), "token\n"))
126+
yield* _(fs.writeFileString(path.join(codexRoot, "auth.json"), "{\"ok\":true}\n"))
127+
128+
const previousRandom = Math.random
129+
yield* _(Effect.addFinalizer(() =>
130+
Effect.sync(() => {
131+
Math.random = previousRandom
132+
})
133+
))
134+
135+
yield* _(Effect.sync(() => {
136+
Math.random = vi.fn(() => 0.1)
137+
}))
138+
const claudeMode = yield* _(resolveAutoAgentMode(config))
139+
expect(claudeMode).toBe("claude")
140+
141+
yield* _(Effect.sync(() => {
142+
Math.random = vi.fn(() => 0.9)
143+
}))
144+
const codexMode = yield* _(resolveAutoAgentMode(config))
145+
expect(codexMode).toBe("codex")
146+
})
147+
).pipe(Effect.provide(NodeContext.layer)))
148+
149+
it.effect("fails explicit Claude mode when Claude auth is missing", () =>
150+
withTempDir((root) =>
151+
Effect.gen(function*(_) {
152+
const path = yield* _(Path.Path)
153+
const config = { ...makeConfig(root, path), agentMode: "claude" as const }
154+
155+
const exit = yield* _(
156+
resolveAutoAgentMode(config).pipe(
157+
Effect.flip,
158+
Effect.map((error) => error._tag)
159+
)
160+
)
161+
expect(exit).toBe("InvalidOption")
162+
})
163+
).pipe(Effect.provide(NodeContext.layer)))
164+
165+
it.effect("fails explicit Codex mode when Codex auth is missing", () =>
166+
withTempDir((root) =>
167+
Effect.gen(function*(_) {
168+
const path = yield* _(Path.Path)
169+
const config = { ...makeConfig(root, path), agentMode: "codex" as const }
170+
171+
const exit = yield* _(
172+
resolveAutoAgentMode(config).pipe(
173+
Effect.flip,
174+
Effect.map((error) => error._tag)
175+
)
176+
)
177+
expect(exit).toBe("InvalidOption")
178+
})
179+
).pipe(Effect.provide(NodeContext.layer)))
180+
81181
it.effect("fails when no auth exists", () =>
82182
withTempDir((root) =>
83183
Effect.gen(function*(_) {

0 commit comments

Comments
 (0)