This is an automated email from the ASF dual-hosted git repository.

jason810496 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 559330b276a TS SDK: add setVariable and deleteVariable to the task 
client (#73357)
559330b276a is described below

commit 559330b276ae4a95e5127eee1950f20343b8b997
Author: Aaron Chen <[email protected]>
AuthorDate: Sun Sep 20 03:48:33 2026 -0700

    TS SDK: add setVariable and deleteVariable to the task client (#73357)
    
    * TS SDK: add setVariable and deleteVariable to the task client
    
    * Update TypeScript SDK documentation and improve deleteVariable behavior 
description
---
 .../language-sdks/typescript.rst                   | 10 ++++
 .../ts_sdk_tests/test_ts_sdk_dag.py                | 33 ++++++++++-
 ts-sdk/README.md                                   | 13 +++--
 ts-sdk/capabilities.yaml                           |  5 +-
 ts-sdk/example/README.md                           |  5 +-
 ts-sdk/example/dags/typescript_example.py          |  5 ++
 ts-sdk/example/src/main.ts                         | 17 +++++-
 ts-sdk/src/coordinator/client.ts                   | 66 +++++++++++++++++-----
 ts-sdk/src/coordinator/protocol.ts                 |  2 +
 ts-sdk/src/sdk/client.ts                           | 18 ++++++
 ts-sdk/tests/coordinator/client.test.ts            | 64 +++++++++++++++++++++
 ts-sdk/tests/public-api.test.ts                    |  4 ++
 12 files changed, 217 insertions(+), 25 deletions(-)

diff --git 
a/airflow-core/docs/authoring-and-scheduling/language-sdks/typescript.rst 
b/airflow-core/docs/authoring-and-scheduling/language-sdks/typescript.rst
index bc21f61f6a1..b954ff9cd6c 100644
--- a/airflow-core/docs/authoring-and-scheduling/language-sdks/typescript.rst
+++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/typescript.rst
@@ -279,6 +279,9 @@ The ``TaskClient`` surface
 * ``getVariable(key)`` returns the Variable as a string, or ``null`` when it 
is missing;
   ``getVariableOrThrow(key)`` throws ``VariableNotFoundError`` instead, 
matching Python ``Variable.get``
   with no default.
+* ``setVariable(key, value, description?)`` stores a Variable, replacing any 
existing value, and
+  ``deleteVariable(key)`` removes one. Values are stored as strings, so 
serialize structured data (for
+  example with ``JSON.stringify``) before storing it.
 * ``getConnection(connId)`` returns a ``ConnectionResult`` with fields ``id`` 
and ``type``, plus the
   optional fields ``host``, ``schema``, ``login``, ``password``, ``port``, and 
``extra`` (each may be
   missing or ``null``), or ``null`` when the connection does not exist;
@@ -289,6 +292,13 @@ The ``TaskClient`` surface
   upstream task's XCom. See :ref:`typescript-sdk/types` for how the stored 
JSON maps to JavaScript types.
 * ``setXCom({key, value, ...})`` publishes an XCom value.
 
+.. note::
+
+   A value supplied by a secrets backend (for example an ``AIRFLOW_VAR_*`` 
environment variable) still
+   takes precedence over the stored value when the Variable is read back. 
Calling ``setVariable`` without
+   a description clears the description the Variable had, and 
``deleteVariable`` resolves even when the
+   key does not exist.
+
 Logging
 -------
 
diff --git 
a/airflow-e2e-tests/tests/airflow_e2e_tests/ts_sdk_tests/test_ts_sdk_dag.py 
b/airflow-e2e-tests/tests/airflow_e2e_tests/ts_sdk_tests/test_ts_sdk_dag.py
index 69c2e662ca1..fe09cc2eb81 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/ts_sdk_tests/test_ts_sdk_dag.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/ts_sdk_tests/test_ts_sdk_dag.py
@@ -24,8 +24,8 @@ Run with::
 Two Dags mix Python tasks with ``@task.stub`` TypeScript tasks, both served by 
the single
 ``airflow-ts-pack`` bundle, and each is triggered once via a module-scoped 
fixture.
 
-``typescript_example`` covers the runtime: Variable and Connection reads, 
Python <-> TypeScript XCom
-round-trips, and task logs reaching the log store.
+``typescript_example`` covers the runtime: Variable reads and writes, 
Connection reads,
+Python <-> TypeScript XCom round-trips, and task logs reaching the log store.
 
 ``typescript_taskflow_example`` covers TaskFlow arguments, including an 
upstream output pulled before
 the handler runs and a ``withArgNames`` rename on its ``report`` task, and 
shares a ``build_message``
@@ -38,8 +38,10 @@ from __future__ import annotations
 import time
 from dataclasses import dataclass
 from datetime import datetime, timezone
+from http import HTTPStatus
 
 import pytest
+import requests
 
 from airflow_e2e_tests.e2e_test_utils.clients import AirflowClient
 
@@ -53,6 +55,11 @@ _LOG_FETCH_TIMEOUT = 120
 _DAG_ID = "typescript_example"
 _TASKFLOW_DAG_ID = "typescript_taskflow_example"
 
+# Written by `write_and_delete_variable`; see ts-sdk/example/src/main.ts.
+_LAST_RUN_VARIABLE = "typescript_example_last_run"
+_LAST_RUN_DESCRIPTION = "Run id of the last typescript_example run"
+_SCRATCH_VARIABLE = "typescript_example_scratch"
+
 
 @dataclass
 class _CompletedRun:
@@ -113,6 +120,7 @@ def test_task_states(completed_run: _CompletedRun):
         "python_start": "success",
         "build_message": "success",
         "read_connection": "success",
+        "write_and_delete_variable": "success",
     }
     for task_id, want in expected.items():
         assert completed_run.ti_states.get(task_id) == want, (
@@ -144,6 +152,27 @@ def test_read_connection_xcom(completed_run: 
_CompletedRun):
     }, f"unexpected 'read_connection' return_value: {value!r}"
 
 
+def test_variable_written_by_typescript_task_is_readable(completed_run: 
_CompletedRun):
+    """``setVariable`` stores this run's id and the description alongside 
it."""
+    variable = completed_run.client.get_variable(_LAST_RUN_VARIABLE)
+    assert variable.get("value") == completed_run.run_id, (
+        f"{_LAST_RUN_VARIABLE} should hold this run's id 
{completed_run.run_id!r}, got {variable!r}"
+    )
+    assert variable.get("description") == _LAST_RUN_DESCRIPTION, (
+        f"{_LAST_RUN_VARIABLE} should carry the description set from 
TypeScript, got {variable!r}"
+    )
+
+
+def test_scratch_variable_deleted_by_typescript_task_is_gone(completed_run: 
_CompletedRun):
+    """A Variable written and then deleted from TypeScript no longer exists."""
+    with pytest.raises(requests.HTTPError) as excinfo:
+        completed_run.client.get_variable(_SCRATCH_VARIABLE)
+    assert excinfo.value.response.status_code == HTTPStatus.NOT_FOUND, (
+        f"{_SCRATCH_VARIABLE} should have been deleted by the TypeScript task, 
"
+        f"got HTTP {excinfo.value.response.status_code}"
+    )
+
+
 def test_coordinator_logs_reach_task_log_store(completed_run: _CompletedRun):
     assert "[ts-sdk.runtime] Coordinator runtime started" in 
completed_run.logs("build_message")
 
diff --git a/ts-sdk/README.md b/ts-sdk/README.md
index 869072f1ff8..361e987fa31 100644
--- a/ts-sdk/README.md
+++ b/ts-sdk/README.md
@@ -309,11 +309,12 @@ Options:
 
 `getClient()` returns a `TaskClient` for task-time Airflow data access, for as 
long as a handler is running:
 
-| Method                                           | Description         |
-| ------------------------------------------------ | ------------------- |
-| `getVariable(key)` / `getVariableOrThrow`        | Airflow Variables   |
-| `getXCom(opts)` / `setXCom(opts)`                | XCom read/write     |
-| `getConnection(connId)` / `getConnectionOrThrow` | Airflow Connections |
+| Method                                                          | 
Description             |
+| --------------------------------------------------------------- | 
----------------------- |
+| `getVariable(key)` / `getVariableOrThrow`                       | Airflow 
Variables       |
+| `setVariable(key, value, description?)` / `deleteVariable(key)` | Variable 
write / delete |
+| `getXCom(opts)` / `setXCom(opts)`                               | XCom 
read/write         |
+| `getConnection(connId)` / `getConnectionOrThrow`                | Airflow 
Connections     |
 
 Locator fields such as `dagId`, `runId`, and `taskId` default to the
 current task context when omitted.
@@ -353,7 +354,7 @@ Do not edit the table by hand. Update the manifest and run 
the `update-ts-sdk-re
 | capability: `task-logging` | MUST | ✓ | 3.4 | structured records over the 
log socket |
 | capability: `xcom-read-write` | MUST | ✓ | 3.4 | getXCom / setXCom |
 | capability: `connection-read` | MUST | ✓ | 3.4 | getConnection |
-| capability: `variable-read-write` | MUST | ✗ | – | getVariable only; no 
write over the comm socket yet |
+| capability: `variable-read-write` | MUST | ✓ | 3.4 | getVariable / 
setVariable / deleteVariable |
 | capability: `self-contained-bundle` | MUST | ✓ | 3.4 | Airflow metadata 
embedded in the bundle |
 | capability: `retry-policy` | MAY | ✗ | – | no task-facing retry-policy API 
yet |
 | capability: `task-state-store` | MAY | ✗ | – | no task-facing state-store 
API yet |
diff --git a/ts-sdk/capabilities.yaml b/ts-sdk/capabilities.yaml
index 65482fc8f4b..170be9385cb 100644
--- a/ts-sdk/capabilities.yaml
+++ b/ts-sdk/capabilities.yaml
@@ -75,8 +75,9 @@ capabilities:
     since: "3.4"
     note: "getConnection"
   variable-read-write:
-    supported: false
-    note: "getVariable only; no write over the comm socket yet"
+    supported: true
+    since: "3.4"
+    note: "getVariable / setVariable / deleteVariable"
   self-contained-bundle:
     supported: true
     since: "3.4"
diff --git a/ts-sdk/example/README.md b/ts-sdk/example/README.md
index 4af4af57471..94c2ead3951 100644
--- a/ts-sdk/example/README.md
+++ b/ts-sdk/example/README.md
@@ -73,7 +73,7 @@ export AIRFLOW__SDK__QUEUE_TO_COORDINATOR='{"typescript": 
"ts"}'
 
 Copy both files in `dags/` into your Airflow Dags folder.
 
-The example also uses one Variable and one Connection:
+The example also reads one Variable and one Connection:
 
 ```bash
 airflow variables set typescript_example_greeting "hello from Airflow"
@@ -84,6 +84,9 @@ airflow connections add typescript_example_http \
   --conn-password pass
 ```
 
+`write_and_delete_variable` writes the Variables it needs: it records the run 
id in
+`typescript_example_last_run` and deletes the `typescript_example_scratch` 
Variable it has just written.
+
 Then start Airflow and trigger the Dag:
 
 ```bash
diff --git a/ts-sdk/example/dags/typescript_example.py 
b/ts-sdk/example/dags/typescript_example.py
index 6f6ae0922d0..67718ec4bcf 100644
--- a/ts-sdk/example/dags/typescript_example.py
+++ b/ts-sdk/example/dags/typescript_example.py
@@ -33,11 +33,16 @@ def build_message(): ...
 def read_connection(): ...
 
 
[email protected](queue="typescript")
+def write_and_delete_variable(): ...
+
+
 @dag(dag_id="typescript_example", schedule=None, catchup=False, 
tags=["typescript", "example"])
 def typescript_example():
     start = python_start()
     message = build_message()
     read_connection()
+    write_and_delete_variable()
 
     start >> message
 
diff --git a/ts-sdk/example/src/main.ts b/ts-sdk/example/src/main.ts
index 20e0ac8edff..fa97d3f9899 100644
--- a/ts-sdk/example/src/main.ts
+++ b/ts-sdk/example/src/main.ts
@@ -22,7 +22,7 @@
 // Both Dags in `dags/` are declared in Python with `@task.stub` tasks routed 
to the Node
 // coordinator, so this side only supplies the task bodies.
 
-import { Bundle, getClient, TaskHandler } from "apache-airflow-ts-sdk";
+import { Bundle, getClient, getContext, TaskHandler } from 
"apache-airflow-ts-sdk";
 
 import { buildSummaryMessage, report, summarize } from "./taskflow.js";
 
@@ -43,6 +43,20 @@ export async function buildMessage() {
   };
 }
 
+/** Records the run that last wrote it, so a later run can see it changed. */
+const LAST_RUN_VARIABLE = "typescript_example_last_run";
+/** Written and deleted within the same task, to show both write directions. */
+const SCRATCH_VARIABLE = "typescript_example_scratch";
+
+export async function writeAndDeleteVariable() {
+  const client = getClient();
+  const { runId } = getContext();
+
+  await client.setVariable(LAST_RUN_VARIABLE, runId, "Run id of the last 
typescript_example run");
+  await client.setVariable(SCRATCH_VARIABLE, runId);
+  await client.deleteVariable(SCRATCH_VARIABLE);
+}
+
 export async function readConnection() {
   const connection = await 
getClient().getConnection("typescript_example_http");
 
@@ -62,6 +76,7 @@ const bundle = new Bundle();
 bundle.register(
   new TaskHandler("typescript_example", "build_message", buildMessage),
   new TaskHandler("typescript_example", "read_connection", readConnection),
+  new TaskHandler("typescript_example", "write_and_delete_variable", 
writeAndDeleteVariable),
   new TaskHandler("typescript_taskflow_example", "summarize", summarize),
   new TaskHandler("typescript_taskflow_example", "report", report),
   new TaskHandler("typescript_taskflow_example", "build_message", 
buildSummaryMessage),
diff --git a/ts-sdk/src/coordinator/client.ts b/ts-sdk/src/coordinator/client.ts
index 32a0405ce6f..ba0be84faa2 100644
--- a/ts-sdk/src/coordinator/client.ts
+++ b/ts-sdk/src/coordinator/client.ts
@@ -25,12 +25,18 @@ import type { ConnectionResult, GetXComOpts, JsonValue, 
SetXComOpts } from "../s
 import { ConnectionNotFoundError, VariableNotFoundError } from 
"../sdk/client.js";
 import type {
   GetVariable,
+  PutVariable,
+  DeleteVariable,
   GetXCom,
   SetXCom,
   GetConnection,
   ConnectionResult as WireConnectionResult,
 } from "./protocol.js";
 
+/** What a supervisor "row is absent" error means for an operation: only a 
lookup
+ *  can return `null`; for a `void` call a swallowed error would read as 
success. */
+type AbsentRowPolicy = "null" | "throw";
+
 function resolveWireMapIndex(
   requestedMapIndex: number | null | undefined,
   contextMapIndex: number,
@@ -88,12 +94,13 @@ export function createCoordinatorClient(
     expectedType: string | null,
     request: unknown,
     extract: (body: Record<string, unknown> | null) => T,
+    absent: AbsentRowPolicy,
   ): Promise<T | null> {
     logs?.debug(`${op} request`);
     const frame = await comm.request(request);
     const err = parseFrameError(frame);
     if (err) {
-      if (isNotFound(err)) {
+      if (absent === "null" && isNotFound(err)) {
         logs?.debug(`${op} not found`, { error: err.code });
         return null;
       }
@@ -117,7 +124,13 @@ export function createCoordinatorClient(
 
     async getVariable(key: string): Promise<string | null> {
       const msg: GetVariable = { type: "GetVariable", key };
-      return rpc("GetVariable", "VariableResult", msg, (body) => (body!.value 
as string) ?? null);
+      return rpc(
+        "GetVariable",
+        "VariableResult",
+        msg,
+        (body) => (body!.value as string) ?? null,
+        "null",
+      );
     },
 
     async getVariableOrThrow(key: string): Promise<string> {
@@ -126,6 +139,23 @@ export function createCoordinatorClient(
       return value;
     },
 
+    async setVariable(key: string, value: string, description?: string | 
null): Promise<void> {
+      // `description` is a required wire field the supervisor validates, so it
+      // is always sent; null is what Python's `Variable.set` stores by 
default.
+      const msg: PutVariable = {
+        type: "PutVariable",
+        key,
+        value,
+        description: description ?? null,
+      };
+      await rpc("PutVariable", null, msg, () => undefined, "throw");
+    },
+
+    async deleteVariable(key: string): Promise<void> {
+      const msg: DeleteVariable = { type: "DeleteVariable", key };
+      await rpc("DeleteVariable", "OKResponse", msg, () => undefined, "throw");
+    },
+
     // ---- XCom ----
 
     async getXComEntry(opts: GetXComOpts): Promise<XComEntry> {
@@ -138,12 +168,18 @@ export function createCoordinatorClient(
         map_index: resolveWireMapIndex(opts.mapIndex, ctx.mapIndex),
         include_prior_dates: opts.includePriorDates ?? false,
       };
-      const entry = await rpc<XComEntry>("GetXCom", "XComResult", msg, (body) 
=> ({
-        found: true,
-        // A row storing null arrives as an XComResult carrying null, so the
-        // result frame decides `found` rather than the value.
-        value: (body!.value ?? null) as JsonValue,
-      }));
+      const entry = await rpc<XComEntry>(
+        "GetXCom",
+        "XComResult",
+        msg,
+        (body) => ({
+          found: true,
+          // A row storing null arrives as an XComResult carrying null, so the
+          // result frame decides `found` rather than the value.
+          value: (body!.value ?? null) as JsonValue,
+        }),
+        "null",
+      );
       // `rpc` answers null for the supervisor's XCOM_NOT_FOUND.
       return entry ?? XCOM_ABSENT;
     },
@@ -163,15 +199,19 @@ export function createCoordinatorClient(
         run_id: opts.runId ?? ctx.runId,
         map_index: resolveWireMapIndex(opts.mapIndex, ctx.mapIndex),
       };
-      await rpc("SetXCom", null, msg, () => undefined);
+      await rpc("SetXCom", null, msg, () => undefined, "throw");
     },
 
     // ---- Connections ----
 
     async getConnection(connId: string): Promise<ConnectionResult | null> {
       const msg: GetConnection = { type: "GetConnection", conn_id: connId };
-      return rpc("GetConnection", "ConnectionResult", msg, (body) =>
-        fromWireConnection(body as unknown as WireConnectionResult),
+      return rpc(
+        "GetConnection",
+        "ConnectionResult",
+        msg,
+        (body) => fromWireConnection(body as unknown as WireConnectionResult),
+        "null",
       );
     },
 
@@ -187,7 +227,7 @@ export function createCoordinatorClient(
 // -------- Error handling (two functions) --------
 //
 // parseFrameError: extract a structured error from the frame (once).
-// isNotFound: decide if the error means "absent" (return null to caller)
+// isNotFound: decide if the error means "absent" (a lookup returns null)
 //             or "failed" (throw).
 
 interface FrameError {
@@ -228,7 +268,7 @@ function isNotFound(err: FrameError): boolean {
   // The supervisor wraps API server 404s as API_SERVER_ERROR with
   // detail.status_code=404 (supervisor.py: WatchedSubprocess.handle_requests).
   // Dag / Dag run lookups hit this path.
-  // TODO: If the TS client adds APIs beyond variables, XCom, and connections,
+  // TODO: If the TS client adds lookups beyond variables, XCom, and 
connections,
   // make not-found handling operation-specific instead of treating every
   // API_SERVER_ERROR 404 as null.
   return err.code === "API_SERVER_ERROR" && err.statusCode === 404;
diff --git a/ts-sdk/src/coordinator/protocol.ts 
b/ts-sdk/src/coordinator/protocol.ts
index 44d0aee1723..323765036ea 100644
--- a/ts-sdk/src/coordinator/protocol.ts
+++ b/ts-sdk/src/coordinator/protocol.ts
@@ -59,6 +59,8 @@ export type {
   XComResult,
   ConnectionResult,
   GetVariable,
+  PutVariable,
+  DeleteVariable,
   GetXCom,
   SetXCom,
   GetConnection,
diff --git a/ts-sdk/src/sdk/client.ts b/ts-sdk/src/sdk/client.ts
index 8d63eee49c3..0facdd9695c 100644
--- a/ts-sdk/src/sdk/client.ts
+++ b/ts-sdk/src/sdk/client.ts
@@ -47,6 +47,24 @@ export interface TaskClient {
    */
   getVariableOrThrow(key: string): Promise<string>;
 
+  /**
+   * Store an Airflow Variable, replacing any existing value.
+   *
+   * The value is stored as a string. Serialize structured data (for example
+   * with `JSON.stringify`) before storing it.
+   *
+   * Omitting `description` clears the description the Variable had.
+   */
+  setVariable(key: string, value: string, description?: string | null): 
Promise<void>;
+
+  /**
+   * Delete an Airflow Variable.
+   *
+   * Resolves even when the key does not exist — the Execution API's delete
+   * route is idempotent and does not report a missing key as an error.
+   */
+  deleteVariable(key: string): Promise<void>;
+
   /**
    * Pull an XCom value.
    *
diff --git a/ts-sdk/tests/coordinator/client.test.ts 
b/ts-sdk/tests/coordinator/client.test.ts
index 3509bdf9136..2934eec32f7 100644
--- a/ts-sdk/tests/coordinator/client.test.ts
+++ b/ts-sdk/tests/coordinator/client.test.ts
@@ -21,6 +21,7 @@ import { describe, it, expect } from "vitest";
 import { ConnectionNotFoundError } from "../../src/sdk/client.js";
 import { createCoordinatorClient } from "../../src/coordinator/client.js";
 import type { CommChannel } from "../../src/coordinator/comm-channel.js";
+import type { TaskClient } from "../../src/sdk/client.js";
 import type { TaskContext } from "../../src/sdk/task.js";
 
 function fakeComm(frames: { body: unknown; error?: unknown }[]): CommChannel {
@@ -43,6 +44,18 @@ function client(frames: { body: unknown; error?: unknown 
}[]) {
   return createCoordinatorClient(fakeComm(frames), FAKE_CTX);
 }
 
+/** Captures the request bodies sent, answering each one with `reply`. */
+function recordingClient(reply: unknown = null) {
+  const sent: Record<string, unknown>[] = [];
+  const comm = {
+    request: async (body: Record<string, unknown>) => {
+      sent.push(body);
+      return { body: reply };
+    },
+  } as unknown as CommChannel;
+  return { client: createCoordinatorClient(comm, FAKE_CTX), sent };
+}
+
 describe("getVariable not-found contract", () => {
   it("returns null for the exact VARIABLE_NOT_FOUND code", async () => {
     const c = client([{ body: { type: "ErrorResponse", error: 
"VARIABLE_NOT_FOUND" } }]);
@@ -83,6 +96,57 @@ describe("getVariableOrThrow", () => {
   });
 });
 
+describe("setVariable", () => {
+  it("sends the description the caller gave", async () => {
+    const { client: c, sent } = recordingClient();
+
+    await c.setVariable("threshold", "42", "rows above this take the slow 
path");
+
+    expect(sent[0]).toEqual({
+      type: "PutVariable",
+      key: "threshold",
+      value: "42",
+      description: "rows above this take the slow path",
+    });
+  });
+
+  it("sends description as null when the caller gives none", async () => {
+    const { client: c, sent } = recordingClient();
+
+    await c.setVariable("threshold", "42");
+
+    expect(sent[0]).toEqual({
+      type: "PutVariable",
+      key: "threshold",
+      value: "42",
+      description: null,
+    });
+  });
+});
+
+describe("deleteVariable", () => {
+  it("sends DeleteVariable and resolves on the supervisor's OKResponse", async 
() => {
+    const { client: c, sent } = recordingClient({ type: "OKResponse", ok: true 
});
+
+    await expect(c.deleteVariable("threshold")).resolves.toBeUndefined();
+
+    expect(sent[0]).toEqual({ type: "DeleteVariable", key: "threshold" });
+  });
+});
+
+describe("writes do not read a supervisor 404 as absence", () => {
+  it.each([
+    ["setVariable", "PutVariable", (c: TaskClient) => c.setVariable("k", "v")],
+    ["deleteVariable", "DeleteVariable", (c: TaskClient) => 
c.deleteVariable("k")],
+    ["setXCom", "SetXCom", (c: TaskClient) => c.setXCom({ key: "k", value: 1 
})],
+  ])("%s rejects", async (_name, op, call) => {
+    const c = client([
+      { body: null, error: { error: "API_SERVER_ERROR", detail: { status_code: 
404 } } },
+    ]);
+    await expect(call(c)).rejects.toThrow(`${op} failed: API_SERVER_ERROR`);
+  });
+});
+
 describe("getXCom not-found contract", () => {
   it("returns null for the exact XCOM_NOT_FOUND code", async () => {
     const c = client([{ body: { type: "ErrorResponse", error: "XCOM_NOT_FOUND" 
} }]);
diff --git a/ts-sdk/tests/public-api.test.ts b/ts-sdk/tests/public-api.test.ts
index 4b8d6be8171..2a86f09d669 100644
--- a/ts-sdk/tests/public-api.test.ts
+++ b/ts-sdk/tests/public-api.test.ts
@@ -347,6 +347,10 @@ describe("public API", () => {
     expectTypeOf<TaskClient["getXCom"]>().toEqualTypeOf<
       <T = unknown>(opts: GetXComOpts) => Promise<T | null>
     >();
+    expectTypeOf<TaskClient["setVariable"]>().toEqualTypeOf<
+      (key: string, value: string, description?: string | null) => 
Promise<void>
+    >();
+    expectTypeOf<TaskClient["deleteVariable"]>().toEqualTypeOf<(key: string) 
=> Promise<void>>();
   });
 
   it("rejects wire-format names and non-JSON XCom values", () => {

Reply via email to