Repository: flipt-io/flipt-client-sdks (language: python)

Issue:
[FLI-1275] Try WASM impl for node SDK
We should try switching to the WASM engine for node, since it has first class support for WASM

https://nodejs.org/en/learn/getting-started/nodejs-with-webassembly

<sub>[FLI-1275](https://linear.app/flipt/issue/FLI-1275/try-wasm-impl-for-node-sdk)</sub>


Discussion / hints:

Fix patch:
diff --git a/flipt-client-browser/README.md b/flipt-client-browser/README.md
index 9e27346e..e3df3dc8 100644
--- a/flipt-client-browser/README.md
+++ b/flipt-client-browser/README.md
@@ -22,14 +22,14 @@ import { FliptEvaluationClient } from '@flipt-io/flipt-client-browser';
 // {
 //  "url": "http://localhost:8080",
 //  "authentication": {
-//    "client_token": "secret"
+//    "clientToken": "secret"
 //  }
 // }
 //
 const fliptEvaluationClient = await FliptEvaluationClient.init('default', {
   url: 'http://localhost:8080',
   authentication: {
-    client_token
+    clientToken
   }
 });
 
@@ -69,7 +69,7 @@ The fetcher can be passed in as an argument to the `FliptEvaluationClient` initi
 const fliptEvaluationClient = await FliptEvaluationClient.init('default', {
   url: 'http://localhost:8080',
   authentication: {
-    client_token
+    clientToken
   },
   fetcher: customFetcher
 });
diff --git a/flipt-client-browser/package-lock.json b/flipt-client-browser/package-lock.json
index 66ec95a7..cbd18370 100644
--- a/flipt-client-browser/package-lock.json
+++ b/flipt-client-browser/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@flipt-io/flipt-client-browser",
-  "version": "0.1.0",
+  "version": "0.2.0-rc.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@flipt-io/flipt-client-browser",
-      "version": "0.1.0",
+      "version": "0.2.0-rc.1",
       "license": "MIT",
       "devDependencies": {
         "@rollup/plugin-typescript": "^11.1.6",
diff --git a/flipt-client-browser/package.json b/flipt-client-browser/package.json
index 4162dfa5..4dba3460 100644
--- a/flipt-client-browser/package.json
+++ b/flipt-client-browser/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@flipt-io/flipt-client-browser",
-  "version": "0.1.0",
+  "version": "0.2.0-rc.1",
   "description": "Flipt Client Evaluation Browser SDK",
   "main": "dist/index.cjs",
   "module": "dist/index.mjs",
diff --git a/flipt-client-browser/src/index.ts b/flipt-client-browser/src/index.ts
index 10f4c889..744db274 100644
--- a/flipt-client-browser/src/index.ts
+++ b/flipt-client-browser/src/index.ts
@@ -1,14 +1,18 @@
 import init, { Engine } from '../dist/flipt_engine_wasm.js';
 import wasm from '../dist/flipt_engine_wasm_bg.wasm';
-
+import { deserialize, serialize } from './utils';
 import {
   BatchEvaluationResponse,
   BatchResult,
   BooleanEvaluationResponse,
   BooleanResult,
   ClientOptions,
+  ErrorEvaluationResponse,
   EvaluationRequest,
+  EvaluationResponse,
+  Flag,
   IFetcher,
+  ListFlagsResult,
   VariantEvaluationResponse,
   VariantResult
 } from './models.js';
@@ -52,15 +56,15 @@ export class FliptEvaluationClient {
     headers.append('x-flipt-accept-server-version', '1.47.0');
 
     if (options.authentication) {
-      if ('client_token' in options.authentication) {
+      if ('clientToken' in options.authentication) {
         headers.append(
           'Authorization',
-          `Bearer ${options.authentication.client_token}`
+          `Bearer ${options.authentication.clientToken}`
         );
-      } else if ('jwt_token' in options.authentication) {
+      } else if ('jwtToken' in options.authentication) {
         headers.append(
           'Authorization',
-          `JWT ${options.authentication.jwt_token}`
+          `JWT ${options.authentication.jwtToken}`
         );
       }
     }
@@ -78,28 +82,25 @@ export class FliptEvaluationClient {
           headers
         });
 
-        // check for 304 status code
         if (resp.status === 304) {
           return resp;
         }
 
-        // ok only checks for range 200-299
         if (!resp.ok) {
-          throw new Error('Failed to fetch data');
+          throw new Error(`Failed to fetch data: ${resp.statusText}`);
         }
 
         return resp;
       };
     }
 
-    // should be no etag on first fetch
+    // handle case if they pass in a custom fetcher that doesn't throw on non-2xx status codes
     const resp = await fetcher();
-    if (!resp) {
-      throw new Error('Failed to fetch data');
+    if (!resp.ok && resp.status !== 304) {
+      throw new Error(`Failed to fetch data: ${resp.statusText}`);
     }
 
     const data = await resp.json();
-
     const engine = new Engine(namespace);
     engine.snapshot(data);
     return new FliptEvaluationClient(engine, fetcher);
@@ -127,60 +128,80 @@ export class FliptEvaluationClient {
 
   /**
    * Evaluate a variant flag
-   * @param flag_key - flag key to evaluate
-   * @param entity_id - entity id to evaluate
+   * @param flagKey - flag key to evaluate
+   * @param entityId - entity id to evaluate
    * @param context - optional evaluation context
    * @returns {VariantEvaluationResponse}
    */
   public evaluateVariant(
-    flag_key: string,
-    entity_id: string,
+    flagKey: string,
+    entityId: string,
     context: {}
   ): VariantEvaluationResponse {
-    const evaluation_request: EvaluationRequest = {
-      flag_key,
-      entity_id,
+    const evaluationRequest: EvaluationRequest = {
+      flagKey,
+      entityId,
       context
     };
 
-    const result = this.engine.evaluate_variant(
-      evaluation_request
-    ) as VariantResult;
+    const result: VariantResult | null = this.engine.evaluate_variant(
+      serialize(evaluationRequest)
+    );
+
+    if (result === null) {
+      throw new Error('Failed to evaluate variant');
+    }
+
+    const variantResult = deserialize<VariantResult>(result);
+
+    if (variantResult.status === 'failure') {
+      throw new Error(variantResult.errorMessage);
+    }
 
-    if (result.status === 'failure') {
-      throw new Error(result.error_message);
+    if (!variantResult.result) {
+      throw new Error('Failed to evaluate variant');
     }
 
-    return result.result as VariantEvaluationResponse;
+    return deserialize<VariantEvaluationResponse>(variantResult.result);
   }
 
   /**
    * Evaluate a boolean flag
-   * @param flag_key - flag key to evaluate
-   * @param entity_id - entity id to evaluate
+   * @param flagKey - flag key to evaluate
+   * @param entityId - entity id to evaluate
    * @param context - optional evaluation context
    * @returns {BooleanEvaluationResponse}
    */
   public evaluateBoolean(
-    flag_key: string,
-    entity_id: string,
+    flagKey: string,
+    entityId: string,
     context: {}
   ): BooleanEvaluationResponse {
-    const evaluation_request: EvaluationRequest = {
-      flag_key,
-      entity_id,
+    const evaluationRequest: EvaluationRequest = {
+      flagKey,
+      entityId,
       context
     };
 
-    const result = this.engine.evaluate_boolean(
-      evaluation_request
-    ) as BooleanResult;
+    const result: BooleanResult | null = this.engine.evaluate_boolean(
+      serialize(evaluationRequest)
+    );
+
+    if (result === null) {
+      throw new Error('Failed to evaluate boolean');
+    }
+
+    const booleanResult = deserialize<BooleanResult>(result);
+
+    if (booleanResult.status === 'failure') {
+      throw new Error(booleanResult.errorMessage);
+    }
 
-    if (result.status === 'failure') {
-      throw new Error(result.error_message);
+    if (!booleanResult.result) {
+      throw new Error('Failed to evaluate boolean');
     }
 
-    return result.result as BooleanEvaluationResponse;
+    return deserialize<BooleanEvaluationResponse>(booleanResult.result);
   }
 
   /**
@@ -189,12 +210,85 @@ export class FliptEvaluationClient {
    * @returns {BatchEvaluationResponse}
    */
   public evaluateBatch(requests: EvaluationRequest[]): BatchEvaluationResponse {
-    const result = this.engine.evaluate_batch(requests) as BatchResult;
+    const serializedRequests = requests.map(serialize);
+    const batchResult: BatchResult | null =
+      this.engine.evaluate_batch(serializedRequests);
+
+    if (batchResult === null) {
+      throw new Error('Failed to evaluate batch');
+    }
+
+    if (batchResult.status === 'failure') {
+      throw new Error(batchResult.errorMessage);
+    }
+
+    if (!batchResult.result) {
+      throw new Error('Failed to evaluate batch');
+    }
+
+    const responses = batchResult.result.responses
+      .map((response): EvaluationResponse | undefined => {
+        if (response.type === 'BOOLEAN_EVALUATION_RESPONSE_TYPE') {
+          const booleanResponse = deserialize<BooleanEvaluationResponse>(
+            // @ts-ignore
+            response.boolean_evaluation_response
+          );
+          return {
+            booleanEvaluationResponse: booleanResponse,
+            type: 'BOOLEAN_EVALUATION_RESPONSE_TYPE'
+          };
+        }
+        if (response.type === 'VARIANT_EVALUATION_RESPONSE_TYPE') {
+          const variantResponse = deserialize<VariantEvaluationResponse>(
+            // @ts-ignore
+            response.variant_evaluation_response
+          );
+          return {
+            variantEvaluationResponse: variantResponse,
+            type: 'VARIANT_EVALUATION_RESPONSE_TYPE'
+          };
+        }
+        if (response.type === 'ERROR_EVALUATION_RESPONSE_TYPE') {
+          const errorResponse = deserialize<ErrorEvaluationResponse>(
+            // @ts-ignore
+            response.error_evaluation_response
+          );
+          return {
+            errorEvaluationResponse: errorResponse,
+            type: 'ERROR_EVALUATION_RESPONSE_TYPE'
+          };
+        }
+        return undefined;
+      })
+      .filter(
+        (response): response is EvaluationResponse => response !== undefined
+      );
+
+    const requestDurationMillis = batchResult.result.requestDurationMillis;
+
+    return {
+      responses,
+      requestDurationMillis
+    };
+  }
+
+  public listFlags(): Flag[] {
+    const listFlagsResult: ListFlagsResult | null = this.engine.list_flags();
+
+    if (listFlagsResult === null) {
+      throw new Error('Failed to list flags');
+    }
+
+    const flags = deserialize<ListFlagsResult>(listFlagsResult);
+
+    if (flags.status === 'failure') {
+      throw new Error(flags.errorMessage);
+    }
 
-    if (result.status === 'failure') {
-      throw new Error(result.error_message);
+    if (!flags.result) {
+      throw new Error('Failed to list flags');
     }
 
-    return result.result as BatchEvaluationResponse;
+    return flags.result.map(deserialize<Flag>);
   }
 }
diff --git a/flipt-client-browser/src/models.ts b/flipt-client-browser/src/models.ts
index 9b3346b3..c7db098c 100644
--- a/flipt-client-browser/src/models.ts
+++ b/flipt-client-browser/src/models.ts
@@ -1,79 +1,226 @@
+/**
+ * Represents the options for a fetcher function.
+ */
 export interface IFetcherOptions {
+  /**
+   * ETag for the request.
+   */
   etag?: string;
 }
 
+/**
+ * Represents a function that fetches a response from a remote Flipt instance.
+ */
 export interface IFetcher {
   (opts?: IFetcherOptions): Promise<Response>;
 }
 
+/**
+ * Authentication strategy for the client.
+ */
 export interface AuthenticationStrategy {}
 
+/**
+ * Client Token Authentication.
+ * @see {@link https://docs.flipt.io/authentication/using-tokens}.
+ */
 export interface ClientTokenAuthentication extends AuthenticationStrategy {
-  client_token: string;
+  /**
+   * Flipt client token.
+   */
+  clientToken: string;
 }
 
+/**
+ * JWT Authentication.
+ * @see {@link https://docs.flipt.io/authentication/using-jwts}
+ */
 export interface JWTAuthentication extends AuthenticationStrategy {
-  jwt_token: string;
+  /**
+   * JWT token.
+   */
+  jwtToken: string;
 }
 
 export interface ClientOptions {
+  /**
+   * The URL of the upstream Flipt instance.
+   *
+   * @defaultValue `http://localhost:8080`
+   */
   url?: string;
+  /**
+   * The interval (seconds) in which to fetch new flag state.
+   * @defaultValue `120` seconds.
+   */
+  updateInterval?: number;
+  /**
+   * The authentication strategy to use when communicating with the upstream Flipt instance. If not provided, the client will default to no authentication.
+   *
+   * @remarks
+   * Client supports the following authentication strategies: No Authentication, {@link ClientTokenAuthentication} and {@link JWTAuthentication}.
+   */
   authentication?: AuthenticationStrategy;
+  /**
+   * The reference to use when fetching flag state. If not provided, reference will not be used.
+   * @see {@link https://docs.flipt.io/guides/user/using-references}
+   */
   reference?: string;
   fetcher?: IFetcher;
 }
 
+/**
+ * Represents a request to evaluate a feature flag.
+ */
 export interface EvaluationRequest {
-  flag_key: string;
-  entity_id: string;
+  /**
+   * Feature flag key
+   */
+  flagKey: string;
+  /**
+   * Entity identifier
+   */
+  entityId: string;
+  /**
+   * Context information for flag evaluation
+   *
+   * @example
+   * ```
+   * {
+   *  fizz: 'buzz',
+   *  kind: 'small'
+   * }
+   * ```
+   */
   context: object;
 }
 
+/**
+ * Represents a feature flag in the system.
+ */
+export interface Flag {
+  /**
+   * Unique key for the flag.
+   */
+  key: string;
+  /**
+   * Status of the flag. For a Variant flag, this represents whether
+   * the flag is enabled. For a Boolean flag, this represents the
+   * default value.
+   */
+  enabled: boolean;
+  /**
+   * Type of the feature flag. This can be either 'BOOLEAN_FLAG_TYPE' or 'VARIANT_FLAG_TYPE'.
+   */
+  type: string;
+}
+
+/**
+ * Represents the response returned after evaluating a feature flag variant.
+ */
 export interface VariantEvaluationResponse {
+  /** Indicates whether the feature flag evaluation resulted in a match. */
   match: boolean;
-  segment_keys: string[];
+  /** List of segment keys that were used to determine the match. */
+  segmentKeys: string[];
+  /** Reason for the result that occurred during the evaluation. */
   reason: string;
-  flag_key: string;
-  variant_key?: string;
-  variant_attachment?: string;
-  request_duration_millis: number;
+  /** Key of the feature flag that was being evaluated. */
+  flagKey: string;
+  /** Variant key that was returned if the evaluation resulted in a match. */
+  variantKey: string;
+  /** Additional data attached to the variant if the evaluation resulted in a match. */
+  variantAttachment: string;
+  /** Duration of the request in milliseconds. */
+  requestDurationMillis: number;
+  /** Timestamp when the response was generated. */
   timestamp: string;
 }
 
+/**
+ * Represents the response returned after evaluating a feature flag.
+ */
 export interface BooleanEvaluationResponse {
+  /** Evaluation value of the flag. */
   enabled: boolean;
-  flag_key: string;
+  /** Key of the feature flag that was being evaluated. */
+  flagKey: string;
+  /** Reason for the result that occurred during the evaluation. */
   reason: string;
-  request_duration_millis: number;
+  /** Duration of the request in milliseconds. */
+  requestDurationMillis: number;
+  /** Timestamp when the response was generated. */
   timestamp: string;
 }
 
+/**
+ * Represents the response returned when an error occurs during the evaluation of a feature flag.
+ */
 export interface ErrorEvaluationResponse {
-  flag_key: string;
-  namespace_key: string;
+  /** Key of the feature flag that was being evaluated. */
+  flagKey: string;
+  /** Key of the namespace in which the feature flag resides. */
+  namespaceKey: string;
+  /** Reason for the result that occurred during the evaluation. */
   reason: string;
 }
 
+/**
+ * Represents a response object that encapsulates various types of evaluation results.
+ */
 export interface EvaluationResponse {
+  /**
+   * The type of evaluation response. Possible values include 'VARIANT_EVALUATION_RESPONSE_TYPE',
+   * 'BOOLEAN_EVALUATION_RESPONSE_TYPE', or 'ERROR_EVALUATION_RESPONSE_TYPE'.
+   */
   type: string;
-  boolean_evaluation_response?: BooleanEvaluationResponse;
-  variant_evaluation_response?: VariantEvaluationResponse;
-  error_evaluation_response?: ErrorEvaluationResponse;
+  /** Boolean evaluation response base on the type */
+  booleanEvaluationResponse?: BooleanEvaluationResponse;
+  /** Variant evaluation response base on the type */
+  variantEvaluationResponse?: VariantEvaluationResponse;
+  /** Error evaluation response base on the type*/
+  errorEvaluationResponse?: ErrorEvaluationResponse;
 }
 
+/**
+ * Represents the response returned after batch evaluating multiple feature flags.
+ */
 export interface BatchEvaluationResponse {
+  /** Array containing individual evaluation responses for each feature flag. */
   responses: EvaluationResponse[];
-  request_duration_millis: number;
-}
-
-export interface Result<T> {
-  status: 'success' | 'failure';
-  result?: T;
-  error_message: string;
+  /** Duration of the request in milliseconds. */
+  requestDurationMillis: number;
 }
 
+/**
+ * Represents a specialized result object for variant evaluation, extending
+ * the generic Result interface with a specific type VariantEvaluationResponse.
+ */
 export interface VariantResult extends Result<VariantEvaluationResponse> {}
 
+/**
+ * Represents a specialized result object for boolean evaluation, extending
+ * the generic Result interface with a specific type BooleanEvaluationResponse.
+ */
 export interface BooleanResult extends Result<BooleanEvaluationResponse> {}
 
+/**
+ * Represents a specialized result object for batch evaluation, extending
+ * the generic Result interface with a specific type BatchEvaluationResponse.
+ */
 export interface BatchResult extends Result<BatchEvaluationResponse> {}
+
+/**
+ * Represents a specialized result object for listing flags, extending
+ * the generic Result interface with a specific type ListFlagsResponse.
+ */
+export interface ListFlagsResult extends Result<Flag[]> {}
+
+export interface Result<T> {
+  /** Status of the result - `success` or `failure`. */
+  status: string;
+  /** Actual result of type T if the operation was successful. */
+  result?: T;
+  /** Error message describing the reason for failure, if applicable.*/
+  errorMessage: string;
+}
diff --git a/flipt-client-browser/src/utils.ts b/flipt-client-browser/src/utils.ts
new file mode 100644
index 00000000..fc549aa8
--- /dev/null
+++ b/flipt-client-browser/src/utils.ts
@@ -0,0 +1,31 @@
+export function snakeToCamel(str: string): string {
+  return str.replace(/([-_][a-z])/g, (group) =>
+    group.toUpperCase().replace('-', '').replace('_', '')
+  );
+}
+
+export function camelToSnake(str: string): string {
+  return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
+}
+
+export function deserialize<T>(data: any): T {
+  const result: any = {};
+  for (const key in data) {
+    if (Object.prototype.hasOwnProperty.call(data, key)) {
+      const camelKey = snakeToCamel(key);
+      result[camelKey] = data[key];
+    }
+  }
+  return result as T;
+}
+
+export function serialize<T>(data: T): any {
+  const result: any = {};
+  for (const key in data) {
+    if (Object.prototype.hasOwnProperty.call(data, key)) {
+      const snakeKey = camelToSnake(key);
+      result[snakeKey] = (data as any)[key];
+    }
+  }
+  return result;
+}
diff --git a/flipt-client-node/src/index.ts b/flipt-client-node/src/index.ts
index d5151e9a..8fb6929d 100644
--- a/flipt-client-node/src/index.ts
+++ b/flipt-client-node/src/index.ts
@@ -29,6 +29,12 @@ export class FliptEvaluationClient {
     this.fetcher = fetcher;
   }
 
+  /**
+   * Initialize the client
+   * @param namespace - optional namespace to evaluate flags
+   * @param options - optional client options
+   * @returns {Promise<FliptEvaluationClient>}
+   */
   static async init(
     namespace: string = 'default',
     options: ClientOptions<AuthenticationStrategy> = {
@@ -120,6 +126,10 @@ export class FliptEvaluationClient {
     }
   }
 
+  /**
+   * Refresh the flags snapshot
+   * @returns void
+   */
   public async refresh() {
     const opts = { etag: this.etag };
     const resp = await this.fetcher(opts);
@@ -136,6 +146,13 @@ export class FliptEvaluationClient {
     this.engine.snapshot(data);
   }
 
+  /**
+   * Evaluate a variant flag
+   * @param flagKey - flag key to evaluate
+   * @param entityId - entity id to evaluate
+   * @param context - optional evaluation context
+   * @returns {VariantEvaluationResponse}
+   */
   public evaluateVariant(
     flagKey: string,
     entityId: string,
@@ -168,6 +185,13 @@ export class FliptEvaluationClient {
     return deserialize<VariantEvaluationResponse>(variantResult.result);
   }
 
+  /**
+   * Evaluate a boolean flag
+   * @param flagKey - flag key to evaluate
+   * @param entityId - entity id to evaluate
+   * @param context - optional evaluation context
+   * @returns {BooleanEvaluationResponse}
+   */
   public evaluateBoolean(
     flagKey: string,
     entityId: string,
@@ -200,6 +224,11 @@ export class FliptEvaluationClient {
     return deserialize<BooleanEvaluationResponse>(booleanResult.result);
   }
 
+  /**
+   * Evaluate a batch of flag requests
+   * @param requests evaluation requests
+   * @returns {BatchEvaluationResponse}
+   */
   public evaluateBatch(requests: EvaluationRequest[]): BatchEvaluationResponse {
     const serializedRequests = requests.map(serialize);
     const batchResult: BatchResult | null =


Test patch:
diff --git a/flipt-client-browser/__tests__/evaluation.test.js b/flipt-client-browser/__tests__/evaluation.test.js
index 97bf46d5..cf3dd633 100644
--- a/flipt-client-browser/__tests__/evaluation.test.js
+++ b/flipt-client-browser/__tests__/evaluation.test.js
@@ -19,11 +19,11 @@ describe('FliptEvaluationClient', () => {
     }
   });
 
-  beforeEach(async () => {
+  beforeAll(async () => {
     client = await flipt.FliptEvaluationClient.init('default', {
       url: fliptUrl,
       authentication: {
-        client_token: authToken
+        clientToken: authToken
       }
     });
   });
@@ -33,11 +33,11 @@ describe('FliptEvaluationClient', () => {
       fizz: 'buzz'
     });
 
-    expect(variant.flag_key).toEqual('flag1');
+    expect(variant.flagKey).toEqual('flag1');
     expect(variant.match).toEqual(true);
     expect(variant.reason).toEqual('MATCH_EVALUATION_REASON');
-    expect(variant.segment_keys).toContain('segment1');
-    expect(variant.variant_key).toContain('variant1');
+    expect(variant.segmentKeys).toContain('segment1');
+    expect(variant.variantKey).toContain('variant1');
   });
 
   test('boolean', () => {
@@ -76,32 +76,32 @@ describe('FliptEvaluationClient', () => {
       fizz: 'buzz'
     });
 
-    expect(variant.flag_key).toEqual('flag1');
+    expect(variant.flagKey).toEqual('flag1');
     expect(variant.match).toEqual(true);
     expect(variant.reason).toEqual('MATCH_EVALUATION_REASON');
-    expect(variant.segment_keys).toContain('segment1');
-    expect(variant.variant_key).toContain('variant1');
+    expect(variant.segmentKeys).toContain('segment1');
+    expect(variant.variantKey).toContain('variant1');
   });
 
   test('batch', () => {
     const batch = client.evaluateBatch([
       {
-        flag_key: 'flag1',
-        entity_id: 'someentity',
+        flagKey: 'flag1',
+        entityId: 'someentity',
         context: {
           fizz: 'buzz'
         }
       },
       {
-        flag_key: 'flag_boolean',
-        entity_id: 'someentity',
+        flagKey: 'flag_boolean',
+        entityId: 'someentity',
         context: {
           fizz: 'buzz'
         }
       },
       {
-        flag_key: 'notfound',
-        entity_id: 'someentity',
+        flagKey: 'notfound',
+        entityId: 'someentity',
         context: {
           fizz: 'buzz'
         }
@@ -112,34 +112,38 @@ describe('FliptEvaluationClient', () => {
     const variant = batch.responses[0];
 
     expect(variant?.type).toEqual('VARIANT_EVALUATION_RESPONSE_TYPE');
-    expect(variant?.variant_evaluation_response?.flag_key).toEqual('flag1');
-    expect(variant?.variant_evaluation_response?.match).toEqual(true);
-    expect(variant?.variant_evaluation_response?.reason).toEqual(
+    expect(variant?.variantEvaluationResponse?.flagKey).toEqual('flag1');
+    expect(variant?.variantEvaluationResponse?.match).toEqual(true);
+    expect(variant?.variantEvaluationResponse?.reason).toEqual(
       'MATCH_EVALUATION_REASON'
     );
-    expect(variant?.variant_evaluation_response?.segment_keys).toContain(
+    expect(variant?.variantEvaluationResponse?.segmentKeys).toContain(
       'segment1'
     );
-    expect(variant?.variant_evaluation_response?.variant_key).toContain(
+    expect(variant?.variantEvaluationResponse?.variantKey).toContain(
       'variant1'
     );
 
     const boolean = batch.responses[1];
     expect(boolean?.type).toEqual('BOOLEAN_EVALUATION_RESPONSE_TYPE');
-    expect(boolean?.boolean_evaluation_response?.flag_key).toEqual(
-      'flag_boolean'
-    );
-    expect(boolean?.boolean_evaluation_response?.enabled).toEqual(true);
-    expect(boolean?.boolean_evaluation_response?.reason).toEqual(
+    expect(boolean?.booleanEvaluationResponse?.flagKey).toEqual('flag_boolean');
+    expect(boolean?.booleanEvaluationResponse?.enabled).toEqual(true);
+    expect(boolean?.booleanEvaluationResponse?.reason).toEqual(
       'MATCH_EVALUATION_REASON'
     );
 
     const error = batch.responses[2];
     expect(error?.type).toEqual('ERROR_EVALUATION_RESPONSE_TYPE');
-    expect(error?.error_evaluation_response?.flag_key).toEqual('notfound');
-    expect(error?.error_evaluation_response?.namespace_key).toEqual('default');
-    expect(error?.error_evaluation_response?.reason).toEqual(
+    expect(error?.errorEvaluationResponse?.flagKey).toEqual('notfound');
+    expect(error?.errorEvaluationResponse?.namespaceKey).toEqual('default');
+    expect(error?.errorEvaluationResponse?.reason).toEqual(
       'NOT_FOUND_ERROR_EVALUATION_REASON'
     );
   });
+
+  test('list flags', () => {
+    const flags = client.listFlags();
+    expect(flags).toBeDefined();
+    expect(flags.length).toBe(2);
+  });
 });
diff --git a/test/main.go b/test/main.go
index e90d98da..1380faa1 100644
--- a/test/main.go
+++ b/test/main.go
@@ -223,13 +223,13 @@ func goTests(ctx context.Context, client *dagger.Client, t *testCase) error {
 // nodeTests runs the node integration test suite against a container running Flipt.
 func nodeTests(ctx context.Context, client *dagger.Client, t *testCase) error {
 	_, err := client.Pipeline("node").Container().From("node:21.2-bookworm").
+		WithWorkdir("/src").
 		WithDirectory("/src", t.dir.Directory("flipt-client-node"), dagger.ContainerWithDirectoryOpts{
 			Exclude: []string{".node_modules/", ".gitignore", "dist/"},
 		}).
 		WithDirectory("/src/dist", t.test.Directory(wasmDir), dagger.ContainerWithDirectoryOpts{
 			Exclude: []string{".node_modules/", ".gitignore", "package.json", "README.md", "LICENSE"},
 		}).
-		WithWorkdir("/src").
 		WithServiceBinding("flipt", t.flipt.WithExec(nil).AsService()).
 		WithEnvVariable("FLIPT_URL", "http://flipt:8080").
 		WithEnvVariable("FLIPT_AUTH_TOKEN", "secret").
@@ -283,13 +283,13 @@ func javaTests(ctx context.Context, client *dagger.Client, t *testCase) error {
 // browserTests runs the browser integration test suite against a container running Flipt.
 func browserTests(ctx context.Context, client *dagger.Client, t *testCase) error {
 	_, err := client.Pipeline("browser").Container().From("node:21.2-bookworm").
+		WithWorkdir("/src").
 		WithDirectory("/src", t.dir.Directory("flipt-client-browser"), dagger.ContainerWithDirectoryOpts{
 			Exclude: []string{".node_modules/", ".gitignore", "dist/"},
 		}).
 		WithDirectory("/src/dist", t.test.Directory(wasmDir), dagger.ContainerWithDirectoryOpts{
 			Exclude: []string{".node_modules/", ".gitignore", "package.json", "README.md", "LICENSE"},
 		}).
-		WithWorkdir("/src").
 		WithServiceBinding("flipt", t.flipt.WithExec(nil).AsService()).
 		WithEnvVariable("FLIPT_URL", "http://flipt:8080").
 		WithEnvVariable("FLIPT_AUTH_TOKEN", "secret").