| @@ -1,5 +1,6 @@ |
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | import { Effect, Predicate } from "effect"; |
| 3 | +import type * as Tracer from "effect/Tracer"; |
| 3 | 4 | |
| 4 | 5 | import { |
| 5 | 6 | AuthTemplateSlug, |
| @@ -29,6 +30,37 @@ const INTEG = IntegrationSlug.make("acme"); · 23 unmodified lines |
| 29 | 30 | const TEMPLATE = AuthTemplateSlug.make("oauth"); |
| 30 | 31 | const FIRST_PARTY = firstPartyOAuthClientSlug("acme"); |
| 31 | 32 | |
| 33 | +const makeRecordingTracer = (spans: Map<string, Map<string, unknown>>): Tracer.Tracer => ({ |
| 34 | + span: (options) => { |
| 35 | + const attributes = new Map<string, unknown>(); |
| 36 | + spans.set(options.name, attributes); |
| 37 | + let status: Tracer.SpanStatus = { _tag: "Started", startTime: options.startTime }; |
| 38 | + return { |
| 39 | + _tag: "Span", |
| 40 | + name: options.name, |
| 41 | + spanId: "0000000000000001", |
| 42 | + traceId: "00000000000000000000000000000001", |
| 43 | + parent: options.parent, |
| 44 | + annotations: options.annotations, |
| 45 | + get status() { |
| 46 | + return status; |
| 47 | + }, |
| 48 | + attributes, |
| 49 | + links: options.links, |
| 50 | + sampled: options.sampled, |
| 51 | + kind: options.kind, |
| 52 | + end: (endTime, exit) => { |
| 53 | + status = { _tag: "Ended", startTime: options.startTime, endTime, exit }; |
| 54 | + }, |
| 55 | + attribute: (key, value) => { |
| 56 | + attributes.set(key, value); |
| 57 | + }, |
| 58 | + event: () => undefined, |
| 59 | + addLinks: () => undefined, |
| 60 | + }; |
| 61 | + }, |
| 62 | +}); |
| 63 | + |
| 32 | 64 | const oauthPlugin = definePlugin(() => ({ |
| 33 | 65 | id: "acme" as const, |
| 34 | 66 | storage: () => ({}), |
| @@ -81,8 +113,9 @@ const firstPartyClientFor = (server: { · 46 unmodified lines |
| 81 | 113 | describe("first-party oauth clients", () => { |
| 82 | 114 | it.effect( |
| 83 | 115 | "start → complete through a config-declared client mints an executable connection", |
| 84 | | − () => |
| 85 | | − Effect.scoped( |
| 116 | + () => { |
| 117 | + const spans = new Map<string, Map<string, unknown>>(); |
| 118 | + return Effect.scoped( |
| 86 | 119 | Effect.gen(function* () { |
| 87 | 120 | const server = yield* serveOAuthTestServer({ scopes: ["read"] }); |
| 88 | 121 | const { executor } = yield* makeTestWorkspaceHarness({ |
| @@ -101,6 +134,9 @@ describe("first-party oauth clients", () => { · 12 unmodified lines |
| 101 | 134 | template: TEMPLATE, |
| 102 | 135 | }); |
| 103 | 136 | expect(started.status).toBe("redirect"); |
| 137 | + expect( |
| 138 | + spans.get("test.oauth.first_party")?.get("executor.oauth.client_first_party"), |
| 139 | + ).toBe(true); |
| 104 | 140 | if (started.status !== "redirect") return; |
| 105 | 141 | |
| 106 | 142 | const callback = yield* server.completeAuthorizationCodeFlow({ |
| @@ -111,6 +147,9 @@ describe("first-party oauth clients", () => { · 4 unmodified lines |
| 111 | 147 | code: callback.code, |
| 112 | 148 | }); |
| 113 | 149 | expect(String(connection.address)).toBe("tools.acme.org.mainAccount"); |
| 150 | + expect( |
| 151 | + spans.get("executor.oauth.complete")?.get("executor.oauth.client_first_party"), |
| 152 | + ).toBe(true); |
| 114 | 153 | |
| 115 | 154 | const out = (yield* executor.execute( |
| 116 | 155 | ToolAddress.make("tools.acme.org.mainAccount.whoami"), |
| @@ -119,7 +158,11 @@ describe("first-party oauth clients", () => { · 2 unmodified lines |
| 119 | 158 | expect(out.token).toMatch(/^at_/); |
| 120 | 159 | expect(yield* server.acceptsAccessToken(out.token)).toBe(true); |
| 121 | 160 | }), |
| 122 | | − ), |
| 161 | + ).pipe( |
| 162 | + Effect.withSpan("test.oauth.first_party"), |
| 163 | + Effect.withTracer(makeRecordingTracer(spans)), |
| 164 | + ); |
| 165 | + }, |
| 123 | 166 | ); |
| 124 | 167 | |
| 125 | 168 | it.effect("refresh resolves the config-declared client (no oauth_client row exists)", () => |