From 8e5f0a82b0a25820b9d45e857db7172ef0411c24 Mon Sep 17 00:00:00 2001 From: Mike Orozco Date: Wed, 22 Jul 2026 20:05:38 -0500 Subject: [PATCH] fix: restore Zcode 3.4.2 task compatibility --- README.md | 8 +- docs/extension-development.md | 4 +- package.json | 2 +- scripts/check-sdk-package.ts | 2 +- sdk/package.json | 2 +- src/host/bootstrap.ts | 2 +- src/protocol/desktop-service.ts | 7 +- src/protocol/task-service.ts | 96 ++++++++++++----- src/shared/constants.ts | 2 +- tests/fixtures/serve-renderer-harness.ts | 2 +- tests/task-service.test.ts | 132 +++++++++++++++++++++-- 11 files changed, 211 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index da094f4..f337a64 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Release executables are currently unsigned. Windows SmartScreen may warn before 3. Verify the archive: ```powershell - $zip = ".\zcode-extensions-v0.3.4-windows-x64.zip" + $zip = ".\zcode-extensions-v0.3.5-windows-x64.zip" $expected = (Get-Content "$zip.sha256").Split()[0].ToLowerInvariant() $actual = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $expected) { throw "Checksum mismatch" } @@ -52,7 +52,7 @@ Release executables are currently unsigned. Windows SmartScreen may warn before 4. Extract the archive to a permanent location. The ZIP contains a stable `zcode-extensions` directory: ```powershell - Expand-Archive .\zcode-extensions-v0.3.4-windows-x64.zip -DestinationPath D:\ + Expand-Archive .\zcode-extensions-v0.3.5-windows-x64.zip -DestinationPath D:\ Set-Location D:\zcode-extensions ``` @@ -110,7 +110,7 @@ Queued updates replace only the extension bundle on startup. The previous bundle Close ZCode, download the new release, and extract it over the same parent directory: ```powershell -Expand-Archive .\zcode-extensions-v0.3.4-windows-x64.zip -DestinationPath D:\ -Force +Expand-Archive .\zcode-extensions-v0.3.5-windows-x64.zip -DestinationPath D:\ -Force Set-Location D:\zcode-extensions .\bin\zdp.exe repair .\bin\zdp.exe launch @@ -181,7 +181,7 @@ bun run build:example bun run build bun run build:sdk bun run pack:sdk -bun run release:package -- --tag v0.3.4 +bun run release:package -- --tag v0.3.5 ``` See [Developing extensions](docs/extension-development.md) for the public API and [Hello Extension](examples/hello-extension) for a complete minimal project. diff --git a/docs/extension-development.md b/docs/extension-development.md index 81fee08..91cb0a0 100644 --- a/docs/extension-development.md +++ b/docs/extension-development.md @@ -1,6 +1,6 @@ # Developing ZCode Desktop Extensions -This guide describes extension API version 1 as implemented by host/SDK 0.3.4. The public package is [`@notmike101/zcode-extension-sdk`](https://www.npmjs.com/package/@notmike101/zcode-extension-sdk), the source contract is [`sdk/index.ts`](../sdk/index.ts), and [Hello Extension](../examples/hello-extension) remains a complete legacy-lifecycle example. +This guide describes extension API version 1 as implemented by host/SDK 0.3.5. The public package is [`@notmike101/zcode-extension-sdk`](https://www.npmjs.com/package/@notmike101/zcode-extension-sdk), the source contract is [`sdk/index.ts`](../sdk/index.ts), and [Hello Extension](../examples/hello-extension) remains a complete legacy-lifecycle example. Extensions are trusted local code. A main entrypoint runs in ZCode's Electron main process with Node.js access, while an optional renderer entrypoint runs inside the ZCode renderer. Declared capabilities control access through the SDK and are shown during installation; they do not sandbox trusted Node or renderer code. @@ -9,7 +9,7 @@ Extensions are trusted local code. A main entrypoint runs in ZCode's Electron ma For a separate Bun or TypeScript project: ```powershell -bun add -d @notmike101/zcode-extension-sdk@0.3.4 +bun add -d @notmike101/zcode-extension-sdk@0.3.5 ``` Import main-only types and helpers from `@notmike101/zcode-extension-sdk/main`, browser-safe renderer types and helpers from `/renderer`, and unstable raw-channel types from `/experimental`. The root export is also browser-safe. A JSON Schema is available at `/manifest.schema.json`, and `validateExtensionManifest` or `assertExtensionManifest` can validate manifests at runtime. diff --git a/package.json b/package.json index 8571e4c..136ebc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zcode-desktop-extensions", - "version": "0.3.4", + "version": "0.3.5", "description": "An update-resistant extension host for the ZCode Electron desktop application.", "private": true, "license": "MIT", diff --git a/scripts/check-sdk-package.ts b/scripts/check-sdk-package.ts index fc20dbb..98dc247 100644 --- a/scripts/check-sdk-package.ts +++ b/scripts/check-sdk-package.ts @@ -11,7 +11,7 @@ const packageJson = JSON.parse(await readFile(path.join(sdk, "package.json"), "u exports: Record; }; -if (packageJson.name !== "@notmike101/zcode-extension-sdk" || packageJson.version !== "0.3.4") { +if (packageJson.name !== "@notmike101/zcode-extension-sdk" || packageJson.version !== "0.3.5") { throw new Error("Unexpected SDK package identity"); } for (const entry of [".", "./main", "./renderer", "./experimental", "./manifest.schema.json"]) { diff --git a/sdk/package.json b/sdk/package.json index 23414ec..f92ad6a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@notmike101/zcode-extension-sdk", - "version": "0.3.4", + "version": "0.3.5", "description": "Public TypeScript SDK and authoring helpers for ZCode Desktop Extensions.", "license": "MIT", "type": "module", diff --git a/src/host/bootstrap.ts b/src/host/bootstrap.ts index d447f8a..863ee9d 100644 --- a/src/host/bootstrap.ts +++ b/src/host/bootstrap.ts @@ -52,7 +52,7 @@ const zcodeGateway = new ZCodeGateway({ emit("host-state-changed"); }, }); -const taskService = new TaskService({gateway: zcodeGateway, logger}); +const taskService = new TaskService({gateway: zcodeGateway, logger, zcodeVersion}); const zcodeService = new ExtensionZCodeService({gateway: zcodeGateway, taskService, hostVersion: HOST_VERSION, zcodeVersion}); function emit(event: string, payload?: unknown): void { diff --git a/src/protocol/desktop-service.ts b/src/protocol/desktop-service.ts index cad1508..2cd96a2 100644 --- a/src/protocol/desktop-service.ts +++ b/src/protocol/desktop-service.ts @@ -1,3 +1,4 @@ +import {randomUUID} from "node:crypto"; import {readdir, readFile} from "node:fs/promises"; import path from "node:path"; import {pathToFileURL} from "node:url"; @@ -93,7 +94,11 @@ export class DesktopServicePortBroker { queueMicrotask(() => { const {port1, port2} = new MessageChannelMain(); try { - originalPostMessage({type: "attach-service-port"}, [port2]); + originalPostMessage({ + type: "attach-service-port", + attachmentId: `zdp-${randomUUID()}`, + clientMode: "desktop-continuous", + }, [port2]); this.#attach({port: port1, process: child}); } catch (error) { closePort(port1); diff --git a/src/protocol/task-service.ts b/src/protocol/task-service.ts index 3c1dafc..c299ef6 100644 --- a/src/protocol/task-service.ts +++ b/src/protocol/task-service.ts @@ -1,6 +1,7 @@ import {randomUUID} from "node:crypto"; import {EventEmitter} from "node:events"; import path from "node:path"; +import semver from "semver"; import type {JsonLogger} from "../shared/logger.ts"; import {taskSpecSchema, type TaskSpec} from "../shared/schemas.ts"; import type { @@ -36,6 +37,7 @@ type TaskServiceOptions = { vendorAsar?: string; portBroker?: Pick; logger: JsonLogger; + zcodeVersion?: string; onHealth?: (status: "idle" | "starting" | "ready" | "error", error?: string) => void; connect?: (port: DesktopServicePort["port"], vendorAsar: string) => Promise; }; @@ -61,6 +63,7 @@ type TaskIndexService = { createTask: (input: Record) => Promise; getTaskMeta: (input: Record) => Promise; renameTask: (input: Record) => Promise; + sendPrompt: (input: Record) => Promise; }; type BroadcastService = { @@ -141,9 +144,16 @@ export class TaskService extends EventEmitter { const spec = taskSpecSchema.parse(input); const {broadcast, session, task} = await this.#services(); const target = workspaceTarget(spec.workspacePath); + const useV4TaskFacade = supportsV4TaskFacade(this.#options.zcodeVersion); + await this.#options.logger.info("Creating native ZCode task", { + workspacePath: spec.workspacePath, + zcodeVersion: this.#options.zcodeVersion, + bridge: useV4TaskFacade ? "task-facade-v4" : "legacy-session", + }); const createdTask = asRecord(await task.createTask({ ...target, mode: spec.mode, + ...(useV4TaskFacade ? {v4Create: true} : {}), ...(spec.model ? {model: spec.model} : {}), ...(spec.thoughtLevel ? {thoughtLevel: spec.thoughtLevel} : {}), ...(spec.toolAllowlist ? {toolAllowlist: spec.toolAllowlist} : {}), @@ -151,20 +161,22 @@ export class TaskService extends EventEmitter { })); const sessionId = stringValue(createdTask.taskId); if (!sessionId) throw new Error("ZCode did not return a task ID"); + await this.#options.logger.info("Native ZCode task created", { + sessionId, + bridge: useV4TaskFacade ? "task-facade-v4" : "legacy-session", + }); - const inputId = randomUUID(); + const traceId = stringValue(createdTask.traceId) ?? randomUUID(); + const inputId = useV4TaskFacade ? traceId : randomUUID(); const queryId = randomUUID(); const messageId = randomUUID(); let resolveCompletion!: (result: TaskResult) => void; const completion = new Promise((resolve) => { resolveCompletion = resolve; }); let settled = false; - const subscriptionTarget = { - ...target, - sessionId, - deliveryKind: "desktop-continuous", - includeSnapshot: true, - }; + const subscriptionTarget = useV4TaskFacade + ? {...target, taskId: sessionId, deliveryKind: "desktop-continuous"} + : {...target, sessionId, deliveryKind: "desktop-continuous", includeSnapshot: true}; const run = {} as ActiveRun; const finish = (result: TaskResult) => { if (settled) return; @@ -187,8 +199,8 @@ export class TaskService extends EventEmitter { interactionBlocked: false, finish, subscription: this.#gateway.subscribe( - "zcode-session", - "onDynamicSessionEvent", + useV4TaskFacade ? "zcode-task" : "zcode-session", + useV4TaskFacade ? "onDynamicTaskEvent" : "onDynamicSessionEvent", subscriptionTarget, (value) => this.#onSessionEvent(run, value), ), @@ -200,18 +212,38 @@ export class TaskService extends EventEmitter { ? await this.#rename(task, target, sessionId, run.title) ?? createdTask : createdTask; await this.#broadcastTaskChange(broadcast, target, sessionId, "created", {task: visibleTask}); - await session.sendPrompt({ - ...target, - sessionId, - inputId, - queryId, - messageId, - content: spec.prompt, - }); - if (run.title) await this.#rename(task, target, sessionId, run.title); - await this.#broadcastTaskChange(broadcast, target, sessionId, "prompt_sent", { + if (useV4TaskFacade) { + await this.#options.logger.info("Submitting prompt through ZCode task facade", {sessionId, traceId}); + await task.sendPrompt({ + taskId: sessionId, + traceId, + queryId, + messageId, + content: spec.prompt, + }); + await this.#options.logger.info("ZCode task facade accepted prompt", {sessionId, traceId}); + } else { + await session.sendPrompt({ + ...target, + sessionId, + inputId, + queryId, + messageId, + content: spec.prompt, + }); + } + const promptSent = () => this.#broadcastTaskChange(broadcast, target, sessionId, "prompt_sent", { prompt: {content: spec.prompt, messageId, sentAt: Date.now()}, }); + if (useV4TaskFacade) { + // ZCode 3.4.2 may leave follow-up task metadata calls pending after the + // task facade accepts a prompt. The run is active at this point, so do + // not keep extension callers (and their UI) waiting on notifications. + void promptSent(); + } else { + if (run.title) await this.#rename(task, target, sessionId, run.title); + await promptSent(); + } } catch (error) { this.#runs.delete(sessionId); run.subscription.dispose(); @@ -287,13 +319,14 @@ export class TaskService extends EventEmitter { run.interactionBlocked = true; return; } - if (envelopeType !== "session.event") return; - const event = asRecord(envelope.event); + const event = envelopeType === "session.event" ? asRecord(envelope.event) : envelope; const eventType = stringValue(event.type); + if (!eventType) return; const payload = asRecord(event.payload); - const eventInputId = stringValue(payload.inputId); + const eventInputId = stringValue(payload.inputId) ?? stringValue(event.inputId); if (eventInputId && eventInputId !== run.inputId) return; - if (eventType === "permission.requested" || eventType === "elicitation.requested") { + if (eventType === "permission.requested" || eventType === "elicitation.requested" + || eventType === "permission_request" || eventType === "elicitation_request") { run.interactionBlocked = true; return; } @@ -301,17 +334,20 @@ export class TaskService extends EventEmitter { void this.#services().then(({task}) => this.#rename(task, run.target, run.sessionId, run.title!)); return; } - if (eventType === "turn.completed") { - const resultType = stringValue(payload.resultType) ?? "success"; + if (eventType === "turn.completed" || eventType === "task_complete") { + const resultType = stringValue(payload.resultType) ?? stringValue(event.stopReason) ?? "success"; if (resultType === "cancelled") run.finish({sessionId: run.sessionId, status: "cancelled"}); - else if (resultType !== "success") run.finish({sessionId: run.sessionId, status: "failed", error: resultType}); + else if (resultType !== "success" && resultType !== "complete") { + run.finish({sessionId: run.sessionId, status: "failed", error: resultType}); + } else run.finish({sessionId: run.sessionId, status: run.interactionBlocked ? "needs_attention" : "succeeded"}); - } else if (eventType === "turn.failed") { + } else if (eventType === "turn.failed" || eventType === "task_error") { const error = asRecord(payload.error); run.finish({ sessionId: run.sessionId, status: run.interactionBlocked ? "needs_attention" : "failed", - error: stringValue(error.message) ?? stringValue(error.detail) ?? "ZCode session turn failed", + error: stringValue(error.message) ?? stringValue(error.detail) + ?? stringValue(event.error) ?? stringValue(event.detail) ?? "ZCode session turn failed", }); } } @@ -374,3 +410,7 @@ function asRecord(value: unknown): Record { function stringValue(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } + +function supportsV4TaskFacade(version: string | undefined): boolean { + return Boolean(version && semver.valid(version) && semver.gte(version, "3.4.2")); +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts index dd5fb66..28edf5b 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,7 +1,7 @@ import path from "node:path"; export const HOST_NAME = "ZCode Desktop Extensions"; -export const HOST_VERSION = "0.3.4"; +export const HOST_VERSION = "0.3.5"; export const API_VERSION = 1; export const INSTALL_STATE_VERSION = 1; export const DEFAULT_ZCODE_ROOT = path.join( diff --git a/tests/fixtures/serve-renderer-harness.ts b/tests/fixtures/serve-renderer-harness.ts index a449372..f627ae5 100644 --- a/tests/fixtures/serve-renderer-harness.ts +++ b/tests/fixtures/serve-renderer-harness.ts @@ -15,7 +15,7 @@ const server = Bun.serve({ }); } if (url.pathname === "/renderer/index.js") { - return new Response(await readFile(path.join(root, "runtime", "versions", "0.3.4", "renderer", "index.js")), { + return new Response(await readFile(path.join(root, "runtime", "versions", "0.3.5", "renderer", "index.js")), { headers: {"content-type": "text/javascript; charset=utf-8"}, }); } diff --git a/tests/task-service.test.ts b/tests/task-service.test.ts index e3627ed..0e4b323 100644 --- a/tests/task-service.test.ts +++ b/tests/task-service.test.ts @@ -1,4 +1,6 @@ import {describe, expect, test} from "bun:test"; +import os from "node:os"; +import path from "node:path"; import type {DesktopServiceConnection, DesktopServicePort} from "../src/protocol/desktop-service.ts"; import {TaskService} from "../src/protocol/task-service.ts"; import {JsonLogger} from "../src/shared/logger.ts"; @@ -24,6 +26,11 @@ describe("native desktop task service", () => { sessionId: "session-1", content: "Review open work", }); + expect(harness.subscriptions[0]).toMatchObject({ + channel: "zcode-session", + event: "onDynamicSessionEvent", + argument: {sessionId: "session-1", deliveryKind: "desktop-continuous", includeSnapshot: true}, + }); expect(harness.renamed).toHaveLength(2); expect(harness.renamed[0]).toMatchObject({taskId: "session-1", title: "⏰ Morning review"}); expect(harness.broadcasts.slice(0, 2).map((entry) => entry.payload.event)).toEqual(["created", "prompt_sent"]); @@ -77,21 +84,121 @@ describe("native desktop task service", () => { await expect(run.completion).resolves.toEqual({sessionId: "session-1", status: "needs_attention"}); await harness.service.shutdown(); }); + + test("uses the ZCode 3.4.2 V4 task facade and accepts direct terminal events", async () => { + const harness = createHarness("3.4.2"); + const handle = await harness.service.run({ + workspacePath: "D:\\project", + prompt: "Reply with ready", + title: "V4 task", + mode: "plan", + }); + + expect(harness.created[0]).toMatchObject({workspacePath: "D:\\project", mode: "plan", v4Create: true}); + expect(harness.sentVia).toEqual(["task"]); + expect(harness.sent[0]).toMatchObject({ + taskId: "session-1", + traceId: "trace-1", + content: "Reply with ready", + }); + expect(harness.sent[0]).not.toHaveProperty("sessionId"); + expect(harness.subscriptions).toEqual([{ + channel: "zcode-task", + event: "onDynamicTaskEvent", + argument: { + workspacePath: "D:\\project", + taskId: "session-1", + deliveryKind: "desktop-continuous", + }, + }]); + + harness.emit({type: "task_complete", inputId: "trace-1", stopReason: "complete"}); + await expect(handle.completion).resolves.toEqual({sessionId: "session-1", status: "succeeded"}); + expect(harness.subscriptionDisposed).toBe(1); + await harness.service.shutdown(); + }); + + test("returns the V4 run handle immediately after prompt acceptance", async () => { + const harness = createHarness("3.4.2", true); + const handle = await Promise.race([ + harness.service.run({workspacePath: "D:\\project", prompt: "Reply with ready", title: "V4 task", mode: "plan"}), + new Promise((_, reject) => setTimeout(() => reject(new Error("run handle did not return")), 100)), + ]); + + expect(handle.sessionId).toBe("session-1"); + expect(harness.sentVia).toEqual(["task"]); + harness.emit({type: "task_complete", inputId: "trace-1", stopReason: "complete"}); + await expect(handle.completion).resolves.toEqual({sessionId: "session-1", status: "succeeded"}); + await harness.service.shutdown(); + }); + + test("maps direct V4 interaction, failure, and cancellation outcomes", async () => { + const attention = createHarness("3.4.2"); + const attentionRun = await attention.service.run({workspacePath: "D:\\project", prompt: "Ask", mode: "plan"}); + attention.emit({type: "permission_request"}); + attention.emit({type: "task_complete", inputId: "trace-1", stopReason: "complete"}); + await expect(attentionRun.completion).resolves.toEqual({sessionId: "session-1", status: "needs_attention"}); + await attention.service.shutdown(); + + const failed = createHarness("3.4.2"); + const failedRun = await failed.service.run({workspacePath: "D:\\project", prompt: "Fail", mode: "plan"}); + failed.emit({type: "task_error", inputId: "trace-1", error: "provider unavailable"}); + await expect(failedRun.completion).resolves.toEqual({ + sessionId: "session-1", + status: "failed", + error: "provider unavailable", + }); + await failed.service.shutdown(); + + const cancelled = createHarness("3.4.2"); + const cancelledRun = await cancelled.service.run({workspacePath: "D:\\project", prompt: "Wait", mode: "plan"}); + cancelled.emit({type: "task_complete", inputId: "trace-1", stopReason: "cancelled"}); + await expect(cancelledRun.completion).resolves.toEqual({sessionId: "session-1", status: "cancelled"}); + + const stopped = createHarness("3.4.2"); + const stoppedRun = await stopped.service.run({workspacePath: "D:\\project", prompt: "Wait", mode: "plan"}); + await stoppedRun.stop(); + await expect(stoppedRun.completion).resolves.toEqual({sessionId: "session-1", status: "cancelled"}); + expect(stopped.stopped[0]).toMatchObject({sessionId: "session-1", workspacePath: "D:\\project"}); + await cancelled.service.shutdown(); + await stopped.service.shutdown(); + }); + + test("stops and cleans up a V4 run after its timeout", async () => { + const harness = createHarness("3.4.2"); + const run = await harness.service.run({ + workspacePath: "D:\\project", + prompt: "Wait", + mode: "plan", + timeoutMs: 5, + }); + + await expect(run.completion).resolves.toEqual({ + sessionId: "session-1", + status: "timed_out", + error: "Task exceeded 5 ms", + }); + expect(harness.stopped[0]).toMatchObject({sessionId: "session-1", workspacePath: "D:\\project"}); + expect(harness.subscriptionDisposed).toBe(1); + await harness.service.shutdown(); + }); }); -function createHarness() { +function createHarness(zcodeVersion = "3.3.6", blockPostAcceptanceCalls = false) { const created: Array> = []; const sent: Array> = []; const resumed: Array> = []; const renamed: Array> = []; const stopped: Array> = []; const broadcasts: Array<{channel: string; payload: Record}> = []; + const sentVia: string[] = []; + const subscriptions: Array<{channel: string; event: string; argument: unknown}> = []; let listener: (event: unknown) => void = () => undefined; let subscriptionDisposed = 0; const session = { async resumeSession(input: Record) { resumed.push(input); return {session: {sessionId: input.sessionId}}; }, async readWorkspaceState() { return {}; }, - async sendPrompt(input: Record) { sent.push(input); }, + async sendPrompt(input: Record) { sentVia.push("session"); sent.push(input); }, async stopSession(input: Record) { stopped.push(input); }, onDynamicSessionEvent() { return (next: (event: unknown) => void) => { @@ -103,8 +210,9 @@ function createHarness() { const task = { async createTask(input: Record) { created.push(input); - return {taskId: input.draftSessionId ?? "session-1", workspacePath: input.workspacePath, title: "Untitled"}; + return {taskId: input.draftSessionId ?? "session-1", traceId: "trace-1", workspacePath: input.workspacePath, title: "Untitled"}; }, + async sendPrompt(input: Record) { sentVia.push("task"); sent.push(input); }, async getTaskMeta() { return undefined; }, async renameTask(input: Record) { renamed.push(input); @@ -112,7 +220,12 @@ function createHarness() { }, }; const broadcast = { - async send(input: {channel: string; payload: Record}) { broadcasts.push(input); }, + async send(input: {channel: string; payload: Record}) { + broadcasts.push(input); + if (blockPostAcceptanceCalls && input.payload.event === "prompt_sent") { + await new Promise((resolve) => setTimeout(resolve, 250)); + } + }, }; const entry = {port: {} as DesktopServicePort["port"], process: {} as DesktopServicePort["process"]}; const broker = { @@ -130,8 +243,10 @@ function createHarness() { channel: (name: keyof typeof services) => ({ async call() { return undefined; }, listen(event: string, argument?: unknown) { - if (name !== "zcode-session" || event !== "onDynamicSessionEvent") throw new Error(`Unexpected listener ${name}:${event}`); - void argument; + const supported = (name === "zcode-session" && event === "onDynamicSessionEvent") + || (name === "zcode-task" && event === "onDynamicTaskEvent"); + if (!supported) throw new Error(`Unexpected listener ${name}:${event}`); + subscriptions.push({channel: name, event, argument}); return session.onDynamicSessionEvent(); }, }), @@ -140,13 +255,16 @@ function createHarness() { const service = new TaskService({ vendorAsar: "unused", portBroker: broker, - logger: new JsonLogger("unused-task-service-test.log", "test"), + logger: new JsonLogger(path.join(os.tmpdir(), "zdp-tests", "task-service.log"), "test"), + zcodeVersion, connect: async () => connection, }); return { service, created, sent, + sentVia, + subscriptions, resumed, renamed, stopped,