@nebutra/agent-runtime 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +21 -676
  2. package/README.md +2 -0
  3. package/dist/adapters/dispatcher-sse.js +1 -0
  4. package/dist/adapters/index.d.ts +24 -5
  5. package/dist/adapters/index.js +28 -0
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/adapters/mcp-catalog.js +1 -0
  8. package/dist/adapters/prisma-rollout.js +1 -0
  9. package/dist/chunk-424PT5DM.js +23 -0
  10. package/dist/chunk-424PT5DM.js.map +1 -0
  11. package/dist/{chunk-NN7DATXA.js → chunk-4Y25ZTKI.js} +3 -3
  12. package/dist/chunk-4Y25ZTKI.js.map +1 -0
  13. package/dist/{chunk-BJBBR3QA.js → chunk-D4YAPLOW.js} +4 -4
  14. package/dist/chunk-D4YAPLOW.js.map +1 -0
  15. package/dist/{chunk-ZMYX5VBU.js → chunk-GQZKYWFT.js} +23 -7
  16. package/dist/chunk-GQZKYWFT.js.map +1 -0
  17. package/dist/chunk-KCNN4QUQ.js +255 -0
  18. package/dist/chunk-KCNN4QUQ.js.map +1 -0
  19. package/dist/{chunk-PGGWSUTM.js → chunk-NI4EDT4T.js} +2 -2
  20. package/dist/chunk-NI4EDT4T.js.map +1 -0
  21. package/dist/chunk-Q62VKHIT.js +178 -0
  22. package/dist/chunk-Q62VKHIT.js.map +1 -0
  23. package/dist/{chunk-MUF7ZZTO.js → chunk-R5HOSQUW.js} +2 -1
  24. package/dist/{chunk-MUF7ZZTO.js.map → chunk-R5HOSQUW.js.map} +1 -1
  25. package/dist/{chunk-5N4644PB.js → chunk-SD2ZJ7XG.js} +4 -4
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +52 -0
  28. package/dist/cli.js.map +1 -0
  29. package/dist/commands.js +1 -0
  30. package/dist/definitions.js +1 -0
  31. package/dist/dispatcher.js +1 -0
  32. package/dist/durable-turn.js +3 -2
  33. package/dist/hook-pipeline.js +1 -0
  34. package/dist/index.d.ts +8 -85
  35. package/dist/index.js +249 -134
  36. package/dist/index.js.map +1 -1
  37. package/dist/loop.d.ts +2 -2
  38. package/dist/loop.js +3 -2
  39. package/dist/mcp-bridge.d.ts +3 -3
  40. package/dist/mcp-bridge.js +3 -2
  41. package/dist/model.js +1 -0
  42. package/dist/orchestration.d.ts +84 -0
  43. package/dist/orchestration.js +16 -0
  44. package/dist/orchestration.js.map +1 -0
  45. package/dist/policy.js +1 -0
  46. package/dist/protocol.js +1 -0
  47. package/dist/pulsar.d.ts +78 -0
  48. package/dist/pulsar.js +17 -0
  49. package/dist/pulsar.js.map +1 -0
  50. package/dist/rollout-store-persistent.js +1 -0
  51. package/dist/rollout.js +1 -0
  52. package/dist/sandbox.js +2 -1
  53. package/dist/skills.js +2 -1
  54. package/dist/subagents.js +1 -0
  55. package/dist/tools.d.ts +4 -3
  56. package/dist/tools.js +5 -3
  57. package/package.json +84 -27
  58. package/.turbo/turbo-build.log +0 -115
  59. package/.turbo/turbo-test.log +0 -44
  60. package/.turbo/turbo-typecheck.log +0 -4
  61. package/CHANGELOG.md +0 -253
  62. package/dist/chunk-BJBBR3QA.js.map +0 -1
  63. package/dist/chunk-NN7DATXA.js.map +0 -1
  64. package/dist/chunk-PGGWSUTM.js.map +0 -1
  65. package/dist/chunk-ZMYX5VBU.js.map +0 -1
  66. package/src/adapters/dispatcher-sse.test.ts +0 -218
  67. package/src/adapters/dispatcher-sse.ts +0 -222
  68. package/src/adapters/index.ts +0 -18
  69. package/src/adapters/mcp-catalog.test.ts +0 -213
  70. package/src/adapters/mcp-catalog.ts +0 -188
  71. package/src/adapters/prisma-rollout.test.ts +0 -153
  72. package/src/adapters/prisma-rollout.ts +0 -104
  73. package/src/agent-runtime.test.ts +0 -176
  74. package/src/artifact-stream.test.ts +0 -330
  75. package/src/artifact-stream.ts +0 -453
  76. package/src/channel-gateway.test.ts +0 -432
  77. package/src/channel-gateway.ts +0 -357
  78. package/src/code-review.test.ts +0 -501
  79. package/src/code-review.ts +0 -495
  80. package/src/command-suggestions.test.ts +0 -251
  81. package/src/command-suggestions.ts +0 -338
  82. package/src/commands.test.ts +0 -184
  83. package/src/commands.ts +0 -140
  84. package/src/commit-message.test.ts +0 -249
  85. package/src/commit-message.ts +0 -180
  86. package/src/context-compaction.test.ts +0 -522
  87. package/src/context-compaction.ts +0 -434
  88. package/src/definitions.test.ts +0 -78
  89. package/src/definitions.ts +0 -190
  90. package/src/deployment-status.test.ts +0 -215
  91. package/src/deployment-status.ts +0 -227
  92. package/src/design-context.test.ts +0 -195
  93. package/src/design-context.ts +0 -198
  94. package/src/dispatcher.test.ts +0 -234
  95. package/src/dispatcher.ts +0 -189
  96. package/src/durable-turn.test.ts +0 -209
  97. package/src/durable-turn.ts +0 -135
  98. package/src/edit-planner.test.ts +0 -204
  99. package/src/edit-planner.ts +0 -325
  100. package/src/fuzzy-match.test.ts +0 -311
  101. package/src/fuzzy-match.ts +0 -444
  102. package/src/hook-pipeline.test.ts +0 -279
  103. package/src/hook-pipeline.ts +0 -373
  104. package/src/inbound-admission.test.ts +0 -394
  105. package/src/inbound-admission.ts +0 -246
  106. package/src/index.ts +0 -47
  107. package/src/loop.test.ts +0 -161
  108. package/src/loop.ts +0 -211
  109. package/src/mcp-bridge.test.ts +0 -165
  110. package/src/mcp-bridge.ts +0 -76
  111. package/src/memory-provider.test.ts +0 -232
  112. package/src/memory-provider.ts +0 -257
  113. package/src/model.ts +0 -168
  114. package/src/permission-ruleset.test.ts +0 -301
  115. package/src/permission-ruleset.ts +0 -200
  116. package/src/policy.ts +0 -151
  117. package/src/project-repo.test.ts +0 -232
  118. package/src/project-repo.ts +0 -311
  119. package/src/protocol.ts +0 -159
  120. package/src/rollout-store-persistent.test.ts +0 -217
  121. package/src/rollout-store-persistent.ts +0 -166
  122. package/src/rollout.ts +0 -150
  123. package/src/sandbox.ts +0 -113
  124. package/src/session-share.test.ts +0 -360
  125. package/src/session-share.ts +0 -310
  126. package/src/skill-distillation.test.ts +0 -177
  127. package/src/skill-distillation.ts +0 -369
  128. package/src/skills.test.ts +0 -277
  129. package/src/skills.ts +0 -255
  130. package/src/subagents.test.ts +0 -290
  131. package/src/subagents.ts +0 -332
  132. package/src/tools.ts +0 -126
  133. package/src/workbench.test.ts +0 -0
  134. package/src/workbench.ts +0 -0
  135. package/tsconfig.json +0 -12
  136. package/tsup.config.ts +0 -33
  137. /package/dist/{chunk-5N4644PB.js.map → chunk-SD2ZJ7XG.js.map} +0 -0
@@ -1,153 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { type ApprovalGate, type ModelInvoker, runTurn } from "../loop";
3
- import { type RolloutLine, replay } from "../rollout";
4
- import { PersistentRolloutStore } from "../rollout-store-persistent";
5
- import { ToolRegistry } from "../tools";
6
- import { createPrismaRolloutPersistence, type PrismaRolloutDelegate } from "./prisma-rollout.js";
7
-
8
- /** In-memory fake mirroring the agent_rollout_lines table semantics. */
9
- function fakeDelegate() {
10
- const rows: { tenantId: string; threadId: string; seq: number; at: Date; payload: string }[] = [];
11
- const delegate: PrismaRolloutDelegate = {
12
- async create({ data }) {
13
- if (
14
- rows.some(
15
- (r) => r.tenantId === data.tenantId && r.threadId === data.threadId && r.seq === data.seq,
16
- )
17
- ) {
18
- // Mirrors the unique (tenant,thread,seq) constraint — fail loud.
19
- throw new Error("unique constraint violation");
20
- }
21
- rows.push({ ...data });
22
- return data;
23
- },
24
- async findMany({ where }) {
25
- return rows
26
- .filter((r) => r.tenantId === where.tenantId && r.threadId === where.threadId)
27
- .sort((a, b) => a.seq - b.seq)
28
- .map((r) => ({ seq: r.seq, payload: r.payload }));
29
- },
30
- };
31
- return { delegate, rows };
32
- }
33
-
34
- const line = (tenantId: string, threadId: string): RolloutLine => ({
35
- tenantId,
36
- threadId,
37
- type: "session_meta",
38
- config: {
39
- model: "m",
40
- provider: "p",
41
- approvalPolicy: "on_request",
42
- capabilityPolicy: "external_sandbox",
43
- },
44
- at: new Date().toISOString(),
45
- });
46
-
47
- describe("createPrismaRolloutPersistence", () => {
48
- it("round-trips through PersistentRolloutStore and replays faithfully", async () => {
49
- const { delegate } = fakeDelegate();
50
- const store = new PersistentRolloutStore(createPrismaRolloutPersistence(delegate));
51
- await store.append(line("org_a", "th_1"));
52
- await store.append({
53
- tenantId: "org_a",
54
- threadId: "th_1",
55
- type: "event",
56
- at: new Date().toISOString(),
57
- event: { type: "item.completed", item: { id: "i1", type: "agent_message", text: "hi" } },
58
- });
59
- const proj = replay(await store.read("org_a", "th_1"));
60
- expect(proj?.items.map((i) => i.id)).toEqual(["i1"]);
61
- });
62
-
63
- it("isolates tenants — B cannot read A's thread", async () => {
64
- const { delegate } = fakeDelegate();
65
- const store = new PersistentRolloutStore(createPrismaRolloutPersistence(delegate));
66
- await store.append(line("org_a", "th_1"));
67
- expect(await store.read("org_b", "th_1")).toHaveLength(0);
68
- });
69
-
70
- it("fails loud on a persistence rejection (never silently drops)", async () => {
71
- const failing: PrismaRolloutDelegate = {
72
- async create() {
73
- throw new Error("db down");
74
- },
75
- async findMany() {
76
- return [];
77
- },
78
- };
79
- const port = createPrismaRolloutPersistence(failing);
80
- await expect(
81
- port.put({
82
- tenantId: "t",
83
- threadId: "th",
84
- seq: 0,
85
- at: new Date().toISOString(),
86
- payload: "{}",
87
- }),
88
- ).rejects.toThrow("db down");
89
- });
90
-
91
- it("fails closed on empty tenant / bad timestamp", async () => {
92
- const { delegate } = fakeDelegate();
93
- const port = createPrismaRolloutPersistence(delegate);
94
- await expect(
95
- port.put({ tenantId: "", threadId: "th", seq: 0, at: "x", payload: "{}" }),
96
- ).rejects.toBeTruthy();
97
- await expect(
98
- port.put({ tenantId: "t", threadId: "th", seq: 0, at: "not-a-date", payload: "{}" }),
99
- ).rejects.toThrow(/invalid timestamp/);
100
- });
101
-
102
- it("resolves a per-tenant delegate (RLS client per call)", async () => {
103
- const a = fakeDelegate();
104
- const b = fakeDelegate();
105
- const port = createPrismaRolloutPersistence((tenantId) =>
106
- tenantId === "org_a" ? a.delegate : b.delegate,
107
- );
108
- await port.put({
109
- tenantId: "org_a",
110
- threadId: "th",
111
- seq: 0,
112
- at: new Date().toISOString(),
113
- payload: "{}",
114
- });
115
- expect(a.rows).toHaveLength(1);
116
- expect(b.rows).toHaveLength(0);
117
- });
118
-
119
- it("drives a full turn whose rollout survives in the durable backend", async () => {
120
- const { delegate, rows } = fakeDelegate();
121
- const store = new PersistentRolloutStore(createPrismaRolloutPersistence(delegate));
122
- const model: ModelInvoker = {
123
- async invoke() {
124
- return { emissions: [{ kind: "text", text: "done" }] };
125
- },
126
- };
127
- const gate: ApprovalGate = {
128
- async request() {
129
- return { kind: "denied" };
130
- },
131
- };
132
- const events = [];
133
- for await (const e of runTurn("hi", {
134
- tenantId: "org_a",
135
- threadId: "th_x",
136
- config: {
137
- model: "m",
138
- provider: "p",
139
- approvalPolicy: "on_request",
140
- capabilityPolicy: "external_sandbox",
141
- },
142
- approvalPolicy: { kind: "on_request" },
143
- model,
144
- tools: new ToolRegistry(),
145
- store,
146
- approvalGate: gate,
147
- })) {
148
- events.push(e);
149
- }
150
- expect(rows.length).toBe(events.length);
151
- expect(rows.every((r) => r.tenantId === "org_a")).toBe(true);
152
- });
153
- });
@@ -1,104 +0,0 @@
1
- /**
2
- * Durable rollout-store backend: `RolloutPersistencePort` over Prisma.
3
- *
4
- * This is the fail-loud system-of-record adapter (table `agent_rollout_lines`,
5
- * migration `20260519000000_add_agent_rollout_store`). Unlike a best-effort
6
- * audit log it never swallows a failed write — a rejected `put` propagates so
7
- * the caller's "never silently drop" contract holds.
8
- *
9
- * Reusability: it depends only on a minimal injected delegate (the
10
- * `PrismaAuditDelegate` pattern), NOT on the generated Prisma client or
11
- * `@nebutra/db`. Any tenant-scoped Prisma client (`getTenantDb(orgId)`)
12
- * structurally satisfies it, so this package stays dependency-light.
13
- */
14
-
15
- import { z } from "zod";
16
- import type { RolloutPersistencePort } from "../rollout-store-persistent";
17
-
18
- /**
19
- * Minimal structural slice of `prisma.agentRolloutLine`. Decoupled from the
20
- * generated client on purpose — only these two operations are needed.
21
- */
22
- export interface PrismaRolloutDelegate {
23
- create(args: {
24
- data: {
25
- tenantId: string;
26
- threadId: string;
27
- seq: number;
28
- at: Date;
29
- payload: string;
30
- };
31
- }): Promise<unknown>;
32
- findMany(args: {
33
- where: { tenantId: string; threadId: string };
34
- orderBy: { seq: "asc" };
35
- select: { seq: true; payload: true };
36
- }): Promise<{ seq: number; payload: string }[]>;
37
- }
38
-
39
- /** Resolves the delegate per call so a request-scoped (RLS) client can be used. */
40
- export type RolloutDelegateResolver =
41
- | PrismaRolloutDelegate
42
- | ((tenantId: string) => PrismaRolloutDelegate | Promise<PrismaRolloutDelegate>);
43
-
44
- const recordSchema = z.object({
45
- tenantId: z.string().min(1, "tenantId is required"),
46
- threadId: z.string().min(1, "threadId is required"),
47
- seq: z.number().int().nonnegative(),
48
- at: z.string().min(1),
49
- payload: z.string(),
50
- });
51
-
52
- const querySchema = z.object({
53
- tenantId: z.string().min(1, "tenantId is required"),
54
- threadId: z.string().min(1, "threadId is required"),
55
- });
56
-
57
- async function resolve(
58
- resolver: RolloutDelegateResolver,
59
- tenantId: string,
60
- ): Promise<PrismaRolloutDelegate> {
61
- return typeof resolver === "function" ? resolver(tenantId) : resolver;
62
- }
63
-
64
- /**
65
- * Build a {@link RolloutPersistencePort} backed by Prisma.
66
- *
67
- * @param delegate a `PrismaRolloutDelegate`, or a resolver mapping a tenantId
68
- * to one (use the latter to obtain a per-tenant RLS client per call).
69
- */
70
- export function createPrismaRolloutPersistence(
71
- delegate: RolloutDelegateResolver,
72
- ): RolloutPersistencePort {
73
- return {
74
- async put(record): Promise<void> {
75
- const r = recordSchema.parse(record);
76
- const at = new Date(r.at);
77
- if (Number.isNaN(at.getTime())) {
78
- throw new Error(`invalid timestamp: ${r.at}`);
79
- }
80
- const db = await resolve(delegate, r.tenantId);
81
- // Fail-loud: any persistence rejection propagates to the caller.
82
- await db.create({
83
- data: {
84
- tenantId: r.tenantId,
85
- threadId: r.threadId,
86
- seq: r.seq,
87
- at,
88
- payload: r.payload,
89
- },
90
- });
91
- },
92
-
93
- async list(tenantId, threadId): Promise<{ seq: number; payload: string }[]> {
94
- const q = querySchema.parse({ tenantId, threadId });
95
- const db = await resolve(delegate, q.tenantId);
96
- const rows = await db.findMany({
97
- where: { tenantId: q.tenantId, threadId: q.threadId },
98
- orderBy: { seq: "asc" },
99
- select: { seq: true, payload: true },
100
- });
101
- return rows.map((row) => ({ seq: row.seq, payload: row.payload }));
102
- },
103
- };
104
- }
@@ -1,176 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { z } from "zod";
3
- import { isTurnTerminal, mergeTurnConfig, type TurnConfig } from "./model";
4
- import { DEFAULT_CAPABILITY_POLICY, DENIED, isApproval, resolveRuleDecision } from "./policy";
5
- import { METHOD_REGISTRY, resolveScope, scopeKey } from "./protocol";
6
- import {
7
- InMemoryRolloutStore,
8
- PERSISTED_OUTPUT_MAX_BYTES,
9
- replay,
10
- sanitizeForPersist,
11
- } from "./rollout";
12
- import {
13
- assertSafePosture,
14
- createHttpSandbox,
15
- NoExecutorConfiguredError,
16
- REFUSING_SANDBOX,
17
- SandboxDelegationError,
18
- } from "./sandbox";
19
- import { ToolRegistry } from "./tools";
20
-
21
- const baseConfig: TurnConfig = {
22
- model: "m",
23
- provider: "p",
24
- approvalPolicy: "on_request",
25
- capabilityPolicy: "external_sandbox",
26
- };
27
-
28
- describe("model", () => {
29
- it("merges overrides immutably", () => {
30
- const merged = mergeTurnConfig(baseConfig, { model: "x" });
31
- expect(merged.model).toBe("x");
32
- expect(baseConfig.model).toBe("m");
33
- });
34
- it("detects terminal turn events", () => {
35
- expect(
36
- isTurnTerminal({
37
- type: "turn.completed",
38
- usage: { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0 },
39
- }),
40
- ).toBe(true);
41
- expect(isTurnTerminal({ type: "turn.started" })).toBe(false);
42
- });
43
- });
44
-
45
- describe("policy", () => {
46
- it("defaults to external_sandbox posture", () => {
47
- expect(DEFAULT_CAPABILITY_POLICY.kind).toBe("external_sandbox");
48
- });
49
- it("forbidden rule auto-rejects regardless of policy", () => {
50
- expect(resolveRuleDecision("forbidden", { kind: "on_request" })).toBe("auto_reject");
51
- });
52
- it("prompt rule under never policy auto-rejects", () => {
53
- expect(resolveRuleDecision("prompt", { kind: "never" })).toBe("auto_reject");
54
- });
55
- it("prompt rule under granular respects the rules gate", () => {
56
- expect(
57
- resolveRuleDecision("prompt", {
58
- kind: "granular",
59
- config: {
60
- sandboxApproval: true,
61
- rules: false,
62
- skillApproval: false,
63
- requestPermissions: false,
64
- mcpElicitations: true,
65
- },
66
- }),
67
- ).toBe("auto_reject");
68
- });
69
- it("denied is not an approval", () => {
70
- expect(isApproval(DENIED)).toBe(false);
71
- expect(isApproval({ kind: "approved_for_session" })).toBe(true);
72
- });
73
- });
74
-
75
- describe("protocol", () => {
76
- it("scope key is tenant-prefixed and thread-isolated", () => {
77
- const a = resolveScope(METHOD_REGISTRY.turnStart, "t1", { threadId: "th1" });
78
- const b = resolveScope(METHOD_REGISTRY.turnStart, "t2", { threadId: "th1" });
79
- expect(scopeKey(a)).not.toBe(scopeKey(b)); // cross-tenant never shares a lane
80
- expect(scopeKey(a)).toContain("t:t1");
81
- });
82
- it("thread-scoped method without threadId fails closed", () => {
83
- expect(() => resolveScope(METHOD_REGISTRY.turnStart, "t1", {})).toThrow();
84
- });
85
- });
86
-
87
- describe("rollout", () => {
88
- it("caps oversized command output", () => {
89
- const big = "x".repeat(PERSISTED_OUTPUT_MAX_BYTES + 100);
90
- const out = sanitizeForPersist({
91
- id: "1",
92
- type: "command_execution",
93
- command: "c",
94
- aggregatedOutput: big,
95
- status: "completed",
96
- });
97
- expect((out as { aggregatedOutput: string }).aggregatedOutput.length).toBeLessThan(big.length);
98
- });
99
- it("replays an append-only log into derived state, bounded by compaction", async () => {
100
- const store = new InMemoryRolloutStore();
101
- const at = new Date().toISOString();
102
- await store.append({
103
- tenantId: "t",
104
- threadId: "th",
105
- type: "session_meta",
106
- config: baseConfig,
107
- at,
108
- });
109
- await store.append({
110
- tenantId: "t",
111
- threadId: "th",
112
- type: "event",
113
- at,
114
- event: { type: "item.completed", item: { id: "i1", type: "agent_message", text: "hi" } },
115
- });
116
- await store.append({
117
- tenantId: "t",
118
- threadId: "th",
119
- type: "compacted",
120
- summary: "summary",
121
- droppedThrough: "i1",
122
- at,
123
- });
124
- const proj = replay(await store.read("t", "th"));
125
- expect(proj?.compactionSummary).toBe("summary");
126
- expect(proj?.items).toHaveLength(0);
127
- });
128
- });
129
-
130
- describe("sandbox", () => {
131
- it("refuses execution by default (no in-process untrusted exec)", async () => {
132
- await expect(
133
- REFUSING_SANDBOX.exec({
134
- tenantId: "t",
135
- threadId: "th",
136
- command: "rm -rf /",
137
- capabilityPolicy: DEFAULT_CAPABILITY_POLICY,
138
- }),
139
- ).rejects.toBeInstanceOf(NoExecutorConfiguredError);
140
- });
141
- it("refuses danger_full_access without explicit opt-in", () => {
142
- expect(() => assertSafePosture({ kind: "danger_full_access" })).toThrow();
143
- expect(() => assertSafePosture({ kind: "danger_full_access" }, true)).not.toThrow();
144
- });
145
- it("surfaces a fail-closed isolator refusal as an error, never a fake result", async () => {
146
- const fakeFetch = (async () =>
147
- new Response(JSON.stringify({ error: "execution_refused" }), {
148
- status: 403,
149
- })) as unknown as typeof fetch;
150
- const sandbox = createHttpSandbox("http://isolator:8020", fakeFetch);
151
- await expect(
152
- sandbox.exec({
153
- tenantId: "org_a",
154
- threadId: "th_1",
155
- command: "rm -rf /",
156
- capabilityPolicy: DEFAULT_CAPABILITY_POLICY,
157
- }),
158
- ).rejects.toBeInstanceOf(SandboxDelegationError);
159
- });
160
- });
161
-
162
- describe("tools", () => {
163
- it("validates input and dispatches with tenant context", async () => {
164
- const reg = new ToolRegistry();
165
- reg.register(
166
- { name: "echo", description: "echo", inputSchema: z.object({ v: z.string() }) },
167
- async (input: { v: string }, ctx) => `${ctx.tenantId}:${input.v}`,
168
- );
169
- expect(await reg.dispatch("echo", { v: "hi" }, { tenantId: "t1", threadId: "th" })).toBe(
170
- "t1:hi",
171
- );
172
- await expect(
173
- reg.dispatch("echo", { v: 1 }, { tenantId: "t1", threadId: "th" }),
174
- ).rejects.toBeTruthy();
175
- });
176
- });