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", () => {