Skip to content

Commit a5345a8

Browse files
committed
fix(web): refresh projects after github oauth
1 parent 3811e90 commit a5345a8

4 files changed

Lines changed: 169 additions & 4 deletions

File tree

packages/api/src/services/auth-github-login-stream.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { resolvePathFromCwd } from "@effect-template/lib/usecases/path-helpers"
1212
import { autoSyncState } from "@effect-template/lib/usecases/state-repo"
1313
import { ensureStateDotDockerGitRepo } from "@effect-template/lib/usecases/state-repo-github"
1414
import { migrateLegacyOrchLayout } from "@effect-template/lib/usecases/auth-sync"
15-
import { Effect, Runtime } from "effect"
15+
import { Effect, Logger, Runtime } from "effect"
1616
import * as Stream from "effect/Stream"
1717
import { spawn, type ChildProcess } from "node:child_process"
1818

@@ -188,6 +188,20 @@ const finalizeMessage = (status: string): string =>
188188
? `\nGitHub login completed.\n${githubLoginStreamSuccessMarker}\n`
189189
: `\n${githubLoginStreamErrorMarkerPrefix}${status}\n`
190190

191+
const normalizeCapturedLogLines = (lines: ReadonlyArray<string>): ReadonlyArray<string> =>
192+
lines
193+
.map((line) => line.trim())
194+
.filter((line) => line.length > 0)
195+
196+
export const renderGithubPostLoginOutput = (
197+
lines: ReadonlyArray<string>,
198+
status: string
199+
): string => {
200+
const output = normalizeCapturedLogLines(lines).join("\n")
201+
const logBlock = output.length === 0 ? "" : `\n${output}\n`
202+
return `${logBlock}${finalizeMessage(status)}`
203+
}
204+
191205
const toStreamError = (error: unknown): ApiInternalError | ApiBadRequestError =>
192206
error instanceof ApiBadRequestError || error instanceof ApiInternalError
193207
? error
@@ -252,17 +266,25 @@ export const streamGithubAuthLogin = (
252266
return
253267
}
254268

269+
const postLoginLogs: Array<string> = []
270+
const logger = Logger.make(({ message }) => {
271+
postLoginLogs.push(String(message))
272+
})
273+
255274
void runPromiseExit(
256275
finalizeGithubLogin(prepared).pipe(
276+
Effect.provide(Logger.replace(Logger.defaultLogger, logger)),
257277
Effect.matchEffect({
258278
onFailure: (error) =>
259279
Effect.sync(() => {
260-
enqueue(`\nGitHub login finished in browser, but post-login sync failed: ${error.message}\n`)
261-
enqueue(finalizeMessage("post-login"))
280+
enqueue(renderGithubPostLoginOutput([
281+
...postLoginLogs,
282+
`GitHub login finished in browser, but post-login sync failed: ${error.message}`
283+
], "post-login"))
262284
}),
263285
onSuccess: () =>
264286
Effect.sync(() => {
265-
enqueue(finalizeMessage("ok"))
287+
enqueue(renderGithubPostLoginOutput(postLoginLogs, "ok"))
266288
})
267289
})
268290
)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from "vitest"
2+
3+
import { renderGithubPostLoginOutput } from "../src/services/auth-github-login-stream.js"
4+
5+
describe("GitHub auth login stream", () => {
6+
it("renders post-login state logs before the success marker", () => {
7+
const output = renderGithubPostLoginOutput([
8+
"Initializing state repository: https://github.com/octocat/.docker-git.git",
9+
"State dir ready: /home/dev/.docker-git"
10+
], "ok")
11+
12+
expect(output).toContain("Initializing state repository")
13+
expect(output).toContain("State dir ready")
14+
expect(output).toContain("GitHub login completed.")
15+
expect(output).toContain("__DOCKER_GIT_GITHUB_LOGIN_STATUS__:ok")
16+
expect(output.indexOf("State dir ready")).toBeLessThan(output.indexOf("GitHub login completed."))
17+
})
18+
19+
it("renders post-login failure details before the failure marker", () => {
20+
const output = renderGithubPostLoginOutput([
21+
"GitHub login finished in browser, but post-login sync failed: git fetch failed"
22+
], "post-login")
23+
24+
expect(output).toContain("post-login sync failed")
25+
expect(output).toContain("__DOCKER_GIT_GITHUB_LOGIN_STATUS__:error:post-login")
26+
expect(output.indexOf("post-login sync failed")).toBeLessThan(output.indexOf("__DOCKER_GIT_GITHUB_LOGIN_STATUS__"))
27+
})
28+
})

packages/app/src/web/actions-github-oauth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const runGithubOauthMutation = (
5050
message: `Saved GitHub token (${label}).`,
5151
snapshot
5252
})
53+
context.reloadDashboard()
5354
}
5455
})
5556
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { describe, expect, it } from "@effect/vitest"
2+
import { Effect } from "effect"
3+
import { vi } from "vitest"
4+
5+
import { githubLoginStreamMarkers } from "../../src/shared/auth-stream-markers.js"
6+
import { runGithubOauthMutation } from "../../src/web/actions-github-oauth.js"
7+
import type { BrowserActionContext } from "../../src/web/actions-shared.js"
8+
import type { AuthSnapshot, GithubAuthStatus } from "../../src/web/api.js"
9+
10+
const loginGithubStreamMock = vi.hoisted(() => vi.fn())
11+
const loadAuthSnapshotMock = vi.hoisted(() => vi.fn())
12+
const loadGithubStatusMock = vi.hoisted(() => vi.fn())
13+
14+
vi.mock("../../src/web/api.js", () => ({
15+
loadAuthSnapshot: loadAuthSnapshotMock,
16+
loadGithubStatus: loadGithubStatusMock,
17+
loginGithubStream: loginGithubStreamMock
18+
}))
19+
20+
const githubStatus: GithubAuthStatus = {
21+
summary: "GitHub tokens (1):",
22+
tokens: [
23+
{
24+
key: "GITHUB_TOKEN",
25+
label: "default",
26+
login: "octocat",
27+
status: "valid"
28+
}
29+
]
30+
}
31+
32+
const authSnapshot: AuthSnapshot = {
33+
claudeAuthEntries: 0,
34+
claudeAuthPath: "/home/dev/.docker-git/.orch/auth/claude",
35+
geminiAuthEntries: 0,
36+
geminiAuthPath: "/home/dev/.docker-git/.orch/auth/gemini",
37+
gitTokenEntries: 0,
38+
gitUserEntries: 0,
39+
githubTokenEntries: 1,
40+
globalEnvPath: "/home/dev/.docker-git/.orch/env/global.env",
41+
totalEntries: 1
42+
}
43+
44+
const makeContext = () => {
45+
let output = ""
46+
const setOutput: BrowserActionContext["setOutput"] = (next) => {
47+
output = typeof next === "function" ? next(output) : next
48+
}
49+
const setMessage: BrowserActionContext["setMessage"] = vi.fn()
50+
const reloadDashboard = vi.fn()
51+
52+
return {
53+
context: {
54+
githubStatus: null,
55+
reloadDashboard,
56+
selectedProjectId: null,
57+
selectedProjectName: null,
58+
setActionPrompt: vi.fn(),
59+
setAuthSnapshot: vi.fn(),
60+
setBusyLabel: vi.fn(),
61+
setGithubStatus: vi.fn(),
62+
setMessage,
63+
setOutput,
64+
setProjectAuthSnapshot: vi.fn(),
65+
setSelectedMenuIndex: vi.fn(),
66+
setSelectedProject: vi.fn(),
67+
setSelectedProjectId: vi.fn(),
68+
setTerminalSession: vi.fn()
69+
} satisfies BrowserActionContext,
70+
output: () => output,
71+
reloadDashboard,
72+
setMessage
73+
}
74+
}
75+
76+
describe("web GitHub OAuth action", () => {
77+
it.effect("refreshes dashboard projects after successful OAuth", () =>
78+
Effect.gen(function*(_) {
79+
loginGithubStreamMock.mockImplementation((_label: string | null, onChunk: (chunk: string) => void) =>
80+
Effect.sync(() => {
81+
onChunk("Copy your one-time code: ABCD-1234\n")
82+
onChunk("State dir ready: /home/dev/.docker-git\n")
83+
onChunk(`${githubLoginStreamMarkers.success}\n`)
84+
return [
85+
"Copy your one-time code: ABCD-1234",
86+
"State dir ready: /home/dev/.docker-git",
87+
githubLoginStreamMarkers.success
88+
].join("\n")
89+
})
90+
)
91+
loadAuthSnapshotMock.mockImplementation(() => Effect.succeed(authSnapshot))
92+
loadGithubStatusMock.mockImplementation(() => Effect.succeed(githubStatus))
93+
94+
const { context, output, reloadDashboard, setMessage } = makeContext()
95+
96+
runGithubOauthMutation({ label: "" }, context)
97+
98+
yield* _(
99+
Effect.tryPromise({
100+
catch: (error) => error,
101+
try: () =>
102+
vi.waitFor(() => {
103+
expect(reloadDashboard).toHaveBeenCalledTimes(1)
104+
})
105+
})
106+
)
107+
108+
expect(output()).toBe("Copy your one-time code: ABCD-1234\nState dir ready: /home/dev/.docker-git\n")
109+
expect(context.setActionPrompt).toHaveBeenCalledWith(null)
110+
expect(context.setAuthSnapshot).toHaveBeenCalledWith(authSnapshot)
111+
expect(context.setGithubStatus).toHaveBeenCalledWith(githubStatus)
112+
expect(setMessage).toHaveBeenLastCalledWith("Saved GitHub token (default).")
113+
}))
114+
})

0 commit comments

Comments
 (0)