From e2eeefef89a45e47a628ab59465dcf2df2c0c681 Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Tue, 3 Jun 2025 16:48:24 +0700 Subject: [PATCH] feat: refactor and WIP add e2e tests --- apps/api/src/auth.ts | 7 + apps/e2e/.env.example | 2 +- apps/e2e/bin/deploy-fixtures.ts | 51 ++++ apps/e2e/package.json | 4 + apps/e2e/src/client.ts | 8 + apps/e2e/src/e2e.test.ts | 26 +- apps/e2e/src/env.ts | 21 ++ apps/e2e/src/index.ts | 2 + apps/e2e/tsconfig.json | 2 +- package.json | 2 +- packages/api-client/src/agentic-api-client.ts | 26 +- packages/cli/src/cli.ts | 9 +- packages/cli/src/lib/auth.ts | 2 +- packages/cli/src/types.ts | 8 +- .../valid/basic-openapi/jsonplaceholder.json | 1 - .../get-tools-from-openapi-spec.test.ts.snap | 233 +++++++++++++++--- .../src/get-tools-from-openapi-spec.ts | 2 +- pnpm-lock.yaml | 10 + 18 files changed, 340 insertions(+), 76 deletions(-) create mode 100644 apps/e2e/bin/deploy-fixtures.ts create mode 100644 apps/e2e/src/client.ts create mode 100644 apps/e2e/src/env.ts create mode 100644 apps/e2e/src/index.ts diff --git a/apps/api/src/auth.ts b/apps/api/src/auth.ts index d6ddc528..35989102 100644 --- a/apps/api/src/auth.ts +++ b/apps/api/src/auth.ts @@ -16,6 +16,13 @@ import { getGitHubClient } from '@/lib/external/github' export const authRouter = issuer({ subjects, storage: DrizzleAuthStorage(), + ttl: { + access: 60 * 60 * 24 * 30, // 30 days + refresh: 60 * 60 * 24 * 365 // 1 year + // Used for creating longer-lived testing tokens + // access: 60 * 60 * 24 * 366, // 1 year + // refresh: 60 * 60 * 24 * 365 * 5 // 5 years + }, providers: { github: GithubProvider({ clientID: env.GITHUB_CLIENT_ID, diff --git a/apps/e2e/.env.example b/apps/e2e/.env.example index 6ff2bb6c..4d5fe1c2 100644 --- a/apps/e2e/.env.example +++ b/apps/e2e/.env.example @@ -5,4 +5,4 @@ # a local .env file in order to run this project. # ------------------------------------------------------------------------------ -# TODO +AGENTIC_API_REFRESH_TOKEN= diff --git a/apps/e2e/bin/deploy-fixtures.ts b/apps/e2e/bin/deploy-fixtures.ts new file mode 100644 index 00000000..de1a5d1c --- /dev/null +++ b/apps/e2e/bin/deploy-fixtures.ts @@ -0,0 +1,51 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { loadAgenticConfig } from '@agentic/platform' +import pMap from 'p-map' + +import { client } from '../src' + +const fixtures = [ + // 'basic-raw-free-ts', + // 'basic-raw-free-json', + // 'pricing-freemium', + // 'pricing-pay-as-you-go', + // 'pricing-3-plans', + // 'pricing-monthly-annual', + // 'pricing-custom-0', + 'basic-openapi' +] + +const fixturesDir = path.join( + fileURLToPath(import.meta.url), + '..', + '..', + '..', + '..', + 'packages', + 'fixtures' +) +const validFixturesDir = path.join(fixturesDir, 'valid') + +async function main() { + const deployments = await pMap( + fixtures, + async (fixture) => { + const fixtureDir = path.join(validFixturesDir, fixture) + + const config = await loadAgenticConfig({ cwd: fixtureDir }) + console.log(config) + + const deployment = await client.createDeployment(config) + return deployment + }, + { + concurrency: 1 + } + ) + + console.log(JSON.stringify(deployments, null, 2)) +} + +await main() diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 5cb33918..8dcc05a4 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -20,6 +20,10 @@ "devDependencies": { "@agentic/platform": "workspace:*", "@agentic/platform-api-client": "workspace:*", + "@agentic/platform-core": "workspace:*", "@agentic/platform-fixtures": "workspace:*" + }, + "dependencies": { + "p-map": "catalog:" } } diff --git a/apps/e2e/src/client.ts b/apps/e2e/src/client.ts new file mode 100644 index 00000000..67643d69 --- /dev/null +++ b/apps/e2e/src/client.ts @@ -0,0 +1,8 @@ +import { AgenticApiClient } from '@agentic/platform-api-client' + +import { env } from './env' + +export const client = new AgenticApiClient({ + apiBaseUrl: env.AGENTIC_API_BASE_URL, + apiKey: env.AGENTIC_API_ACCESS_TOKEN +}) diff --git a/apps/e2e/src/e2e.test.ts b/apps/e2e/src/e2e.test.ts index 53d6afb8..49d44b55 100644 --- a/apps/e2e/src/e2e.test.ts +++ b/apps/e2e/src/e2e.test.ts @@ -1,20 +1,19 @@ -import 'dotenv/config' - import path from 'node:path' import { fileURLToPath } from 'node:url' import { loadAgenticConfig } from '@agentic/platform' -import { AgenticApiClient } from '@agentic/platform-api-client' import { describe, expect, test } from 'vitest' +import { client } from './client' + const fixtures = [ - 'basic-raw-free-ts', - 'basic-raw-free-json', - 'pricing-freemium', - 'pricing-pay-as-you-go', - 'pricing-3-plans', - 'pricing-monthly-annual', - 'pricing-custom-0', + // 'basic-raw-free-ts', + // 'basic-raw-free-json', + // 'pricing-freemium', + // 'pricing-pay-as-you-go', + // 'pricing-3-plans', + // 'pricing-monthly-annual', + // 'pricing-custom-0', 'basic-openapi' ] @@ -29,11 +28,6 @@ const fixturesDir = path.join( ) const validFixturesDir = path.join(fixturesDir, 'valid') -const client = new AgenticApiClient({ - apiBaseUrl: process.env.AGENTIC_API_BASE_URL -}) -await client.setRefreshAuthToken(process.env.AGENTIC_API_REFRESH_TOKEN!) - for (const fixture of fixtures) { test( `${fixture}`, @@ -44,7 +38,7 @@ for (const fixture of fixtures) { const fixtureDir = path.join(validFixturesDir, fixture) const config = await loadAgenticConfig({ cwd: fixtureDir }) - expect(config).toMatchSnapshot() + const deployment = await client.createDeployment(config) } ) } diff --git a/apps/e2e/src/env.ts b/apps/e2e/src/env.ts new file mode 100644 index 00000000..dbac197a --- /dev/null +++ b/apps/e2e/src/env.ts @@ -0,0 +1,21 @@ +import 'dotenv/config' + +import { parseZodSchema } from '@agentic/platform-core' +import { z } from 'zod' + +export const envSchema = z.object({ + NODE_ENV: z + .enum(['development', 'test', 'production']) + .default('development'), + + AGENTIC_API_BASE_URL: z.string().url().optional(), + AGENTIC_API_ACCESS_TOKEN: z.string().nonempty() +}) + +// eslint-disable-next-line no-process-env +export const env = parseZodSchema(envSchema, process.env, { + error: 'Invalid environment variables' +}) + +export const isDev = env.NODE_ENV === 'development' +export const isProd = env.NODE_ENV === 'production' diff --git a/apps/e2e/src/index.ts b/apps/e2e/src/index.ts new file mode 100644 index 00000000..a3de2422 --- /dev/null +++ b/apps/e2e/src/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export * from './env' diff --git a/apps/e2e/tsconfig.json b/apps/e2e/tsconfig.json index 6631568d..1c31e32f 100644 --- a/apps/e2e/tsconfig.json +++ b/apps/e2e/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "@fisch0920/config/tsconfig-node", - "include": ["src", "*.config.ts"], + "include": ["src", "bin", "*.config.ts"], "exclude": ["node_modules", "dist"] } diff --git a/package.json b/package.json index 0e3fd0be..c87576a8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "type": "git", "url": "git+https://github.com/transitive-bullshit/agentic-platform.git" }, - "packageManager": "pnpm@10.11.0", + "packageManager": "pnpm@10.11.1", "engines": { "node": ">=22" }, diff --git a/packages/api-client/src/agentic-api-client.ts b/packages/api-client/src/agentic-api-client.ts index 4d2b8208..862959c7 100644 --- a/packages/api-client/src/agentic-api-client.ts +++ b/packages/api-client/src/agentic-api-client.ts @@ -68,13 +68,15 @@ export class AgenticApiClient { hooks: { beforeRequest: [ async (request) => { - // Always verify freshness of auth tokens before making a request - await this.verifyAuthAndRefreshIfNecessary() - assert(this._authTokens, 'Not authenticated') - request.headers.set( - 'Authorization', - `Bearer ${this._authTokens.access}` - ) + if (!this.apiKey && this._authTokens) { + // Always verify freshness of auth tokens before making a request + await this.verifyAuthAndRefreshIfNecessary() + assert(this._authTokens, 'Not authenticated') + request.headers.set( + 'Authorization', + `Bearer ${this._authTokens.access}` + ) + } } ] } @@ -89,15 +91,11 @@ export class AgenticApiClient { return this._authTokens } - async setRefreshAuthToken(refreshToken: string): Promise { + async setAuth(tokens: AuthTokens): Promise { this._ensureNoApiKey() - const result = await this._authClient.refresh(refreshToken) - if (result.err) { - throw result.err - } - - this._authTokens = result.tokens + this._authTokens = tokens + return this.verifyAuthAndRefreshIfNecessary() } async verifyAuthAndRefreshIfNecessary(): Promise { diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 78a1d5ac..e73cc5be 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -22,10 +22,7 @@ async function main() { apiBaseUrl: process.env.AGENTIC_API_BASE_URL, onUpdateAuth: (update) => { if (update) { - AuthStore.setAuth({ - refreshToken: update.session.refresh, - user: update.user - }) + AuthStore.setAuth(update) } else { AuthStore.clearAuth() } @@ -36,7 +33,7 @@ async function main() { const authSession = AuthStore.tryGetAuth() if (authSession) { try { - await client.setRefreshAuthToken(authSession.refreshToken) + await client.setAuth(authSession.session) } catch { console.warn('Existing auth session is invalid; logging out.\n') AuthStore.clearAuth() @@ -45,7 +42,7 @@ async function main() { // Initialize the CLI program const program = new Command('agentic') - .option('-j, --json', 'Print output in JSON format') + .option('-j, --json', 'Print output as JSON') .showHelpAfterError() const logger = { diff --git a/packages/cli/src/lib/auth.ts b/packages/cli/src/lib/auth.ts index dc8d3156..7d4470dd 100644 --- a/packages/cli/src/lib/auth.ts +++ b/packages/cli/src/lib/auth.ts @@ -59,7 +59,7 @@ export async function auth({ // AuthStore should be updated via the onUpdateAuth callback const session = AuthStore.tryGetAuth() - assert(session && session.refreshToken === client.authTokens.refresh) + assert(session && session.session.access === client.authTokens.access) _resolveAuth(session) return c.text( diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 36429af4..c4551846 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,4 +1,8 @@ -import type { AgenticApiClient, AuthUser } from '@agentic/platform-api-client' +import type { + AgenticApiClient, + AuthTokens, + AuthUser +} from '@agentic/platform-api-client' import type { Command } from 'commander' export type Context = { @@ -12,6 +16,6 @@ export type Context = { } export type AuthSession = { - refreshToken: string + session: AuthTokens user: AuthUser } diff --git a/packages/fixtures/valid/basic-openapi/jsonplaceholder.json b/packages/fixtures/valid/basic-openapi/jsonplaceholder.json index 9d13bd8c..67925935 100644 --- a/packages/fixtures/valid/basic-openapi/jsonplaceholder.json +++ b/packages/fixtures/valid/basic-openapi/jsonplaceholder.json @@ -72,7 +72,6 @@ { "required": true, "schema": { - "title": "postId", "type": "string" }, "name": "postId", diff --git a/packages/openapi-utils/src/__snapshots__/get-tools-from-openapi-spec.test.ts.snap b/packages/openapi-utils/src/__snapshots__/get-tools-from-openapi-spec.test.ts.snap index 032b853b..d7fdcbbc 100644 --- a/packages/openapi-utils/src/__snapshots__/get-tools-from-openapi-spec.test.ts.snap +++ b/packages/openapi-utils/src/__snapshots__/get-tools-from-openapi-spec.test.ts.snap @@ -17,7 +17,12 @@ exports[`getToolsFromOpenAPISpec > basic.json 1`] = ` { "description": "Echo", "inputSchema": { - "properties": {}, + "properties": { + "name": { + "title": "name", + "type": "string", + }, + }, "required": [], "type": "object", }, @@ -531,8 +536,15 @@ exports[`getToolsFromOpenAPISpec > firecrawl.json 1`] = ` { "description": "Get the status of a crawl job", "inputSchema": { - "properties": {}, - "required": [], + "properties": { + "jobId": { + "description": "ID of the crawl job", + "type": "string", + }, + }, + "required": [ + "jobId", + ], "type": "object", }, "name": "get_crawl_status", @@ -695,8 +707,15 @@ exports[`getToolsFromOpenAPISpec > firecrawl.json 1`] = ` { "description": "Cancel a crawl job", "inputSchema": { - "properties": {}, - "required": [], + "properties": { + "jobId": { + "description": "ID of the crawl job", + "type": "string", + }, + }, + "required": [ + "jobId", + ], "type": "object", }, "name": "cancel_crawl_job", @@ -738,6 +757,14 @@ exports[`getToolsFromOpenAPISpec > mixed.json 1`] = ` "title": "id", "type": "string", }, + "name": { + "title": "name", + "type": "string", + }, + "x-custom-header": { + "title": "x-custom-header", + "type": "string", + }, }, "required": [ "id", @@ -779,8 +806,133 @@ exports[`getToolsFromOpenAPISpec > open-meteo.yaml 1`] = ` { "description": "7 day weather variables in hourly and daily resolution for given WGS84 latitude and longitude coordinates. Available worldwide.", "inputSchema": { - "properties": {}, - "required": [], + "properties": { + "current_weather": { + "type": "boolean", + }, + "daily": { + "items": { + "enum": [ + "temperature_2m_max", + "temperature_2m_min", + "apparent_temperature_max", + "apparent_temperature_min", + "precipitation_sum", + "precipitation_hours", + "weather_code", + "sunrise", + "sunset", + "wind_speed_10m_max", + "wind_gusts_10m_max", + "wind_direction_10m_dominant", + "shortwave_radiation_sum", + "uv_index_max", + "uv_index_clear_sky_max", + "et0_fao_evapotranspiration", + ], + "type": "string", + }, + "type": "array", + }, + "hourly": { + "items": { + "enum": [ + "temperature_2m", + "relative_humidity_2m", + "dew_point_2m", + "apparent_temperature", + "pressure_msl", + "cloud_cover", + "cloud_cover_low", + "cloud_cover_mid", + "cloud_cover_high", + "wind_speed_10m", + "wind_speed_80m", + "wind_speed_120m", + "wind_speed_180m", + "wind_direction_10m", + "wind_direction_80m", + "wind_direction_120m", + "wind_direction_180m", + "wind_gusts_10m", + "shortwave_radiation", + "direct_radiation", + "direct_normal_irradiance", + "diffuse_radiation", + "vapour_pressure_deficit", + "evapotranspiration", + "precipitation", + "weather_code", + "snow_height", + "freezing_level_height", + "soil_temperature_0cm", + "soil_temperature_6cm", + "soil_temperature_18cm", + "soil_temperature_54cm", + "soil_moisture_0_1cm", + "soil_moisture_1_3cm", + "soil_moisture_3_9cm", + "soil_moisture_9_27cm", + "soil_moisture_27_81cm", + ], + "type": "string", + }, + "type": "array", + }, + "latitude": { + "description": "WGS84 coordinate", + "format": "double", + "type": "number", + }, + "longitude": { + "description": "WGS84 coordinate", + "format": "double", + "type": "number", + }, + "past_days": { + "description": "If \`past_days\` is set, yesterdays or the day before yesterdays data are also returned.", + "enum": [ + 1, + 2, + ], + "type": "integer", + }, + "temperature_unit": { + "default": "celsius", + "enum": [ + "celsius", + "fahrenheit", + ], + "type": "string", + }, + "timeformat": { + "default": "iso8601", + "description": "If format \`unixtime\` is selected, all time values are returned in UNIX epoch time in seconds. Please not that all time is then in GMT+0! For daily values with unix timestamp, please apply \`utc_offset_seconds\` again to get the correct date.", + "enum": [ + "iso8601", + "unixtime", + ], + "type": "string", + }, + "timezone": { + "description": "If \`timezone\` is set, all timestamps are returned as local-time and data is returned starting at 0:00 local-time. Any time zone name from the [time zone database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) is supported.", + "type": "string", + }, + "wind_speed_unit": { + "default": "kmh", + "enum": [ + "kmh", + "ms", + "mph", + "kn", + ], + "type": "string", + }, + }, + "required": [ + "latitude", + "longitude", + ], "type": "object", }, "name": "get_v1_forecast", @@ -1252,7 +1404,14 @@ exports[`getToolsFromOpenAPISpec > pet-store.json 1`] = ` { "description": "List all pets", "inputSchema": { - "properties": {}, + "properties": { + "limit": { + "description": "How many items to return at one time (max 100)", + "format": "int32", + "maximum": 100, + "type": "integer", + }, + }, "required": [], "type": "object", }, @@ -1267,12 +1426,6 @@ exports[`getToolsFromOpenAPISpec > pet-store.json 1`] = ` "format": "int64", "type": "integer", }, - "limit": { - "description": "How many items to return at one time (max 100)", - "format": "int32", - "maximum": 100, - "type": "integer", - }, "name": { "type": "string", }, @@ -1292,8 +1445,15 @@ exports[`getToolsFromOpenAPISpec > pet-store.json 1`] = ` { "description": "Info for a specific pet", "inputSchema": { - "properties": {}, - "required": [], + "properties": { + "petId": { + "description": "The id of the pet to retrieve", + "type": "string", + }, + }, + "required": [ + "petId", + ], "type": "object", }, "name": "show_pet_by_id", @@ -1370,7 +1530,20 @@ Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagitt Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.", "inputSchema": { - "properties": {}, + "properties": { + "limit": { + "description": "maximum number of results to return", + "format": "int32", + "type": "integer", + }, + "tags": { + "description": "tags to filter by", + "items": { + "type": "string", + }, + "type": "array", + }, + }, "required": [], "type": "object", }, @@ -1381,24 +1554,12 @@ Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condime "description": "Creates a new pet in the store. Duplicates are allowed", "inputSchema": { "properties": { - "limit": { - "description": "maximum number of results to return", - "format": "int32", - "type": "integer", - }, "name": { "type": "string", }, "tag": { "type": "string", }, - "tags": { - "description": "tags to filter by", - "items": { - "type": "string", - }, - "type": "array", - }, }, "required": [ "name", @@ -1411,8 +1572,16 @@ Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condime { "description": "Returns a user based on a single ID, if the user does not have access to the pet", "inputSchema": { - "properties": {}, - "required": [], + "properties": { + "id": { + "description": "ID of pet to fetch", + "format": "int64", + "type": "integer", + }, + }, + "required": [ + "id", + ], "type": "object", }, "name": "find_pet_by_id", @@ -1423,7 +1592,7 @@ Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condime "inputSchema": { "properties": { "id": { - "description": "ID of pet to fetch", + "description": "ID of pet to delete", "format": "int64", "type": "integer", }, diff --git a/packages/openapi-utils/src/get-tools-from-openapi-spec.ts b/packages/openapi-utils/src/get-tools-from-openapi-spec.ts index 50d39952..d9296792 100644 --- a/packages/openapi-utils/src/get-tools-from-openapi-spec.ts +++ b/packages/openapi-utils/src/get-tools-from-openapi-spec.ts @@ -175,7 +175,7 @@ export async function getToolsFromOpenAPISpec( for (const source of paramSources) { if (params[source]) { - mergeJsonSchemaObjects(pathParamsJsonSchema, params[source], { + mergeJsonSchemaObjects(operationParamsJsonSchema, params[source], { source, sources: operationParamsSources, label: `operation "${operationId}"` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 776684dc..d0d2dbd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,9 @@ catalogs: p-all: specifier: ^5.0.0 version: 5.0.0 + p-map: + specifier: ^7.0.3 + version: 7.0.3 parse-json: specifier: ^8.3.0 version: 8.3.0 @@ -344,6 +347,10 @@ importers: version: 0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.7) apps/e2e: + dependencies: + p-map: + specifier: 'catalog:' + version: 7.0.3 devDependencies: '@agentic/platform': specifier: workspace:* @@ -351,6 +358,9 @@ importers: '@agentic/platform-api-client': specifier: workspace:* version: link:../../packages/api-client + '@agentic/platform-core': + specifier: workspace:* + version: link:../../packages/core '@agentic/platform-fixtures': specifier: workspace:* version: link:../../packages/fixtures