Преглед изворни кода

fix(account): handle pending console login polling (#18281)

Kit Langton пре 1 месец
родитељ
комит
6e09a1d904
2 измењених фајлова са 82 додато и 11 уклоњено
  1. 7 1
      packages/opencode/src/account/effect.ts
  2. 75 10
      packages/opencode/test/account/service.test.ts

+ 7 - 1
packages/opencode/src/account/effect.ts

@@ -148,6 +148,12 @@ export namespace AccountEffect {
           mapAccountServiceError("HTTP request failed"),
         )
 
+      const executeEffect = <E>(request: Effect.Effect<HttpClientRequest.HttpClientRequest, E>) =>
+        request.pipe(
+          Effect.flatMap((req) => http.execute(req)),
+          mapAccountServiceError("HTTP request failed"),
+        )
+
       const resolveToken = Effect.fnUntraced(function* (row: AccountRow) {
         const now = yield* Clock.currentTimeMillis
         if (row.token_expiry && row.token_expiry > now) return row.access_token
@@ -290,7 +296,7 @@ export namespace AccountEffect {
       })
 
       const poll = Effect.fn("Account.poll")(function* (input: Login) {
-        const response = yield* executeEffectOk(
+        const response = yield* executeEffect(
           HttpClientRequest.post(`${input.server}/auth/device/token`).pipe(
             HttpClientRequest.acceptJson,
             HttpClientRequest.schemaBodyJson(DeviceTokenRequest)(

+ 75 - 10
packages/opencode/test/account/service.test.ts

@@ -34,6 +34,24 @@ const encodeOrg = Schema.encodeSync(Org)
 
 const org = (id: string, name: string) => encodeOrg(new Org({ id: OrgID.make(id), name }))
 
+const login = () =>
+  new Login({
+    code: DeviceCode.make("device-code"),
+    user: UserCode.make("user-code"),
+    url: "https://one.example.com/verify",
+    server: "https://one.example.com",
+    expiry: Duration.seconds(600),
+    interval: Duration.seconds(5),
+  })
+
+const deviceTokenClient = (body: unknown, status = 400) =>
+  HttpClient.make((req) =>
+    Effect.succeed(req.url === "https://one.example.com/auth/device/token" ? json(req, body, status) : json(req, {}, 404)),
+  )
+
+const poll = (body: unknown, status = 400) =>
+  AccountEffect.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(deviceTokenClient(body, status))))
+
 it.effect("orgsByAccount groups orgs per account", () =>
   Effect.gen(function* () {
     yield* AccountRepo.use((r) =>
@@ -172,15 +190,6 @@ it.effect("config sends the selected org header", () =>
 
 it.effect("poll stores the account and first org on success", () =>
   Effect.gen(function* () {
-    const login = new Login({
-      code: DeviceCode.make("device-code"),
-      user: UserCode.make("user-code"),
-      url: "https://one.example.com/verify",
-      server: "https://one.example.com",
-      expiry: Duration.seconds(600),
-      interval: Duration.seconds(5),
-    })
-
     const client = HttpClient.make((req) =>
       Effect.succeed(
         req.url === "https://one.example.com/auth/device/token"
@@ -198,7 +207,7 @@ it.effect("poll stores the account and first org on success", () =>
       ),
     )
 
-    const res = yield* AccountEffect.Service.use((s) => s.poll(login)).pipe(Effect.provide(live(client)))
+    const res = yield* AccountEffect.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(client)))
 
     expect(res._tag).toBe("PollSuccess")
     if (res._tag === "PollSuccess") {
@@ -215,3 +224,59 @@ it.effect("poll stores the account and first org on success", () =>
     )
   }),
 )
+
+for (const [name, body, expectedTag] of [
+  [
+    "pending",
+    {
+      error: "authorization_pending",
+      error_description: "The authorization request is still pending",
+    },
+    "PollPending",
+  ],
+  [
+    "slow",
+    {
+      error: "slow_down",
+      error_description: "Polling too frequently, please slow down",
+    },
+    "PollSlow",
+  ],
+  [
+    "denied",
+    {
+      error: "access_denied",
+      error_description: "The authorization request was denied",
+    },
+    "PollDenied",
+  ],
+  [
+    "expired",
+    {
+      error: "expired_token",
+      error_description: "The device code has expired",
+    },
+    "PollExpired",
+  ],
+] as const) {
+  it.effect(`poll returns ${name} for ${body.error}`, () =>
+    Effect.gen(function* () {
+      const result = yield* poll(body)
+      expect(result._tag).toBe(expectedTag)
+    }),
+  )
+}
+
+it.effect("poll returns poll error for other OAuth errors", () =>
+  Effect.gen(function* () {
+    const result = yield* poll({
+      error: "server_error",
+      error_description: "An unexpected error occurred",
+    })
+
+    expect(result._tag).toBe("PollError")
+    if (result._tag === "PollError") {
+      expect(String(result.cause)).toContain("server_error")
+    }
+  }),
+)